From 528a53f7f76f08af5902f4cfa8235380b3434ba0 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Mon, 25 Apr 2022 00:43:06 +0200 Subject: drafty types refactor with simple ascii package types.go compiles but that is about it, here be dragons. Pushing so that we can get an idea of what this refactor would roughly look like. --- pkg/ascii/ascii.go | 306 ------------------- pkg/ascii/ascii_test.go | 455 --------------------------- pkg/types/ascii/ascii.go | 173 +++++++++++ pkg/types/ascii/ascii_test.go | 290 ++++++++++++++++++ pkg/types/binary/ssh/ssh.go | 34 +++ pkg/types/binary/trunnel/trunnel.go | 33 ++ pkg/types/crypto.go | 30 -- pkg/types/crypto_test.go | 64 ---- pkg/types/encoding.go | 19 -- pkg/types/encoding_test.go | 29 -- pkg/types/endpoint.go | 22 -- pkg/types/leaf.go | 118 -------- pkg/types/leaf_test.go | 302 ------------------ pkg/types/proof.go | 46 --- pkg/types/proof_test.go | 138 --------- pkg/types/tree_head.go | 90 ------ pkg/types/tree_head_test.go | 262 ---------------- pkg/types/types.go | 590 ++++++++++++++++++++++++++++++++++++ 18 files changed, 1120 insertions(+), 1881 deletions(-) delete mode 100644 pkg/ascii/ascii.go delete mode 100644 pkg/ascii/ascii_test.go create mode 100644 pkg/types/ascii/ascii.go create mode 100644 pkg/types/ascii/ascii_test.go create mode 100644 pkg/types/binary/ssh/ssh.go create mode 100644 pkg/types/binary/trunnel/trunnel.go delete mode 100644 pkg/types/crypto.go delete mode 100644 pkg/types/crypto_test.go delete mode 100644 pkg/types/encoding.go delete mode 100644 pkg/types/encoding_test.go delete mode 100644 pkg/types/endpoint.go delete mode 100644 pkg/types/leaf.go delete mode 100644 pkg/types/leaf_test.go delete mode 100644 pkg/types/proof.go delete mode 100644 pkg/types/proof_test.go delete mode 100644 pkg/types/tree_head.go delete mode 100644 pkg/types/tree_head_test.go create mode 100644 pkg/types/types.go diff --git a/pkg/ascii/ascii.go b/pkg/ascii/ascii.go deleted file mode 100644 index d91a240..0000000 --- a/pkg/ascii/ascii.go +++ /dev/null @@ -1,306 +0,0 @@ -// package ascii implements an ASCII key-value parser. -// -// The top-most (de)serialize must operate on a struct pointer. A struct may -// contain other structs, in which case all tag names should be unique. Public -// fields without tag names are ignored. Private fields are also ignored. -// -// The supported field types are: -// - struct -// - string (no empty strings) -// - uint64 (only digits in ASCII representation) -// - byte array (only lower-case hex in ASCII representation) -// - slice of uint64 (no empty slices) -// - slice of byte array (no empty slices) -// -// A key must not contain an encoding's end-of-key value. -// A value must not contain an encoding's end-of-value value. -// -// For additional details, please refer to the Sigsum v0 API documentation. -// -package ascii - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "reflect" - "strconv" - "strings" - - "git.sigsum.org/sigsum-go/pkg/hex" -) - -var StdEncoding = NewEncoding("ascii", "=", "\n") - -type Encoding struct { - identifier string - endOfKey string - endOfValue string -} - -func NewEncoding(id, eok, eov string) *Encoding { - return &Encoding{ - identifier: id, - endOfKey: eok, - endOfValue: eov, - } -} - -type someValue struct { - v reflect.Value - ok bool -} - -// Serialize tries to serialize an interface as ASCII key-value pairs -func (e *Encoding) Serialize(w io.Writer, i interface{}) error { - v, err := dereferenceStructPointer(i) - if err != nil { - return err - } - - t := v.Type() - for i := 0; i < t.NumField(); i++ { - switch v.Field(i).Type().Kind() { - case reflect.Struct: - if err := e.Serialize(w, v.Field(i).Addr().Interface()); err != nil { - return err - } - default: - if t.Field(i).PkgPath != "" { - continue // skip private field - } - key, ok := t.Field(i).Tag.Lookup(e.identifier) - if !ok { - continue // skip public field without tag - } - - if strings.Contains(key, e.endOfKey) { - return fmt.Errorf("ascii: key %q contains end-of-key character", key) - } - if err := e.write(w, key, v.Field(i)); err != nil { - return err - } - } - } - return nil -} - -func (e *Encoding) write(w io.Writer, key string, v reflect.Value) error { - t := v.Type() - switch t { - case reflect.TypeOf(uint64(0)): - val := fmt.Sprintf("%d", v.Uint()) - return e.writeOne(w, key, val) - } - - k := t.Kind() - switch k { - case reflect.Array: - if kind := t.Elem().Kind(); kind != reflect.Uint8 { - return fmt.Errorf("ascii: array kind not supported: %v", kind) - } - - arrayLen := v.Len() - array := make([]byte, arrayLen, arrayLen) - for i := 0; i < arrayLen; i++ { - array[i] = uint8(v.Index(i).Uint()) - } - - val := hex.Serialize(array) - return e.writeOne(w, key, val) - - case reflect.Slice: - kind := t.Elem().Kind() - if kind != reflect.Array && kind != reflect.Uint64 { - return fmt.Errorf("ascii: slice kind not supported: %v", kind) - } - if v.Len() == 0 { - return fmt.Errorf("ascii: slice must not be empty") - } - - var err error - for i := 0; i < v.Len(); i++ { - err = e.write(w, key, v.Index(i)) - } - return err - - case reflect.String: - if v.Len() == 0 { - return fmt.Errorf("ascii: string must not be empty") - } - return e.writeOne(w, key, v.String()) - } - - return fmt.Errorf("ascii: unsupported type %v and kind %v", t, k) -} - -func (e *Encoding) writeOne(w io.Writer, key, value string) error { - _, err := w.Write([]byte(key + e.endOfKey + value + e.endOfValue)) - return err -} - -// Deserialize tries to deserialize a buffer of ASCII key-value pairs -func (e *Encoding) Deserialize(r io.Reader, i interface{}) error { - m := make(map[string]*someValue) - if err := e.mapKeys(i, m); err != nil { - return err - } - - buf, err := ioutil.ReadAll(r) - if err != nil { - return fmt.Errorf("ascii: failed reading incoming buffer") - } - - // trim end of buffer so that loop does not run on an empty line - if len(buf) <= len(e.endOfValue) { - return fmt.Errorf("ascii: buffer contains no key-value pair") - } - offset := len(buf) - len(e.endOfValue) - if !bytes.Equal(buf[offset:], []byte(e.endOfValue)) { - return fmt.Errorf("ascii: buffer must end with endOfValue") - } - buf = buf[:offset] - - for _, kv := range bytes.Split(buf, []byte(e.endOfValue)) { - split := bytes.Split(kv, []byte(e.endOfKey)) - if len(split) == 1 { - return fmt.Errorf("ascii: missing key-value pair in %q", string(kv)) - } - - key := string(split[0]) - value := string(bytes.Join(split[1:], nil)) - ref, ok := m[key] - if !ok { - return fmt.Errorf("ascii: unexpected key %q", key) - } - if len(value) == 0 { - fmt.Errorf("ascii: missing value for key %q", key) - } - if err := setKey(ref, key, value); err != nil { - return err - } - } - return requireValues(m) -} - -func (e *Encoding) mapKeys(i interface{}, m map[string]*someValue) error { - v, err := dereferenceStructPointer(i) - if err != nil { - return err - } - - t := v.Type() - for i := 0; i < t.NumField(); i++ { - switch v.Field(i).Type().Kind() { - case reflect.Struct: - i := v.Field(i).Addr().Interface() - e.mapKeys(i, m) // return is always nil - default: - if t.Field(i).PkgPath != "" { - continue // skip private field - } - key, ok := t.Field(i).Tag.Lookup(e.identifier) - if !ok { - continue // skip public field without tag - } - m[key] = &someValue{ - v: v.Field(i), - } - } - } - return nil -} - -func setKey(ref *someValue, key, value string) error { - v := ref.v - if v.Kind() == reflect.Ptr && !v.IsNil() { - v = v.Elem() - } - - t := v.Type() - switch t { - case reflect.TypeOf(uint64(0)): - num, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return err - } - - ref.ok = true - v.SetUint(num) - return nil - } - - k := t.Kind() - switch k { - case reflect.Array: - arrayLen := v.Len() - b, err := hex.Deserialize(value) - if err != nil { - return err - } - if len(b) != arrayLen { - return fmt.Errorf("ascii: invalid array size for key %q", key) - } - - ref.ok = true - reflect.Copy(v, reflect.ValueOf(b)) - return nil - - case reflect.Slice: - sliceType := t - kind := sliceType.Elem().Kind() - if kind != reflect.Array && kind != reflect.Uint64 { - return fmt.Errorf("ascii: slice kind not supported: %v", kind) - } - - if v.IsNil() { - v.Set(reflect.MakeSlice(sliceType, 0, 0)) - } - sv := &someValue{ - v: reflect.New(sliceType.Elem()), - } - if err := setKey(sv, key, value); err != nil { - return err - } - - ref.ok = true - v.Set(reflect.Append(v, sv.v.Elem())) - return nil - - case reflect.String: - if len(value) == 0 { - return fmt.Errorf("ascii: string must not be empty") - } - - ref.ok = true - v.SetString(value) - return nil - } - - return fmt.Errorf("ascii: unsupported type %v and kind %v", t, k) -} - -func requireValues(m map[string]*someValue) error { - for k, v := range m { - if !v.ok { - return fmt.Errorf("ascii: missing value for key %q", k) - } - } - return nil -} - -func dereferenceStructPointer(i interface{}) (*reflect.Value, error) { - v := reflect.ValueOf(i) - if v.Kind() != reflect.Ptr { - return nil, fmt.Errorf("ascii: interface value must be pointer") - } - if v.IsNil() { - return nil, fmt.Errorf("ascii: interface value must be non-nil pointer") - } - v = v.Elem() - if v.Type().Kind() != reflect.Struct { - return nil, fmt.Errorf("ascii: interface value must point to struct") - } - return &v, nil -} diff --git a/pkg/ascii/ascii_test.go b/pkg/ascii/ascii_test.go deleted file mode 100644 index 8aee70c..0000000 --- a/pkg/ascii/ascii_test.go +++ /dev/null @@ -1,455 +0,0 @@ -package ascii - -import ( - "bytes" - "reflect" - "testing" -) - -type testStruct struct { - Num uint64 `ascii/test:"num"` - Struct testStructOther - Skip uint64 - skip uint64 -} - -type testStructOther struct { - Array testArray `ascii/test:"array"` - Slice []testArray `ascii/test:"slice"` - String string `ascii/test:"string"` -} - -type testArray [2]byte - -type testStructUnsupportedType struct { - ByteSlice []byte `ascii/test:"byte_slice"` -} - -func TestSerialize(t *testing.T) { - e := NewEncoding("ascii/test", "<--", ";;") - for _, table := range []struct { - desc string - want string - err bool - i interface{} - }{ - { - desc: "invalid: not pointer to struct", - err: true, - i: testStruct{}, - }, - { - desc: "invalid: struct with invalid key", - err: true, - i: &struct { - Num uint64 `ascii/test:"num<--nom"` - }{ - Num: 1, - }, - }, - { - desc: "invalid: struct with invalid type", - err: true, - i: &testStructUnsupportedType{ - ByteSlice: []byte("hellow"), - }, - }, - { - desc: "invalid: struct with invalid type and kind", - err: true, - i: &struct { - Struct testStructUnsupportedType - }{ - Struct: testStructUnsupportedType{ - ByteSlice: []byte("hellow"), - }, - }, - }, - { - desc: "valid", - want: "num<--1;;array<--01fe;;slice<--01fe;;slice<--00ff;;string<--hellow;;", - i: &testStruct{ - Num: 1, - Struct: testStructOther{ - Array: testArray{1, 254}, - Slice: []testArray{ - testArray{1, 254}, - testArray{0, 255}, - }, - String: "hellow", - }, - }, - }, - } { - b := bytes.NewBuffer(nil) - err := e.Serialize(b, table.i) - if got, want := err != nil, table.err; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := string(b.Bytes()), table.want; got != want { - t.Errorf("got buf %s but wanted %s in test %q", got, want, table.desc) - } - } -} - -func TestWrite(t *testing.T) { - e := NewEncoding("ascii/test", "<--", ";;") - for _, table := range []struct { - desc string - want string - err bool - i interface{} - }{ - { - desc: "invalid: array with wrong type", - err: true, - i: [2]string{"first", "second"}, - }, - { - desc: "invalid: slice with wrong type", - err: true, - i: []string{"first", "second"}, - }, - { - desc: "invalid: empty slice with right type", - err: true, - i: make([][2]byte, 0), - }, - { - desc: "invalid: empty string", - err: true, - i: "", - }, - { - desc: "invalid: unsupported type and kind", - err: true, - i: int32(0), - }, - { - desc: "valid: uint64", - want: "some key<--1;;", - i: uint64(1), - }, - { - desc: "valid: byte array", - want: "some key<--01fe;;", - i: [2]byte{1, 254}, - }, - { - desc: "valid: slice array", - want: "some key<--01fe;;some key<--00ff;;", - i: [][2]byte{ - [2]byte{1, 254}, - [2]byte{0, 255}, - }, - }, - { - desc: "valid: slice uint64", - want: "some key<--1;;some key<--2;;", - i: []uint64{1, 2}, - }, - { - desc: "valid: string", - want: "some key<--some value;;", - i: "some value", - }, - } { - buf := bytes.NewBuffer(nil) - err := e.write(buf, "some key", reflect.ValueOf(table.i)) - if got, want := err != nil, table.err; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := string(buf.Bytes()), table.want; got != want { - t.Errorf("got buf %s but wanted %s in test %q", got, want, table.desc) - } - } -} - -func TestWriteOne(t *testing.T) { - buf := bytes.NewBuffer(nil) - e := NewEncoding("ascii/test", "<--", ";;") - e.writeOne(buf, "some key", "some value") - want := "some key<--some value;;" - if got := string(buf.Bytes()); got != want { - t.Errorf("got buf %s but wanted %s", got, want) - } -} - -func TestDeserialize(t *testing.T) { - e := NewEncoding("ascii/test", "<--", ";;") - for _, table := range []struct { - desc string - buf string - want interface{} - err bool - }{ - { - desc: "invalid: interface must be pointer to struct", - buf: ";", - want: uint64(0), - err: true, - }, - { - desc: "invalid: buffer too small", - buf: ";", - want: testStruct{}, - err: true, - }, - { - desc: "invalid: buffer must end with endOfValue", - buf: "num<--1;;string<--hellow;;array<--01fe;;slice<--01fe;;slice<--00ff^^", - want: testStruct{}, - err: true, - }, - { - desc: "invalid: missing key num", - buf: "string<--hellow;;array<--01fe;;slice<--01fe;;slice<--00ff;;", - want: testStruct{}, - err: true, - }, - { - desc: "invalid: missing key-value pair on num line", - buf: "string<--hellow;;num;;array<--01fe;;slice<--01fe;;slice<--00ff;;", - want: testStruct{}, - err: true, - }, - { - desc: "invalid: missing value for key num", - buf: "num<--;;string<--hellow;;array<--01fe;;slice<--01fe;;slice<--00ff;;", - want: testStruct{}, - err: true, - }, - { - desc: "invalid: value for key num must be digits only", - buf: "num<--+1;;string<--hellow;;array<--01fe;;slice<--01fe;;slice<--00ff;;", - want: testStruct{}, - err: true, - }, - { - desc: "invalid: missing field for key num2", - buf: "num<--1;;string<--hellow;;num2<--2;;array<--01fe;;slice<--01fe;;slice<--00ff;;", - want: testStruct{}, - err: true, - }, - { - desc: "valid", - buf: "num<--1;;string<--hellow;;array<--01fe;;slice<--01fe;;slice<--00ff;;", - want: testStruct{ - Num: 1, - Struct: testStructOther{ - Array: testArray{1, 254}, - Slice: []testArray{ - testArray{1, 254}, - testArray{0, 255}, - }, - String: "hellow", - }, - }, - }, - } { - v := reflect.New(reflect.TypeOf(table.want)) - err := e.Deserialize(bytes.NewBuffer([]byte(table.buf)), v.Interface()) - if got, want := err != nil, table.err; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - - v = v.Elem() // have pointer to struct, get just struct as in table - if got, want := v.Interface(), table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got interface %v but wanted %v in test %q", got, want, table.desc) - } - } - -} - -func TestMapKeys(t *testing.T) { - s := testStruct{} - m := make(map[string]*someValue) - e := NewEncoding("ascii/test", "<--", ";;") - if err := e.mapKeys(s, m); err == nil { - t.Errorf("expected mapping to fail without pointer") - } - if err := e.mapKeys(&s, m); err != nil { - t.Errorf("expected mapping to succeed") - return - } - - wantKeys := []string{"num", "array", "slice", "string"} - if got, want := len(m), len(wantKeys); got != want { - t.Errorf("got %d keys, wanted %d", got, want) - } - for _, key := range wantKeys { - if _, ok := m[key]; !ok { - t.Errorf("expected key %q in map", key) - } - } -} - -func TestSetKey(t *testing.T) { - for _, table := range []struct { - desc string - key string - value string - want interface{} - err bool - }{ - { - desc: "invalid: unsupported type and kind", - key: "num", - value: "1", - want: uint32(1), - err: true, - }, - // uint64 - { - desc: "invalid: uint64: underflow", - key: "num", - value: "-1", - want: uint64(0), - err: true, - }, - { - desc: "invalid: uint64: overflow", - key: "num", - value: "18446744073709551616", - want: uint64(0), - err: true, - }, - { - desc: "invalid: uint64: not a number", - key: "num", - value: "+1", - want: uint64(0), - err: true, - }, - { - desc: "invalid: uint64: number with white space", - key: "num", - value: "1 ", - want: uint64(0), - err: true, - }, - { - desc: "valid: uint64", - key: "num", - value: "1", - want: uint64(1), - }, - // string - { - desc: "invalid: string: empty", - key: "string", - value: "", - want: "", - err: true, - }, - { - desc: "valid: string", - key: "string", - value: "hellow", - want: "hellow", - }, - // array - { - desc: "invalid: array: bad hex", - key: "array", - value: "00fE", - want: [2]byte{}, - err: true, - }, - { - desc: "invalid: array: wrong size", - key: "array", - value: "01fe", - want: [3]byte{}, - err: true, - }, - { - desc: "valid: array", - key: "num", - value: "01fe", - want: [2]byte{1, 254}, - }, - // slice - { - desc: "invalid: slice: bad type", - key: "slice", - value: "01fe", - want: []string{ - "hello", - }, - err: true, - }, - { - desc: "invalid: bad hex", - key: "slice", - value: "01FE", - want: [][2]byte{ - [2]byte{1, 254}, - }, - err: true, - }, - { - desc: "valid: slice", - key: "slice", - value: "01fe", - want: [][2]byte{ - [2]byte{1, 254}, - }, - }, - { - desc: "valid: slice", - key: "slice", - value: "4711", - want: []uint64{4711}, - }, - } { - ref := &someValue{ - v: reflect.New(reflect.TypeOf(table.want)), - } - err := setKey(ref, table.key, table.value) - if got, want := err != nil, table.err; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - - ref.v = ref.v.Elem() // get same type as table - if got, want := ref.v.Interface(), table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got interface %v but wanted %v in test %q", got, want, table.desc) - } - if got, want := ref.ok, true; got != want { - t.Errorf("got ok %v but wanted %v in test %q", got, want, table.desc) - } - } -} - -func TestDereferenceStructPointer(t *testing.T) { - var ts testStruct - if _, err := dereferenceStructPointer(ts); err == nil { - t.Errorf("should have failed dereferencing non-pointer") - } - - var tsp *testStruct - if _, err := dereferenceStructPointer(tsp); err == nil { - t.Errorf("should have failed dereferencing nil-pointer") - } - - var ta testArray - if _, err := dereferenceStructPointer(&ta); err == nil { - t.Errorf("should have failed dereferencing non-struct pointer") - } - - if _, err := dereferenceStructPointer(&ts); err != nil { - t.Errorf("should have succeeded dereferencing pointer to struct") - } -} diff --git a/pkg/types/ascii/ascii.go b/pkg/types/ascii/ascii.go new file mode 100644 index 0000000..92ead4b --- /dev/null +++ b/pkg/types/ascii/ascii.go @@ -0,0 +1,173 @@ +// package ascii provides ASCII key-value (de)serialization, see §3: +// +// https://git.sigsum.org/sigsum/plain/doc/api.md +// +// Write key-value pairs to a buffer using the WritePair() method. +// +// Read key-value pairs from a buffer using the ReadPairs() method. It takes as +// input a function that parses the buffer using a map's Dequeue*() methods. +// +// XXX: add a usage example, until then see TestReadPairs(). +// +package ascii + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strconv" + + "git.sigsum.org/sigsum-go/pkg/hex" +) + +const ( + EndOfKey = "=" + EndOfValue = "\n" +) + +var ( + endOfKey = []byte(EndOfKey) + endOfValue = []byte(EndOfValue) +) + +// WritePair writes a key-value pair +func WritePair(w io.Writer, key, value string) error { + _, err := w.Write(bytes.Join([][]byte{[]byte(key), endOfKey, []byte(value), endOfValue}, nil)) + return err +} + +// ReadPairs parses key-value pairs strictly using the provided parse function +func ReadPairs(r io.Reader, parse func(*Map) error) error { + m, err := newMap(r) + if err != nil { + return err + } + if err := parse(&m); err != nil { + return err + } + return m.done() +} + +// Map is a map of ASCII key-value pairs. An ASCII key has a list of ASCII +// values. A value can be dequeued for a key as a certain type. Call Done() +// after dequeing all expected values to be strict about no redundant values. +type Map map[string][]string + +// NumValues returns the number of values for a given key. If the key does not +// exist, the number of values is per definition zero. +func (m *Map) NumValues(key string) uint64 { + values, ok := (*m)[key] + if !ok { + return 0 + } + return uint64(len(values)) +} + +// DequeueString dequeues a string value for a given key. +func (m *Map) DequeueString(key string, str *string) (err error) { + *str, err = m.dequeue(key) + if err != nil { + return fmt.Errorf("dequeue: %w", err) + } + return nil +} + +// DequeueUint64 dequeues an uint64 value for a given key. +func (m *Map) DequeueUint64(key string, num *uint64) error { + v, err := m.dequeue(key) + if err != nil { + return fmt.Errorf("dequeue: %w", err) + } + *num, err = strconv.ParseUint(v, 10, 64) + if err != nil { + return fmt.Errorf("invalid uint64: %w", err) + } + return nil +} + +// DequeueArray dequeues an array value for a given key +func (m *Map) DequeueArray(key string, arr []byte) error { + v, err := m.dequeue(key) + if err != nil { + return fmt.Errorf("dequeue: %w", err) + } + b, err := hex.Deserialize(v) + if err != nil { + return fmt.Errorf("invalid array: %w", err) + } + if n := len(b); n != len(arr) { + return fmt.Errorf("invalid array size %d", n) + } + copy(arr, b) + return nil +} + +// dequeue dequeues a value for a given key +func (m *Map) dequeue(key string) (string, error) { + _, ok := (*m)[key] + if !ok { + return "", fmt.Errorf("missing key %q", key) + } + if len((*m)[key]) == 0 { + return "", fmt.Errorf("missing value for key %q", key) + } + + value := (*m)[key][0] + (*m)[key] = (*m)[key][1:] + return value, nil +} + +// done checks that there are no keys with remaining values +func (m *Map) done() error { + for k, v := range *m { + if len(v) != 0 { + return fmt.Errorf("remaining values for key %q", k) + } + } + return nil +} + +// newMap parses ASCII-encoded key-value pairs into a map +func newMap(r io.Reader) (m Map, err error) { + buf, err := ioutil.ReadAll(r) + if err != nil { + return m, fmt.Errorf("read: %w", err) + } + + b, err := trimEnd(buf) + if err != nil { + return m, fmt.Errorf("malformed input: %w", err) + } + + m = make(map[string][]string) + for i, kv := range bytes.Split(b, endOfValue) { + split := bytes.Split(kv, endOfKey) + if len(split) == 1 { + return m, fmt.Errorf("no key-value pair on line %d: %q", i+1, string(kv)) + } + + key := string(split[0]) + value := string(bytes.Join(split[1:], endOfKey)) + if _, ok := m[key]; !ok { + m[key] = make([]string, 0, 1) + } + m[key] = append(m[key], value) + } + + return m, nil +} + +// trimEnd ensures that we can range over the output of a split on endOfValue +// without the last itteration being an empty string. Note that it would not be +// correct to simply skip the last itteration. That line could me malformed. +func trimEnd(buf []byte) ([]byte, error) { + if len(buf) <= len(endOfValue) { + return nil, fmt.Errorf("buffer contains no key-value pair") + } + offset := len(buf) - len(endOfValue) + if !bytes.Equal(buf[offset:], endOfValue) { + return nil, fmt.Errorf("buffer must end with %q", EndOfValue) + } + return buf[:offset], nil +} diff --git a/pkg/types/ascii/ascii_test.go b/pkg/types/ascii/ascii_test.go new file mode 100644 index 0000000..d0d578b --- /dev/null +++ b/pkg/types/ascii/ascii_test.go @@ -0,0 +1,290 @@ +package ascii + +import ( + "bytes" + "fmt" + "io" + "reflect" + "testing" +) + +func TestWritePair(t *testing.T) { + key := "red" + value := "1" + want := "red=1\n" + + buf := bytes.NewBuffer(nil) + if err := WritePair(buf, key, value); err != nil { + t.Errorf("write pair: %v", err) + } + if got := string(buf.Bytes()); got != want { + t.Errorf("got key-value pair %q but wanted %q", got, want) + } +} + +func TestReadPairs(t *testing.T) { + type collection struct { + String string + Num uint64 + Array [2]byte + Arrays [][2]byte + } + + var c collection + parser := func(m *Map) error { + if err := m.DequeueString("string", &c.String); err != nil { + return fmt.Errorf("string: %w", err) + } + if err := m.DequeueUint64("num", &c.Num); err != nil { + return fmt.Errorf("num: %w", err) + } + if err := m.DequeueArray("array", c.Array[:]); err != nil { + return fmt.Errorf("array: %w", err) + } + + n := m.NumValues("arrays") + if n == 0 { + return fmt.Errorf("arrays: empty") + } + c.Arrays = make([][2]byte, 0, n) + for i := uint64(0); i < n; i++ { + var array [2]byte + if err := m.DequeueArray("arrays", array[:]); err != nil { + return fmt.Errorf("%d: arrays: %w", i+1, err) + } + c.Arrays = append(c.Arrays, array) + } + return nil + } + + for _, table := range []struct { + desc string + input io.Reader + want *collection + }{ + { + desc: "invalid: cannot parse into map", + input: bytes.NewBufferString("string=a"), + }, + { + desc: "invalid: malformed value", + input: bytes.NewBufferString("string=a\nnum=a\narray=0101\narrays=0101\narrays=ffff\n"), + }, + { + desc: "invalid: remaining value", + input: bytes.NewBufferString("string=a\nnum=1\narray=0101\narrays=0101\narrays=ffff\nhello=abc\n"), + }, + { + desc: "valid", + input: bytes.NewBufferString("string=a\nnum=1\narray=0101\narrays=0101\narrays=ffff\n"), + want: &collection{ + String: "a", + Num: 1, + Array: [2]byte{1, 1}, + Arrays: [][2]byte{ + [2]byte{1, 1}, + [2]byte{255, 255}, + }, + }, + }, + } { + c = collection{} + err := ReadPairs(table.input, parser) + if got, want := err != nil, table.want == nil; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + if err != nil { + continue + } + if got, want := c, *table.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s: got collection\n%+v\nbut wanted\n%+v", table.desc, got, want) + } + } +} + +func TestNewMap(t *testing.T) { + for _, table := range []struct { + desc string + input io.Reader + want Map + }{ + { + desc: "invalid: trim: no key-value pairs", + input: bytes.NewBuffer(nil), + }, + { + desc: "invalid: trim: ending", + input: bytes.NewBufferString("red=1\nblue=2"), + }, + { + desc: "invalid: missing key-value pair on line", + input: bytes.NewBufferString("red=1\n\nblue=2\n"), + }, + { + desc: "valid", + input: bytes.NewBufferString("red=1\nblue=1\nblue=2\ngreen=1\nred==2\n"), + want: map[string][]string{ + "red": []string{"1", "=2"}, + "blue": []string{"1", "2"}, + "green": []string{"1"}, + }, + }, + } { + m, err := newMap(table.input) + if got, want := err != nil, table.want == nil; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + if err != nil { + continue + } + if got, want := m, table.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s: got map\n%v\nbut wanted\n%v", table.desc, got, want) + } + } +} + +func TestDone(t *testing.T) { + for _, table := range []struct { + desc string + input Map + wantOK bool + }{ + { + desc: "valid: keys with no values", + input: map[string][]string{ + "red": []string{"1"}, + "blue": []string{}, + }, + }, + { + desc: "valid: empty", + input: map[string][]string{}, + wantOK: true, + }, + { + desc: "valid: keys with no values", + input: map[string][]string{ + "red": []string{}, + "blue": []string{}, + }, + wantOK: true, + }, + } { + err := table.input.done() + if got, want := err != nil, !table.wantOK; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + } +} + +func TestNumValues(t *testing.T) { + var m Map = map[string][]string{ + "red": []string{}, + "blue": []string{"1"}, + "green": []string{"a", "bc", "def"}, + } + if got, want := m.NumValues("orange"), uint64(0); got != want { + t.Errorf("orange: got %d values but wanted %d", got, want) + } + if got, want := m.NumValues("red"), uint64(0); got != want { + t.Errorf("red: got %d values but wanted %d", got, want) + } + if got, want := m.NumValues("blue"), uint64(1); got != want { + t.Errorf("blue: got %d values but wanted %d", got, want) + } + if got, want := m.NumValues("green"), uint64(3); got != want { + t.Errorf("green: got %d values but wanted %d", got, want) + } +} + +func TestDequeue(t *testing.T) { + var first Map = map[string][]string{ + "red": []string{}, + "blue": []string{"1"}, + "green": []string{"a", "bc", "def"}, + } + if _, err := first.dequeue("orange"); err == nil { + t.Errorf("orange: expected dequeue error but got none") + } + if _, err := first.dequeue("red"); err == nil { + t.Errorf("red: expected dequeue error but got none") + } + + str, err := first.dequeue("green") + if err != nil { + t.Errorf("green: expected dequeue to succeed but got error: %v", err) + } + if got, want := str, "a"; got != want { + t.Errorf("green: got value %q but wanted %q", got, want) + } + + var second Map = map[string][]string{ + "red": []string{}, + "blue": []string{"1"}, + "green": []string{"bc", "def"}, + } + if got, want := second, first; !reflect.DeepEqual(got, want) { + t.Errorf("got map\n%v\nbut wanted\n%v", got, want) + } +} + +func TestDequeueString(t *testing.T) { + var first Map = map[string][]string{ + "blue": []string{"1"}, + } + + var str string + if err := first.DequeueString("blue", &str); err != nil { + t.Errorf("expected dequeue ok but got error: %v", err) + return + } + if got, want := str, "1"; got != want { + t.Errorf("got string %q but wanted %q", got, want) + } + if err := first.DequeueString("blue", &str); err == nil { + t.Errorf("expected dequeue error but got none") + } +} + +func TestDequeueUint64(t *testing.T) { + var first Map = map[string][]string{ + "blue": []string{"a", "1"}, + } + + var num uint64 + if err := first.DequeueUint64("blue", &num); err == nil { + t.Errorf("expected parse error but got none") + } + if err := first.DequeueUint64("blue", &num); err != nil { + t.Errorf("expected dequeue success but got error: %v", err) + } + if got, want := num, uint64(1); got != want { + t.Errorf("got number %d but wanted %d", got, want) + } + if err := first.DequeueUint64("blue", &num); err == nil { + t.Errorf("expected dequeue error but got none") + } +} + +func TestDequeueArray(t *testing.T) { + var first Map = map[string][]string{ + "blue": []string{"00FF", "0001ff", "00ff"}, + } + + var arr [2]byte + if err := first.DequeueArray("blue", arr[:]); err == nil { + t.Errorf("expected parse error but got none (bad hex)") + } + if err := first.DequeueArray("blue", arr[:]); err == nil { + t.Errorf("expected parse error but got none (bad length)") + } + if err := first.DequeueArray("blue", arr[:]); err != nil { + t.Errorf("expected dequeue success but got error: %v", err) + } + if got, want := arr, [2]byte{0, 255}; got != want { + t.Errorf("got array %v but wanted %v", got, want) + } + if err := first.DequeueArray("blue", arr[:]); err == nil { + t.Errorf("expected dequeue error but got none") + } +} diff --git a/pkg/types/binary/ssh/ssh.go b/pkg/types/binary/ssh/ssh.go new file mode 100644 index 0000000..9693476 --- /dev/null +++ b/pkg/types/binary/ssh/ssh.go @@ -0,0 +1,34 @@ +// package ssh provides selected parts of the SSH data format, see: +// +// - https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig +// - https://datatracker.ietf.org/doc/html/rfc4251#section-5 +// +package ssh + +import ( + "bytes" + "encoding/binary" +) + +// ToSignBlob outputs the raw bytes to be signed for a given namespace and +// message. The reserved string is empty and the specified hash is SHA256. +func ToSignBlob(namespace string, hashedMessage []byte) []byte { + buf := bytes.NewBuffer(nil) + + buf.Write([]byte("SSHSIG")) + addString(buf, namespace) + addString(buf, "") + addString(buf, "sha256") + addString(buf, string(hashedMessage[:])) + + return buf.Bytes() +} + +func addUint32(buf *bytes.Buffer, num uint32) { + binary.Write(buf, binary.BigEndian, num) +} + +func addString(buf *bytes.Buffer, str string) { + addUint32(buf, uint32(len(str))) + buf.Write([]byte(str)) +} diff --git a/pkg/types/binary/trunnel/trunnel.go b/pkg/types/binary/trunnel/trunnel.go new file mode 100644 index 0000000..fbf41f9 --- /dev/null +++ b/pkg/types/binary/trunnel/trunnel.go @@ -0,0 +1,33 @@ +// package trunnel provides selected Trunnel primitives, see: +// +// - https://gitlab.torproject.org/tpo/core/trunnel/-/blob/main/doc/trunnel.md +package trunnel + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +func Uint64(buf *bytes.Buffer, num *uint64) error { + if err := binary.Read(buf, binary.BigEndian, num); err != nil { + return fmt.Errorf("uint64: %w", err) + } + return nil +} + +func Array(buf *bytes.Buffer, arr []byte) error { + if _, err := io.ReadFull(buf, arr); err != nil { + return fmt.Errorf("array[%d]: %w", len(arr), err) + } + return nil +} + +func AddUint64(buf *bytes.Buffer, num uint64) { + binary.Write(buf, binary.BigEndian, num) +} + +func AddArray(buf *bytes.Buffer, arr []byte) { + buf.Write(arr[:]) +} diff --git a/pkg/types/crypto.go b/pkg/types/crypto.go deleted file mode 100644 index df93108..0000000 --- a/pkg/types/crypto.go +++ /dev/null @@ -1,30 +0,0 @@ -package types - -import ( - "crypto/ed25519" - "crypto/sha256" -) - -const ( - HashSize = sha256.Size - SignatureSize = ed25519.SignatureSize - PublicKeySize = ed25519.PublicKeySize - - LeafNodePrefix = byte(0x00) - InteriorNodePrefix = byte(0x01) -) - -type ( - Hash [HashSize]byte - Signature [SignatureSize]byte - PublicKey [PublicKeySize]byte -) - -func HashFn(buf []byte) *Hash { - var hash Hash = sha256.Sum256(buf) - return &hash -} - -func LeafHash(buf []byte) *Hash { - return HashFn(append([]byte{LeafNodePrefix}, buf...)) -} diff --git a/pkg/types/crypto_test.go b/pkg/types/crypto_test.go deleted file mode 100644 index d95d5fa..0000000 --- a/pkg/types/crypto_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package types - -import ( - "crypto" - "crypto/ed25519" - "crypto/rand" - "io" - "testing" -) - -type testSigner struct { - PublicKey PublicKey - Signature Signature - Error error -} - -func (ts *testSigner) Public() crypto.PublicKey { - return ed25519.PublicKey(ts.PublicKey[:]) -} - -func (ts *testSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - return ts.Signature[:], ts.Error -} - -func newKeyPair(t *testing.T) (crypto.Signer, PublicKey) { - vk, sk, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatal(err) - } - - var pub PublicKey - copy(pub[:], vk[:]) - return sk, pub -} - -func newHashBufferInc(t *testing.T) *Hash { - t.Helper() - - var buf Hash - for i := 0; i < len(buf); i++ { - buf[i] = byte(i) - } - return &buf -} - -func newSigBufferInc(t *testing.T) *Signature { - t.Helper() - - var buf Signature - for i := 0; i < len(buf); i++ { - buf[i] = byte(i) - } - return &buf -} - -func newPubBufferInc(t *testing.T) *PublicKey { - t.Helper() - - var buf PublicKey - for i := 0; i < len(buf); i++ { - buf[i] = byte(i) - } - return &buf -} diff --git a/pkg/types/encoding.go b/pkg/types/encoding.go deleted file mode 100644 index 9fd2caa..0000000 --- a/pkg/types/encoding.go +++ /dev/null @@ -1,19 +0,0 @@ -package types - -import ( - "encoding/binary" -) - -// RFC4251, section 5 - -func putSSHString(b []byte, str string) int { - l := len(str) - - i := 0 - binary.BigEndian.PutUint32(b[i:i+4], uint32(l)) - i += 4 - copy(b[i:i+l], str) - i += l - - return i -} diff --git a/pkg/types/encoding_test.go b/pkg/types/encoding_test.go deleted file mode 100644 index cbcf3ba..0000000 --- a/pkg/types/encoding_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package types - -import ( - "bytes" - "testing" -) - -func TestPutSSHString(t *testing.T) { - for _, tbl := range []struct { - desc string - in string - }{ - { - desc: "valid", - in: "ö foo is a bar", - }, - } { - b := make([]byte, 4+len(tbl.in)) - i := putSSHString(b[:], tbl.in) - - if got, want := i, len(tbl.in)+4; got != want { - t.Errorf("%q: len: got %d but wanted %d in test", tbl.desc, got, want) - } - - if got, want := b[4:4+len(tbl.in)], []byte(tbl.in); !bytes.Equal(got, want) { - t.Errorf("%q: got %x but wanted %x", tbl.desc, got, want) - } - } -} diff --git a/pkg/types/endpoint.go b/pkg/types/endpoint.go deleted file mode 100644 index 0e4bab2..0000000 --- a/pkg/types/endpoint.go +++ /dev/null @@ -1,22 +0,0 @@ -package types - -import "strings" - -type Endpoint string - -const ( - EndpointAddLeaf = Endpoint("add-leaf") - EndpointAddCosignature = Endpoint("add-cosignature") - EndpointGetTreeHeadLatest = Endpoint("get-tree-head-latest") - EndpointGetTreeHeadToSign = Endpoint("get-tree-head-to-sign") - EndpointGetTreeHeadCosigned = Endpoint("get-tree-head-cosigned") - EndpointGetInclusionProof = Endpoint("get-inclusion-proof") - EndpointGetConsistencyProof = Endpoint("get-consistency-proof") - EndpointGetLeaves = Endpoint("get-leaves") -) - -// Path joins a number of components to form a full endpoint path. For example, -// EndpointAddLeaf.Path("example.com", "sigsum/v0") -> example.com/sigsum/v0/add-leaf. -func (e Endpoint) Path(components ...string) string { - return strings.Join(append(components, string(e)), "/") -} diff --git a/pkg/types/leaf.go b/pkg/types/leaf.go deleted file mode 100644 index 2ba9299..0000000 --- a/pkg/types/leaf.go +++ /dev/null @@ -1,118 +0,0 @@ -package types - -import ( - "crypto" - "crypto/ed25519" - "encoding/binary" - "fmt" - "io" - - "git.sigsum.org/sigsum-go/pkg/ascii" -) - -type Statement struct { - ShardHint uint64 `ascii:"shard_hint"` - Checksum Hash `ascii:"checksum"` -} - -type Leaf struct { - Statement - Signature Signature `ascii:"signature"` - KeyHash Hash `ascii:"key_hash"` -} - -type Leaves []Leaf - -func (s *Statement) ToBinary() []byte { - namespace := fmt.Sprintf("tree_leaf:v0:%d@sigsum.org", s.ShardHint) - b := make([]byte, 6+4+len(namespace)+4+0+4+6+4+HashSize) - - copy(b[0:6], "SSHSIG") - i := 6 - i += putSSHString(b[i:], namespace) - i += putSSHString(b[i:], "") - i += putSSHString(b[i:], "sha256") - i += putSSHString(b[i:], string(s.Checksum[:])) - - return b -} - -func (s *Statement) Sign(signer crypto.Signer) (*Signature, error) { - sig, err := signer.Sign(nil, s.ToBinary(), crypto.Hash(0)) - if err != nil { - return nil, fmt.Errorf("types: failed signing statement") - } - - var signature Signature - copy(signature[:], sig) - return &signature, nil -} - -func (s *Statement) Verify(key *PublicKey, sig *Signature) bool { - return ed25519.Verify(ed25519.PublicKey(key[:]), s.ToBinary(), sig[:]) -} - -func (l *Leaf) ToBinary() []byte { - b := make([]byte, 136) - binary.BigEndian.PutUint64(b[0:8], l.ShardHint) - copy(b[8:40], l.Checksum[:]) - copy(b[40:104], l.Signature[:]) - copy(b[104:136], l.KeyHash[:]) - return b -} - -func (l *Leaf) FromBinary(b []byte) error { - if len(b) != 136 { - return fmt.Errorf("types: invalid leaf size: %d", len(b)) - } - - l.ShardHint = binary.BigEndian.Uint64(b[0:8]) - copy(l.Checksum[:], b[8:40]) - copy(l.Signature[:], b[40:104]) - copy(l.KeyHash[:], b[104:136]) - return nil -} - -func (l *Leaf) ToASCII(w io.Writer) error { - return ascii.StdEncoding.Serialize(w, l) -} - -func (l *Leaf) FromASCII(r io.Reader) error { - return ascii.StdEncoding.Deserialize(r, l) -} - -func (l *Leaves) FromASCII(r io.Reader) error { - leaves := &struct { - ShardHint []uint64 `ascii:"shard_hint"` - Checksum []Hash `ascii:"checksum"` - Signature []Signature `ascii:"signature"` - KeyHash []Hash `ascii:"key_hash"` - }{} - - if err := ascii.StdEncoding.Deserialize(r, leaves); err != nil { - return err - } - n := len(leaves.ShardHint) - if n != len(leaves.Checksum) { - return fmt.Errorf("types: mismatched leaf field counts") - } - if n != len(leaves.Signature) { - return fmt.Errorf("types: mismatched leaf field counts") - } - if n != len(leaves.KeyHash) { - return fmt.Errorf("types: mismatched leaf field counts") - } - - *l = make([]Leaf, 0, n) - for i := 0; i < n; i++ { - *l = append(*l, Leaf{ - Statement: Statement{ - ShardHint: leaves.ShardHint[i], - Checksum: leaves.Checksum[i], - }, - Signature: leaves.Signature[i], - KeyHash: leaves.KeyHash[i], - }) - } - return nil -} diff --git a/pkg/types/leaf_test.go b/pkg/types/leaf_test.go deleted file mode 100644 index 645f49e..0000000 --- a/pkg/types/leaf_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package types - -import ( - "bytes" - "crypto" - "fmt" - "io" - "reflect" - "strings" - "testing" -) - -func TestStatementToBinary(t *testing.T) { - desc := "valid: shard hint 72623859790382856, checksum 0x00,0x01,..." - if got, want := validStatement(t).ToBinary(), validStatementBytes(t); !bytes.Equal(got, want) { - t.Errorf("got statement\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestStatementSign(t *testing.T) { - for _, table := range []struct { - desc string - stm *Statement - signer crypto.Signer - wantSig *Signature - wantErr bool - }{ - { - desc: "invalid: signer error", - stm: validStatement(t), - signer: &testSigner{*newPubBufferInc(t), *newSigBufferInc(t), fmt.Errorf("signing error")}, - wantErr: true, - }, - { - desc: "valid", - stm: validStatement(t), - signer: &testSigner{*newPubBufferInc(t), *newSigBufferInc(t), nil}, - wantSig: newSigBufferInc(t), - }, - } { - sig, err := table.stm.Sign(table.signer) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - - if got, want := sig[:], table.wantSig[:]; !bytes.Equal(got, want) { - t.Errorf("got signature\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.desc) - } - } -} - -func TestStatementVerify(t *testing.T) { - stm := validStatement(t) - signer, pub := newKeyPair(t) - - sig, err := stm.Sign(signer) - if err != nil { - t.Fatal(err) - } - - if !stm.Verify(&pub, sig) { - t.Errorf("failed verifying a valid statement") - } - - stm.ShardHint += 1 - if stm.Verify(&pub, sig) { - t.Errorf("succeeded verifying an invalid statement") - } -} - -func TestLeafToBinary(t *testing.T) { - desc := "valid: shard hint 72623859790382856, buffers 0x00,0x01,..." - if got, want := validLeaf(t).ToBinary(), validLeafBytes(t); !bytes.Equal(got, want) { - t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestLeafFromBinary(t *testing.T) { - for _, table := range []struct { - desc string - serialized []byte - wantErr bool - want *Leaf - }{ - { - desc: "invalid: not enough bytes", - serialized: make([]byte, 135), - wantErr: true, - }, - { - desc: "invalid: too many bytes", - serialized: make([]byte, 137), - wantErr: true, - }, - { - desc: "valid: shard hint 72623859790382856, buffers 0x00,0x01,...", - serialized: validLeafBytes(t), - want: validLeaf(t), - }, - } { - var leaf Leaf - err := leaf.FromBinary(table.serialized) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &leaf, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func TestLeafToASCII(t *testing.T) { - desc := "valid: shard hint 72623859790382856, buffers 0x00,0x01,..." - buf := bytes.NewBuffer(nil) - if err := validLeaf(t).ToASCII(buf); err != nil { - t.Fatalf("got error true but wanted false in test %q: %v", desc, err) - } - if got, want := string(buf.Bytes()), validLeafASCII(t); got != want { - t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestLeafFromASCII(t *testing.T) { - for _, table := range []struct { - desc string - serialized io.Reader - wantErr bool - want *Leaf - }{ - { - desc: "invalid: not a tree leaf (too few key-value pairs)", - serialized: bytes.NewBuffer([]byte("shard_hint=0\n")), - wantErr: true, - }, - { - desc: "invalid: not a tree leaf (too many key-value pairs)", - serialized: bytes.NewBuffer(append([]byte(validLeafASCII(t)), []byte("key=value\n")...)), - wantErr: true, - }, - { - desc: "valid: shard hint 72623859790382856, buffers 0x00,0x01,...", - serialized: bytes.NewBuffer([]byte(validLeafASCII(t))), - want: validLeaf(t), - }, - } { - var leaf Leaf - err := leaf.FromASCII(table.serialized) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &leaf, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func TestLeavesFromASCII(t *testing.T) { - for _, table := range []struct { - desc string - serialized io.Reader - wantErr bool - want *Leaves - }{ - { - desc: "invalid: not a list of tree leaves (too few key-value pairs)", - serialized: bytes.NewBuffer([]byte("shard_hint=0\n")), - wantErr: true, - }, - { - desc: "invalid: not a list of tree leaves (too many key-value pairs)", - serialized: bytes.NewBuffer(append([]byte(validLeafASCII(t)), []byte("key=value\n")...)), - wantErr: true, - }, - { - desc: "invalid: not a list of tree leaves (too few shard hints))", - serialized: bytes.NewBuffer([]byte(invalidLeavesASCII(t, "shard_hint"))), - wantErr: true, - }, - { - desc: "invalid: not a list of tree leaves (too few checksums))", - serialized: bytes.NewBuffer([]byte(invalidLeavesASCII(t, "checksum"))), - wantErr: true, - }, - { - desc: "invalid: not a list of tree leaves (too few signatures))", - serialized: bytes.NewBuffer([]byte(invalidLeavesASCII(t, "signature"))), - wantErr: true, - }, - { - desc: "invalid: not a list of tree leaves (too few key hashes))", - serialized: bytes.NewBuffer([]byte(invalidLeavesASCII(t, "key_hash"))), - wantErr: true, - }, - { - desc: "valid leaves", - serialized: bytes.NewBuffer([]byte(validLeavesASCII(t))), - want: validLeaves(t), - }, - } { - var leaves Leaves - err := leaves.FromASCII(table.serialized) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &leaves, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got leaves\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func validStatement(t *testing.T) *Statement { - return &Statement{ - ShardHint: 72623859790382856, - Checksum: *HashFn(newHashBufferInc(t)[:]), - } -} - -func validStatementBytes(t *testing.T) []byte { - return bytes.Join([][]byte{ - []byte("SSHSIG"), - []byte{0, 0, 0, 41}, []byte("tree_leaf:v0:72623859790382856@sigsum.org"), - []byte{0, 0, 0, 0}, - []byte{0, 0, 0, 6}, []byte("sha256"), - []byte{0, 0, 0, 32}, HashFn(newHashBufferInc(t)[:])[:], - }, nil) -} - -func validLeaf(t *testing.T) *Leaf { - return &Leaf{ - Statement: Statement{ - ShardHint: 72623859790382856, - Checksum: *HashFn(newHashBufferInc(t)[:]), - }, - Signature: *newSigBufferInc(t), - KeyHash: *newHashBufferInc(t), - } -} - -func validLeafBytes(t *testing.T) []byte { - return bytes.Join([][]byte{ - []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, - HashFn(newHashBufferInc(t)[:])[:], - newSigBufferInc(t)[:], - newHashBufferInc(t)[:], - }, nil) -} - -func validLeafASCII(t *testing.T) string { - return fmt.Sprintf("%s=%d\n%s=%x\n%s=%x\n%s=%x\n", - "shard_hint", 72623859790382856, - "checksum", HashFn(newHashBufferInc(t)[:])[:], - "signature", newSigBufferInc(t)[:], - "key_hash", newHashBufferInc(t)[:], - ) -} - -func validLeaves(t *testing.T) *Leaves { - t.Helper() - return &Leaves{*validLeaf(t), Leaf{}} -} - -func validLeavesASCII(t *testing.T) string { - t.Helper() - return validLeafASCII(t) + fmt.Sprintf("%s=%d\n%s=%x\n%s=%x\n%s=%x\n", - "shard_hint", 0, - "checksum", Hash{}, - "signature", Signature{}, - "key_hash", Hash{}, - ) -} - -func invalidLeavesASCII(t *testing.T, key string) string { - buf := validLeavesASCII(t) - lines := strings.Split(buf, "\n") - - var ret string - switch key { - case "shard_hint": - ret = strings.Join(lines[1:], "\n") - case "checksum": - ret = strings.Join(append(lines[:1], lines[2:]...), "\n") - case "signature": - ret = strings.Join(append(lines[0:2], lines[3:]...), "\n") - case "key_hash": - ret = strings.Join(append(lines[0:3], lines[4:]...), "\n") - default: - t.Fatalf("must have a valid key to remove") - } - return ret -} diff --git a/pkg/types/proof.go b/pkg/types/proof.go deleted file mode 100644 index 8c1474e..0000000 --- a/pkg/types/proof.go +++ /dev/null @@ -1,46 +0,0 @@ -package types - -import ( - "io" - - "git.sigsum.org/sigsum-go/pkg/ascii" -) - -type InclusionProof struct { - TreeSize uint64 - LeafIndex uint64 `ascii:"leaf_index"` - Path []Hash `ascii:"inclusion_path"` -} - -type ConsistencyProof struct { - NewSize uint64 - OldSize uint64 - Path []Hash `ascii:"consistency_path"` -} - -func (p *InclusionProof) ToASCII(w io.Writer) error { - return ascii.StdEncoding.Serialize(w, p) -} - -func (p *InclusionProof) FromASCII(r io.Reader, treeSize uint64) error { - p.TreeSize = treeSize - return ascii.StdEncoding.Deserialize(r, p) -} - -func (p *InclusionProof) Verify(treeSize uint64) bool { - return false // TODO: verify inclusion proof -} - -func (p *ConsistencyProof) ToASCII(w io.Writer) error { - return ascii.StdEncoding.Serialize(w, p) -} - -func (p *ConsistencyProof) FromASCII(r io.Reader, oldSize, newSize uint64) error { - p.OldSize = oldSize - p.NewSize = newSize - return ascii.StdEncoding.Deserialize(r, p) -} - -func (p *ConsistencyProof) Verify(newRoot, oldRoot *Hash) bool { - return false // TODO: verify consistency proof -} diff --git a/pkg/types/proof_test.go b/pkg/types/proof_test.go deleted file mode 100644 index 8285b6e..0000000 --- a/pkg/types/proof_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package types - -import ( - "bytes" - "fmt" - "io" - "reflect" - "testing" -) - -func TestInclusionProofToASCII(t *testing.T) { - desc := "valid" - buf := bytes.NewBuffer(nil) - if err := validInclusionProof(t).ToASCII(buf); err != nil { - t.Fatalf("got error true but wanted false in test %q: %v", desc, err) - } - if got, want := string(buf.Bytes()), validInclusionProofASCII(t); got != want { - t.Errorf("got inclusion proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestInclusionProofFromASCII(t *testing.T) { - for _, table := range []struct { - desc string - serialized io.Reader - wantErr bool - want *InclusionProof - }{ - { - desc: "invalid: not an inclusion proof (unexpected key-value pair)", - serialized: bytes.NewBuffer(append([]byte(validInclusionProofASCII(t)), []byte("tree_size=4")...)), - wantErr: true, - want: validInclusionProof(t), // to populate input to FromASCII - }, - { - desc: "valid", - serialized: bytes.NewBuffer([]byte(validInclusionProofASCII(t))), - want: validInclusionProof(t), - }, - } { - var proof InclusionProof - err := proof.FromASCII(table.serialized, table.want.TreeSize) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &proof, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got inclusion proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func TestConsistencyProofToASCII(t *testing.T) { - desc := "valid" - buf := bytes.NewBuffer(nil) - if err := validConsistencyProof(t).ToASCII(buf); err != nil { - t.Fatalf("got error true but wanted false in test %q: %v", desc, err) - } - if got, want := string(buf.Bytes()), validConsistencyProofASCII(t); got != want { - t.Errorf("got consistency proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestConsistencyProofFromASCII(t *testing.T) { - for _, table := range []struct { - desc string - serialized io.Reader - wantErr bool - want *ConsistencyProof - }{ - { - desc: "invalid: not a consistency proof (unexpected key-value pair)", - serialized: bytes.NewBuffer(append([]byte(validConsistencyProofASCII(t)), []byte("start_size=1")...)), - wantErr: true, - want: validConsistencyProof(t), // to populate input to FromASCII - }, - { - desc: "valid", - serialized: bytes.NewBuffer([]byte(validConsistencyProofASCII(t))), - want: validConsistencyProof(t), - }, - } { - var proof ConsistencyProof - err := proof.FromASCII(table.serialized, table.want.OldSize, table.want.NewSize) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &proof, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got consistency proof\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func validInclusionProof(t *testing.T) *InclusionProof { - t.Helper() - return &InclusionProof{ - LeafIndex: 1, - TreeSize: 4, - Path: []Hash{ - Hash{}, - *newHashBufferInc(t), - }, - } -} - -func validInclusionProofASCII(t *testing.T) string { - t.Helper() - return fmt.Sprintf("%s=%d\n%s=%x\n%s=%x\n", - "leaf_index", 1, - "inclusion_path", Hash{}, - "inclusion_path", newHashBufferInc(t)[:], - ) -} - -func validConsistencyProof(t *testing.T) *ConsistencyProof { - t.Helper() - return &ConsistencyProof{ - NewSize: 1, - OldSize: 4, - Path: []Hash{ - Hash{}, - *newHashBufferInc(t), - }, - } -} - -func validConsistencyProofASCII(t *testing.T) string { - t.Helper() - return fmt.Sprintf("%s=%x\n%s=%x\n", - "consistency_path", Hash{}, - "consistency_path", newHashBufferInc(t)[:], - ) -} diff --git a/pkg/types/tree_head.go b/pkg/types/tree_head.go deleted file mode 100644 index de62526..0000000 --- a/pkg/types/tree_head.go +++ /dev/null @@ -1,90 +0,0 @@ -package types - -import ( - "crypto" - "crypto/ed25519" - "encoding/binary" - "fmt" - "io" - - "git.sigsum.org/sigsum-go/pkg/ascii" - "git.sigsum.org/sigsum-go/pkg/hex" -) - -type TreeHead struct { - Timestamp uint64 `ascii:"timestamp"` - TreeSize uint64 `ascii:"tree_size"` - RootHash Hash `ascii:"root_hash"` -} - -type SignedTreeHead struct { - TreeHead - Signature Signature `ascii:"signature"` -} - -type CosignedTreeHead struct { - SignedTreeHead - Cosignature []Signature `ascii:"cosignature"` - KeyHash []Hash `ascii:"key_hash"` -} - -func (th *TreeHead) toBinary() []byte { - b := make([]byte, 48) - binary.BigEndian.PutUint64(b[0:8], th.Timestamp) - binary.BigEndian.PutUint64(b[8:16], th.TreeSize) - copy(b[16:48], th.RootHash[:]) - return b -} - -func (th *TreeHead) ToBinary(keyHash *Hash) []byte { - namespace := fmt.Sprintf("tree_head:v0:%s@sigsum.org", hex.Serialize(keyHash[:])) // length 88 - b := make([]byte, 6+4+88+4+0+4+6+4+HashSize) - - copy(b[0:6], "SSHSIG") - i := 6 - i += putSSHString(b[i:], namespace) - i += putSSHString(b[i:], "") - i += putSSHString(b[i:], "sha256") - i += putSSHString(b[i:], string((*HashFn(th.toBinary()))[:])) - - return b -} - -func (th *TreeHead) Sign(s crypto.Signer, kh *Hash) (*SignedTreeHead, error) { - sig, err := s.Sign(nil, th.ToBinary(kh), crypto.Hash(0)) - if err != nil { - return nil, fmt.Errorf("types: failed signing tree head") - } - - sth := &SignedTreeHead{ - TreeHead: *th, - } - copy(sth.Signature[:], sig) - return sth, nil -} - -func (sth *SignedTreeHead) ToASCII(w io.Writer) error { - return ascii.StdEncoding.Serialize(w, sth) -} - -func (sth *SignedTreeHead) FromASCII(r io.Reader) error { - return ascii.StdEncoding.Deserialize(r, sth) -} - -func (sth *SignedTreeHead) Verify(key *PublicKey, kh *Hash) bool { - return ed25519.Verify(ed25519.PublicKey(key[:]), sth.TreeHead.ToBinary(kh), sth.Signature[:]) -} - -func (cth *CosignedTreeHead) ToASCII(w io.Writer) error { - return ascii.StdEncoding.Serialize(w, cth) -} - -func (cth *CosignedTreeHead) FromASCII(r io.Reader) error { - if err := ascii.StdEncoding.Deserialize(r, cth); err != nil { - return err - } - if len(cth.Cosignature) != len(cth.KeyHash) { - return fmt.Errorf("types: mismatched cosignature count") - } - return nil -} diff --git a/pkg/types/tree_head_test.go b/pkg/types/tree_head_test.go deleted file mode 100644 index a1ffa6f..0000000 --- a/pkg/types/tree_head_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package types - -import ( - "bytes" - "crypto" - "fmt" - "io" - "reflect" - "testing" - - "git.sigsum.org/sigsum-go/pkg/hex" -) - -func TestTreeHeadToBinary(t *testing.T) { - desc := "valid" - kh := Hash{} - if got, want := validTreeHead(t).ToBinary(&kh), validTreeHeadBytes(t, &kh); !bytes.Equal(got, want) { - t.Errorf("got tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestTreeHeadSign(t *testing.T) { - for _, table := range []struct { - desc string - th *TreeHead - signer crypto.Signer - wantSig *Signature - wantErr bool - }{ - { - desc: "invalid: signer error", - th: validTreeHead(t), - signer: &testSigner{*newPubBufferInc(t), *newSigBufferInc(t), fmt.Errorf("signing error")}, - wantErr: true, - }, - { - desc: "valid", - th: validTreeHead(t), - signer: &testSigner{*newPubBufferInc(t), *newSigBufferInc(t), nil}, - wantSig: newSigBufferInc(t), - }, - } { - logKey := PublicKey{} - sth, err := table.th.Sign(table.signer, HashFn(logKey[:])) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - - wantSTH := &SignedTreeHead{ - TreeHead: *table.th, - Signature: *table.wantSig, - } - if got, want := sth, wantSTH; !reflect.DeepEqual(got, want) { - t.Errorf("got sth\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.desc) - } - } -} - -func TestSignedTreeHeadToASCII(t *testing.T) { - desc := "valid" - buf := bytes.NewBuffer(nil) - if err := validSignedTreeHead(t).ToASCII(buf); err != nil { - t.Fatalf("got error true but wanted false in test %q: %v", desc, err) - } - if got, want := string(buf.Bytes()), validSignedTreeHeadASCII(t); got != want { - t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestSignedTreeHeadFromASCII(t *testing.T) { - for _, table := range []struct { - desc string - serialized io.Reader - wantErr bool - want *SignedTreeHead - }{ - { - desc: "invalid: not a signed tree head (unexpected key-value pair)", - serialized: bytes.NewBuffer(append( - []byte(validSignedTreeHeadASCII(t)), - []byte("key=4")...), - ), - wantErr: true, - }, - { - desc: "valid", - serialized: bytes.NewBuffer([]byte(validSignedTreeHeadASCII(t))), - want: validSignedTreeHead(t), - }, - } { - var sth SignedTreeHead - err := sth.FromASCII(table.serialized) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &sth, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func TestSignedTreeHeadVerify(t *testing.T) { - th := validTreeHead(t) - signer, pub := newKeyPair(t) - kh := HashFn(pub[:]) - - sth, err := th.Sign(signer, kh) - if err != nil { - t.Fatal(err) - } - - if !sth.Verify(&pub, kh) { - t.Errorf("failed verifying a valid signed tree head") - } - - sth.TreeSize += 1 - if sth.Verify(&pub, kh) { - t.Errorf("succeeded verifying an invalid signed tree head") - } -} - -func TestCosignedTreeHeadToASCII(t *testing.T) { - desc := "valid" - buf := bytes.NewBuffer(nil) - if err := validCosignedTreeHead(t).ToASCII(buf); err != nil { - t.Fatalf("got error true but wanted false in test %q: %v", desc, err) - } - if got, want := string(buf.Bytes()), validCosignedTreeHeadASCII(t); got != want { - t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, desc) - } -} - -func TestCosignedTreeHeadFromASCII(t *testing.T) { - for _, table := range []struct { - desc string - serialized io.Reader - wantErr bool - want *CosignedTreeHead - }{ - { - desc: "invalid: not a cosigned tree head (unexpected key-value pair)", - serialized: bytes.NewBuffer(append( - []byte(validCosignedTreeHeadASCII(t)), - []byte("key=4")...), - ), - wantErr: true, - }, - { - desc: "invalid: not a cosigned tree head (not enough cosignatures)", - serialized: bytes.NewBuffer(append( - []byte(validCosignedTreeHeadASCII(t)), - []byte(fmt.Sprintf("key_hash=%x\n", Hash{}))..., - )), - wantErr: true, - }, - { - desc: "valid", - serialized: bytes.NewBuffer([]byte(validCosignedTreeHeadASCII(t))), - want: validCosignedTreeHead(t), - }, - } { - var cth CosignedTreeHead - err := cth.FromASCII(table.serialized) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) - } - if err != nil { - continue - } - if got, want := &cth, table.want; !reflect.DeepEqual(got, want) { - t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.desc) - } - } -} - -func validTreeHead(t *testing.T) *TreeHead { - return &TreeHead{ - Timestamp: 72623859790382856, - TreeSize: 257, - RootHash: *newHashBufferInc(t), - } -} - -func validTreeHeadBytes(t *testing.T, keyHash *Hash) []byte { - ns := fmt.Sprintf("tree_head:v0:%s@sigsum.org", hex.Serialize(keyHash[:])) - th := bytes.Join([][]byte{ - []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, - []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01}, - newHashBufferInc(t)[:], - }, nil) - return bytes.Join([][]byte{ - []byte("SSHSIG"), - []byte{0, 0, 0, 88}, []byte(ns), - []byte{0, 0, 0, 0}, - []byte{0, 0, 0, 6}, []byte("sha256"), - []byte{0, 0, 0, 32}, (*HashFn(th))[:], - }, nil) -} - -func validSignedTreeHead(t *testing.T) *SignedTreeHead { - t.Helper() - return &SignedTreeHead{ - TreeHead: TreeHead{ - Timestamp: 1, - TreeSize: 2, - RootHash: *newHashBufferInc(t), - }, - Signature: *newSigBufferInc(t), - } -} - -func validSignedTreeHeadASCII(t *testing.T) string { - t.Helper() - return fmt.Sprintf("%s=%d\n%s=%d\n%s=%x\n%s=%x\n", - "timestamp", 1, - "tree_size", 2, - "root_hash", newHashBufferInc(t)[:], - "signature", newSigBufferInc(t)[:], - ) -} - -func validCosignedTreeHead(t *testing.T) *CosignedTreeHead { - t.Helper() - return &CosignedTreeHead{ - SignedTreeHead: SignedTreeHead{ - TreeHead: TreeHead{ - Timestamp: 1, - TreeSize: 2, - RootHash: *newHashBufferInc(t), - }, - Signature: *newSigBufferInc(t), - }, - Cosignature: []Signature{ - Signature{}, - *newSigBufferInc(t), - }, - KeyHash: []Hash{ - Hash{}, - *newHashBufferInc(t), - }, - } -} - -func validCosignedTreeHeadASCII(t *testing.T) string { - t.Helper() - return fmt.Sprintf("%s=%d\n%s=%d\n%s=%x\n%s=%x\n%s=%x\n%s=%x\n%s=%x\n%s=%x\n", - "timestamp", 1, - "tree_size", 2, - "root_hash", newHashBufferInc(t)[:], - "signature", newSigBufferInc(t)[:], - "cosignature", Signature{}, - "cosignature", newSigBufferInc(t)[:], - "key_hash", Hash{}, - "key_hash", newHashBufferInc(t)[:], - ) -} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..238e48f --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,590 @@ +package types + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "crypto/sha256" + "fmt" + "io" + "strings" + + "git.sigsum.org/sigsum-go/pkg/hex" + "git.sigsum.org/sigsum-go/pkg/types/ascii" + "git.sigsum.org/sigsum-go/pkg/types/binary/ssh" + "git.sigsum.org/sigsum-go/pkg/types/binary/trunnel" +) + +// Hash is a SHA256 hash, see §XXX: +// +// u8 Hash[32]; +// +type Hash [HashSize]byte + +const HashSize = 32 + +func HashFn(b []byte) Hash { + return sha256.Sum256(b) +} + +// Signature is an Ed25519 signature, see §XXX: +// +// u8 Signature[64]; +// +type Signature [SignatureSize]byte + +const SignatureSize = 64 + +// PublicKey is an Ed25519 public key, see §XXX: +// +// u8 public_key[32]; +// +type PublicKey [PublicKeySize]byte + +const PublicKeySize = 32 + +func (k *PublicKey) Verify(msg []byte, sig Signature) error { + if !ed25519.Verify(ed25519.PublicKey(k[:]), msg, sig[:]) { + return fmt.Errorf("invalid ed25519 signature") + } + return nil +} + +// PrivateKey provides access to the private part of an Ed25519 key-pair +type PrivateKey struct { + crypto.Signer +} + +func (k *PrivateKey) Sign(message []byte) (s Signature, err error) { + sig, err := k.Signer.Sign(nil, message, crypto.Hash(0)) + if err != nil { + return s, fmt.Errorf("sign: %w", err) + } + if n := len(sig); n != SignatureSize { + return s, fmt.Errorf("invalid signature size %d", n) + } + copy(s[:], sig) + return s, nil +} + +// TreeHead is a Merkle tree head, see §2.3.1: +// +// struct tree_head { +// u64 timestamp; +// u64 tree_size; +// hash root_hash; +// }; +// +type TreeHead struct { + Timestamp uint64 + TreeSize uint64 + RootHash Hash +} + +func (th *TreeHead) ToTrunnel() []byte { + buf := bytes.NewBuffer(nil) + + trunnel.AddUint64(buf, th.Timestamp) + trunnel.AddUint64(buf, th.TreeSize) + buf.Write(th.RootHash[:]) + + return buf.Bytes() +} + +// ToSSH serialization is defined in §2.3.2 +func (th *TreeHead) ToSSH(keyHash Hash) []byte { + namespace := fmt.Sprintf("tree_head:v0:%s@sigsum.org", hex.Serialize(keyHash[:])) + return ssh.ToSignBlob(namespace, th.ToTrunnel()) +} + +func (th *TreeHead) Sign(k PrivateKey, logKeyHash Hash) (Signature, error) { + return k.Sign(th.ToSSH(logKeyHash)) +} + +func (th *TreeHead) Verify(k PublicKey, logKeyHash Hash, sig Signature) error { + return k.Verify(th.ToSSH(logKeyHash), sig) +} + +// Checksum is a checksum, see §XXX: +// +// hash checksum; +// +type Checksum Hash + +// ToSSH serialization is defined in §2.3.3 +func (c *Checksum) ToSSH(shardHint uint64) []byte { + namespace := fmt.Sprintf("tree_leaf:v0:%d@sigsum.org", shardHint) + return ssh.ToSignBlob(namespace, c[:]) +} + +func (c *Checksum) Sign(k PrivateKey, shardHint uint64) (Signature, error) { + return k.Sign(c.ToSSH(shardHint)) +} + +func (c *Checksum) Verify(k PublicKey, shardHint uint64, sig Signature) error { + return k.Verify(c.ToSSH(shardHint), sig) +} + +// TreeLeaf is a Merkle tree leaf, see §2.3.3: +// +// struct tree_leaf { +// u64 shard_hint; +// checksum checksum; +// signature signature; +// hash key_hash; +// }; +// +type TreeLeaf struct { + ShardHint uint64 + Checksum Checksum + Signature Signature + KeyHash Hash +} + +func (tl *TreeLeaf) ToTrunnel() []byte { + buf := bytes.NewBuffer(nil) + + trunnel.AddUint64(buf, tl.ShardHint) + buf.Write(tl.Checksum[:]) + buf.Write(tl.Signature[:]) + buf.Write(tl.KeyHash[:]) + + return buf.Bytes() +} + +func (tl *TreeLeaf) FromTrunnel(buf *bytes.Buffer) error { + if err := trunnel.Uint64(buf, &tl.ShardHint); err != nil { + return fmt.Errorf("tree_leaf.shard_hint: %w", err) + } + if err := trunnel.Array(buf, tl.Checksum[:]); err != nil { + return fmt.Errorf("tree_leaf.checksum: %w", err) + } + if err := trunnel.Array(buf, tl.Signature[:]); err != nil { + return fmt.Errorf("tree_leaf.signature: %w", err) + } + if err := trunnel.Array(buf, tl.KeyHash[:]); err != nil { + return fmt.Errorf("tree_leaf.key_hash: %w", err) + } + if rest, err := io.ReadAll(buf); err != nil || len(rest) != 0 { + return fmt.Errorf("invalid remainder: rest is %x and err %v", rest, err) + } + return nil +} + +// Endpoint is named log endpoint, see §3.1 - §3.7 +type Endpoint string + +const ( + EndpointAddLeaf = Endpoint("add-leaf") + EndpointAddCosignature = Endpoint("add-cosignature") + EndpointGetTreeHeadToCosign = Endpoint("get-tree-head-to-sign") + EndpointGetTreeHeadCosigned = Endpoint("get-tree-head-cosigned") + EndpointGetInclusionProof = Endpoint("get-inclusion-proof") + EndpointGetConsistencyProof = Endpoint("get-consistency-proof") + EndpointGetLeaves = Endpoint("get-leaves") +) + +// Path returns a complete endpoint URL for a given log URL. The format of a +// log's URL is defined in §3, e.g., "https://log.example.com/sigsum/v0". +func (e Endpoint) URL(logURL string) string { + return logURL + "/" + string(e) +} + +const ( + asciiError = "error" // XXX: update s/E/e in api.md + asciiTimestamp = "timestamp" + asciiTreeSize = "tree_size" + asciiRootHash = "root_hash" + asciiSignature = "signature" + asciiCosignature = "cosignature" + asciiKeyHash = "key_hash" + asciiLeafIndex = "leaf_index" + asciiInclusionPath = "inclusion_path" + asciiConsistencyPath = "consistency_path" + asciiShardHint = "shard_hint" + asciiChecksum = "checksum" + asciiMessage = "message" // XXX: update s/preimage/message in api.md + asciiPublicKey = "public_key" // XXX: update s/verification_key/public_key in api.md + asciiDomainHint = "domain_hint" +) + +// Error is an error mesage, see §3 +type Error string + +func (e *Error) ToASCII(w io.Writer) error { + if strings.Contains(string(*e), ascii.EndOfValue) { + return fmt.Errorf("string contains end-of-value pattern") // XXX: in ascii package instead? + } + if err := ascii.WritePair(w, asciiError, string(*e)); err != nil { + fmt.Errorf("%s: %w", asciiError, err) + } + return nil +} + +func (e *Error) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + if err := m.DequeueString(asciiError, (*string)(e)); err != nil { + return fmt.Errorf("%s: %w", asciiError, err) + } + return nil + }) +} + +// SignedTreeHead is the output of get-tree-head-to-cosign, see §3.1 +type SignedTreeHead struct { + TreeHead + Signature Signature +} + +func (sth *SignedTreeHead) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiTimestamp, fmt.Sprintf("%d", sth.Timestamp)); err != nil { + return fmt.Errorf("%s: %w", asciiTimestamp, err) + } + if err := ascii.WritePair(w, asciiTreeSize, fmt.Sprintf("%d", sth.TreeSize)); err != nil { + return fmt.Errorf("%s: %w", asciiTreeSize, err) + } + if err := ascii.WritePair(w, asciiRootHash, hex.Serialize(sth.RootHash[:])); err != nil { + return fmt.Errorf("%s: %w", asciiRootHash, err) + } + if err := ascii.WritePair(w, asciiSignature, hex.Serialize(sth.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + return nil +} + +func (sth *SignedTreeHead) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + *sth, err = sthFromASCII(m) + return err + }) +} + +// CosignedTreeHead is the output of get-tree-head-cosigned, see §3.2 +type CosignedTreeHead struct { + SignedTreeHead + Cosignatures []Cosignature +} + +func (cth *CosignedTreeHead) ToASCII(w io.Writer) error { + if len(cth.Cosignatures) == 0 { + return fmt.Errorf("no cosignatures") + } + + for i, c := range cth.Cosignatures { + if err := c.ToASCII(w); err != nil { + return fmt.Errorf("%d: %w", i+1, err) + } + } + return cth.SignedTreeHead.ToASCII(w) +} + +func (cth *CosignedTreeHead) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + n := m.NumValues(asciiCosignature) + if n == 0 { + return fmt.Errorf("no cosignatures") + } + + cth.Cosignatures = make([]Cosignature, 0, n) + for i := uint64(0); i < n; i++ { + c, err := cosignatureFromASCII(m) + if err != nil { + return fmt.Errorf("%d: %w", i+1, err) + } + cth.Cosignatures = append(cth.Cosignatures, c) + } + cth.SignedTreeHead, err = sthFromASCII(m) + return err + }) +} + +type Cosignature struct { + KeyHash Hash + Signature Signature +} + +func (c *Cosignature) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiKeyHash, hex.Serialize(c.KeyHash[:])); err != nil { + return fmt.Errorf("%s: %w", asciiKeyHash, err) + } + if err := ascii.WritePair(w, asciiCosignature, hex.Serialize(c.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiCosignature, err) + } + return nil +} + +func (c *Cosignature) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + *c, err = cosignatureFromASCII(m) + return err + }) +} + +// InclusionProof is the output of get-inclusion-proof, see §3.3 +type InclusionProof struct { + LeafIndex uint64 + InclusionPath []Hash +} + +func (p *InclusionProof) ToASCII(w io.Writer) error { + if len(p.InclusionPath) == 0 { + return fmt.Errorf("no inclusion path") + } + + for i, h := range p.InclusionPath { + if err := ascii.WritePair(w, asciiInclusionPath, hex.Serialize(h[:])); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiInclusionPath, err) + } + } + if err := ascii.WritePair(w, asciiLeafIndex, fmt.Sprintf("%d", p.LeafIndex)); err != nil { + return fmt.Errorf("%s: %w", asciiLeafIndex, err) + } + return nil +} + +func (p *InclusionProof) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + n := m.NumValues(asciiInclusionPath) + if n == 0 { + return fmt.Errorf("no inclusion path") + } + + p.InclusionPath = make([]Hash, 0, n) + for i := uint64(0); i < n; i++ { + var h Hash + if err := m.DequeueArray(asciiInclusionPath, h[:]); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiInclusionPath, err) + } + p.InclusionPath = append(p.InclusionPath, h) + } + if err := m.DequeueUint64(asciiLeafIndex, &p.LeafIndex); err != nil { + return fmt.Errorf("%s: %w", asciiLeafIndex, err) + } + return nil + }) +} + +// ConsistencyProof is the output of get-consistency proof, see §3.4 +type ConsistencyProof struct { + ConsistencyPath []Hash +} + +func (p *ConsistencyProof) ToASCII(w io.Writer) error { + if len(p.ConsistencyPath) == 0 { + return fmt.Errorf("no consistency path") + } + + for i, h := range p.ConsistencyPath { + if err := ascii.WritePair(w, asciiConsistencyPath, hex.Serialize(h[:])); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiConsistencyPath, err) + } + } + return nil +} + +func (p *ConsistencyProof) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + n := m.NumValues(asciiConsistencyPath) + if n == 0 { + return fmt.Errorf("no inclusion path") + } + + p.ConsistencyPath = make([]Hash, 0, n) + for i := uint64(0); i < n; i++ { + var h Hash + if err := m.DequeueArray(asciiConsistencyPath, h[:]); err != nil { + return fmt.Errorf("%d: %s: %w", i+1, asciiConsistencyPath, err) + } + p.ConsistencyPath = append(p.ConsistencyPath, h) + } + return nil + }) +} + +// Leaves is the output of get-leaves, see §3.5 +type Leaves []TreeLeaf + +func (l *Leaves) ToASCII(w io.Writer) error { + if len(*l) == 0 { + return fmt.Errorf("no leaves") + } + + for i, leaf := range *l { + if err := leaf.ToASCII(w); err != nil { + return fmt.Errorf("%d: %w", i+1, err) + } + } + return nil +} + +func (l *Leaves) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) error { + n := m.NumValues(asciiShardHint) + if n == 0 { + return fmt.Errorf("no leaves") + } + + *l = make([]TreeLeaf, 0, n) + for i := uint64(0); i < n; i++ { + var leaf TreeLeaf + if err := m.DequeueUint64(asciiShardHint, &leaf.ShardHint); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := m.DequeueArray(asciiChecksum, leaf.Checksum[:]); err != nil { + return fmt.Errorf("%s: %w", asciiChecksum, err) + } + if err := m.DequeueArray(asciiSignature, leaf.Signature[:]); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := m.DequeueArray(asciiKeyHash, leaf.KeyHash[:]); err != nil { + return fmt.Errorf("%s: %w", asciiKeyHash, err) + } + *l = append(*l, leaf) + } + return nil + }) +} + +func (l *TreeLeaf) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiShardHint, fmt.Sprintf("%d", l.ShardHint)); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := ascii.WritePair(w, asciiChecksum, hex.Serialize(l.Checksum[:])); err != nil { + return fmt.Errorf("%s: %w", asciiChecksum, err) + } + if err := ascii.WritePair(w, asciiSignature, hex.Serialize(l.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := ascii.WritePair(w, asciiKeyHash, hex.Serialize(l.KeyHash[:])); err != nil { + return fmt.Errorf("%s: %w", asciiKeyHash, err) + } + return nil +} + +// RequestInclusionProof is the input of get-inclusion-proof, see §3.3 +type RequestInclusionProof struct { + TreeSize uint64 + LeafHash Hash +} + +func (req *RequestInclusionProof) ToURL(logURL string) string { + return "TODO" +} + +func (req *RequestInclusionProof) FromURL(url string) error { + return nil // TODO +} + +// RequestConsistencyProof is the input of get-consistency-proof, see §3.4 +type RequestConsistencyProof struct { + OldSize uint64 + NewSize uint64 +} + +func (req *RequestConsistencyProof) ToURL(logURL string) string { + return "TODO" +} + +func (req *RequestConsistencyProof) FromURL(url string) error { + return nil // TODO +} + +// RequestLeaves is the input of a get-leaves, see §3.5 +type RequestLeaves struct { + OldSize uint64 + NewSize uint64 +} + +func (req *RequestLeaves) ToURL(logURL string) string { + return "TODO" +} + +func (req *RequestLeaves) FromURL(url string) error { + return nil // TODO +} + +// RequestLeaf is the input of add-leaf, see §3.6 +type RequestLeaf struct { + ShardHint uint64 + Message Hash + Signature Signature + PublicKey PublicKey + DomainHint string +} + +func (req *RequestLeaf) ToASCII(w io.Writer) error { + if err := ascii.WritePair(w, asciiShardHint, fmt.Sprintf("%d", req.ShardHint)); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := ascii.WritePair(w, asciiMessage, hex.Serialize(req.Message[:])); err != nil { + return fmt.Errorf("%s: %w", asciiMessage, err) + } + if err := ascii.WritePair(w, asciiSignature, hex.Serialize(req.Signature[:])); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := ascii.WritePair(w, asciiPublicKey, hex.Serialize(req.PublicKey[:])); err != nil { + return fmt.Errorf("%s: %w", asciiPublicKey, err) + } + if err := ascii.WritePair(w, asciiDomainHint, req.DomainHint); err != nil { + return fmt.Errorf("%s: %w", asciiDomainHint, err) + } + return nil +} + +func (req *RequestLeaf) FromASCII(r io.Reader) error { + return ascii.ReadPairs(r, func(m *ascii.Map) (err error) { + if err := m.DequeueUint64(asciiShardHint, &req.ShardHint); err != nil { + return fmt.Errorf("%s: %w", asciiShardHint, err) + } + if err := m.DequeueArray(asciiMessage, req.Message[:]); err != nil { + return fmt.Errorf("%s: %w", asciiMessage, err) + } + if err := m.DequeueArray(asciiSignature, req.Signature[:]); err != nil { + return fmt.Errorf("%s: %w", asciiSignature, err) + } + if err := m.DequeueArray(asciiPublicKey, req.PublicKey[:]); err != nil { + return fmt.Errorf("%s: %w", asciiPublicKey, err) + } + if err := m.DequeueString(asciiDomainHint, &req.DomainHint); err != nil { + return fmt.Errorf("%s: %w", asciiDomainHint, err) + } + return nil + }) +} + +// RequestCosignature is the input of add-cosignature, see §3.7 +type RequestCosignature Cosignature + +func (req *RequestCosignature) ToASCII(w io.Writer) error { + return (*Cosignature)(req).ToASCII(w) +} + +func (req *RequestCosignature) FromASCII(r io.Reader) error { + return (*Cosignature)(req).FromASCII(r) +} + +func sthFromASCII(m *ascii.Map) (sth SignedTreeHead, err error) { + if m.DequeueUint64(asciiTimestamp, &sth.Timestamp); err != nil { + return sth, fmt.Errorf("%s: %w", asciiTimestamp, err) + } + if m.DequeueUint64(asciiTreeSize, &sth.TreeSize); err != nil { + return sth, fmt.Errorf("%s: %w", asciiTreeSize, err) + } + if m.DequeueArray(asciiRootHash, sth.RootHash[:]); err != nil { + return sth, fmt.Errorf("%s: %w", asciiRootHash, err) + } + if m.DequeueArray(asciiSignature, sth.Signature[:]); err != nil { + return sth, fmt.Errorf("%s: %w", asciiSignature, err) + } + return sth, nil +} + +func cosignatureFromASCII(m *ascii.Map) (c Cosignature, err error) { + if err := m.DequeueArray(asciiCosignature, c.Signature[:]); err != nil { + return c, fmt.Errorf("%s: %w", asciiCosignature, err) + } + if err := m.DequeueArray(asciiKeyHash, c.KeyHash[:]); err != nil { + return c, fmt.Errorf("%s: %w", asciiCosignature, err) + } + return c, nil +} -- cgit v1.2.3