aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@mullvad.net>2022-04-25 00:43:06 +0200
committerRasmus Dahlberg <rasmus@mullvad.net>2022-04-25 00:43:06 +0200
commit528a53f7f76f08af5902f4cfa8235380b3434ba0 (patch)
tree662b7834d5ce15627554e9307a4e00f7364fba11
parent4fc0ff2ec2f48519ee245d6d7edee1921cb3b8bc (diff)
drafty types refactor with simple ascii packagergdd/sketch
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.
-rw-r--r--pkg/ascii/ascii.go306
-rw-r--r--pkg/ascii/ascii_test.go455
-rw-r--r--pkg/types/ascii/ascii.go173
-rw-r--r--pkg/types/ascii/ascii_test.go290
-rw-r--r--pkg/types/binary/ssh/ssh.go34
-rw-r--r--pkg/types/binary/trunnel/trunnel.go33
-rw-r--r--pkg/types/crypto.go30
-rw-r--r--pkg/types/crypto_test.go64
-rw-r--r--pkg/types/encoding.go19
-rw-r--r--pkg/types/encoding_test.go29
-rw-r--r--pkg/types/endpoint.go22
-rw-r--r--pkg/types/leaf.go118
-rw-r--r--pkg/types/leaf_test.go302
-rw-r--r--pkg/types/proof.go46
-rw-r--r--pkg/types/proof_test.go138
-rw-r--r--pkg/types/tree_head.go90
-rw-r--r--pkg/types/tree_head_test.go262
-rw-r--r--pkg/types/types.go590
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
+}