diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/ascii/ascii.go | 306 | ||||
-rw-r--r-- | pkg/ascii/ascii_test.go | 455 | ||||
-rw-r--r-- | pkg/types/ascii/ascii.go | 173 | ||||
-rw-r--r-- | pkg/types/ascii/ascii_test.go | 290 | ||||
-rw-r--r-- | pkg/types/binary/ssh/ssh.go | 34 | ||||
-rw-r--r-- | pkg/types/binary/trunnel/trunnel.go | 33 | ||||
-rw-r--r-- | pkg/types/crypto.go | 30 | ||||
-rw-r--r-- | pkg/types/crypto_test.go | 64 | ||||
-rw-r--r-- | pkg/types/encoding.go | 19 | ||||
-rw-r--r-- | pkg/types/encoding_test.go | 29 | ||||
-rw-r--r-- | pkg/types/endpoint.go | 22 | ||||
-rw-r--r-- | pkg/types/leaf.go | 118 | ||||
-rw-r--r-- | pkg/types/leaf_test.go | 302 | ||||
-rw-r--r-- | pkg/types/proof.go | 46 | ||||
-rw-r--r-- | pkg/types/proof_test.go | 138 | ||||
-rw-r--r-- | pkg/types/tree_head.go | 90 | ||||
-rw-r--r-- | pkg/types/tree_head_test.go | 262 | ||||
-rw-r--r-- | pkg/types/types.go | 590 |
18 files changed, 1120 insertions, 1881 deletions
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 +} |