diff options
| -rw-r--r-- | namespace/namespace.go | 150 | ||||
| -rw-r--r-- | namespace/namespace_test.go | 218 | ||||
| -rw-r--r-- | namespace/testdata/data.go | 15 | 
3 files changed, 383 insertions, 0 deletions
| diff --git a/namespace/namespace.go b/namespace/namespace.go new file mode 100644 index 0000000..a02de6d --- /dev/null +++ b/namespace/namespace.go @@ -0,0 +1,150 @@ +// Package namespace provides namespace functionality.  A namespace refers to a +// particular verification key and signing algorithm that can be serialized with +// TLS 1.2 notation, see RFC 5246 (ยง4).  Only Ed25519 is supported at this time. +// +// For example, this is how a serialized Ed25519 namespace looks like: +// +// 0   2                      34 (byte index) +// +---+----------------------+ +// | 1 +   Verification key   + +// +---+----------------------+ +package namespace + +import ( +	"fmt" + +	"crypto/ed25519" + +	"github.com/google/certificate-transparency-go/tls" +) + +// NamespaceFormat defines a particular namespace type that is versioend +type NamespaceFormat tls.Enum + +const ( +	NamespaceFormatReserved  NamespaceFormat = 0 +	NamespaceFormatEd25519V1 NamespaceFormat = 1 +) + +// Namespace references a versioned namespace based on a given format specifier +type Namespace struct { +	Format             NamespaceFormat     `tls:"maxval:65535"` +	NamespaceEd25519V1 *NamespaceEd25519V1 `tls:"selector:Format,val:1"` +} + +// NamespaceEd25519V1 uses an Ed25519 verification key as namespace.  Encoding, +// signing, and verification operations are defined by RFC 8032. +type NamespaceEd25519V1 struct { +	Namespace []byte `tls:"minlen:32,maxlen:32"` +} + +// String returns a human-readable representation of a namespace. +func (n Namespace) String() string { +	switch n.Format { +	case NamespaceFormatEd25519V1: +		return fmt.Sprintf("%x", n.NamespaceEd25519V1.Namespace) +	default: +		return "reserved" +	} +} + +// NewNamespaceEd25519V1 returns an new Ed25519V1 namespace based on a +// verification key. +func NewNamespaceEd25519V1(vk []byte) (*Namespace, error) { +	if len(vk) != 32 { +		return nil, fmt.Errorf("invalid verification key: must be 32 bytes") +	} +	return &Namespace{ +		Format: NamespaceFormatEd25519V1, +		NamespaceEd25519V1: &NamespaceEd25519V1{ +			Namespace: vk, +		}, +	}, nil +} + +// Verify checks that signature is valid over message for this namespace +func (ns *Namespace) Verify(message, signature []byte) error { +	switch ns.Format { +	case NamespaceFormatEd25519V1: +		if !ed25519.Verify(ed25519.PublicKey(ns.NamespaceEd25519V1.Namespace), message, signature) { +			return fmt.Errorf("ed25519 signature verification failed") +		} +	default: +		return fmt.Errorf("namespace not supported: %v", ns.Format) +	} +	return nil +} + +func (ns *Namespace) Marshal() ([]byte, error) { +	serialized, err := tls.Marshal(*ns) +	if err != nil { +		return nil, fmt.Errorf("marshaled failed for namespace(%v): %v", ns.Format, err) +	} +	return serialized, err +} + +func (ns *Namespace) Unmarshal(serialized []byte) error { +	extra, err := tls.Unmarshal(serialized, ns) +	if err != nil { +		return fmt.Errorf("unmarshal failed for namespace: %v", err) +	} else if len(extra) > 0 { +		return fmt.Errorf("unmarshal found extra data for namespace(%v): %v", ns.Format, err) +	} +	return nil +} + +// NamespacePool is a pool of namespaces that contain complete verification keys +type NamespacePool struct { +	pool map[string]*Namespace +	list []*Namespace +	// If we need to update this structure without a restart => add mutex. +} + +// NewNameSpacePool creates a new namespace pool from a list of namespaces.  An +// error is returned if there are duplicate namespaces or namespaces without a +// complete verification key.  The latter is determined by namespaceWithKey(). +func NewNamespacePool(namespaces []*Namespace) (*NamespacePool, error) { +	np := &NamespacePool{ +		pool: make(map[string]*Namespace), +		list: make([]*Namespace, 0), +	} +	for _, namespace := range namespaces { +		if !namespaceWithKey(namespace.Format) { +			return nil, fmt.Errorf("need verification key in namespace pool: %v", namespace.Format) +		} +		if _, ok := np.pool[namespace.String()]; ok { +			return nil, fmt.Errorf("duplicate namespace: %v", namespace.String()) +		} +		np.pool[namespace.String()] = namespace +		np.list = append(np.list, namespace) +	} +	return np, nil +} + +// Find checks if namespace is a member of the namespace pool. +func (np *NamespacePool) Find(namespace *Namespace) (*Namespace, bool) { +	if _, ok := np.pool[namespace.String()]; !ok { +		return nil, false +	} +	// If the passed namespace is a key fingerprint the actual key needs to be +	// attached before returning.  Not applicable for Ed25519.  Docdoc later. +	return namespace, true +} + +// List returns a copied list of namespaces that is used by this pool. +func (np *NamespacePool) List() []*Namespace { +	namespaces := make([]*Namespace, len(np.list)) +	copy(namespaces, np.list) +	return namespaces +} + +// namespaceWithKey returns true if a namespace format contains a complete +// verification key.  I.e., some formats might have a key fingerprint instead. +func namespaceWithKey(format NamespaceFormat) bool { +	switch format { +	case NamespaceFormatEd25519V1: +		return true +	default: +		return false +	} +} diff --git a/namespace/namespace_test.go b/namespace/namespace_test.go new file mode 100644 index 0000000..10c0bf0 --- /dev/null +++ b/namespace/namespace_test.go @@ -0,0 +1,218 @@ +package namespace + +import ( +	"bytes" +	"testing" + +	"crypto/ed25519" + +	"github.com/system-transparency/stfe/namespace/testdata" +) + +func TestNewNamespaceEd25519V1(t *testing.T) { +	for _, table := range []struct { +		description string +		vk          []byte +		wantErr     bool +	}{ +		{ +			description: "invalid", +			vk:          append(testdata.Ed25519Vk, 0x00), +			wantErr:     true, +		}, +		{ +			description: "valid", +			vk:          testdata.Ed25519Vk, +		}, +	} { +		n, err := NewNamespaceEd25519V1(table.vk) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := n.Format, NamespaceFormatEd25519V1; got != want { +			t.Errorf("got namespace format %v but wanted %v in test %q", got, want, table.description) +			continue +		} +		if got, want := n.NamespaceEd25519V1.Namespace, table.vk; !bytes.Equal(got, want) { +			t.Errorf("got namespace %X but wanted %X in test %q", got, want, table.description) +		} +	} +} + +func TestVerify(t *testing.T) { +	testMsg := []byte("msg") +	for _, table := range []struct { +		description string +		namespace   *Namespace +		msg, sig    []byte +		wantErr     bool +	}{ +		{ +			description: "invalid: unsupported namespace", +			namespace:   &Namespace{Format: NamespaceFormatReserved}, +			msg:         testMsg, +			sig:         []byte("sig"), +			wantErr:     true, +		}, +		{ +			description: "invalid: bad ed25519 verification key", +			namespace:   mustNewNamespaceEd25519V1(t, testdata.Ed25519Sk[:32]), +			msg:         testMsg, +			sig:         ed25519.Sign(ed25519.PrivateKey(testdata.Ed25519Sk), testMsg), +			wantErr:     true, +		}, +		{ +			description: "invalid: ed25519 signature is not over message", +			namespace:   mustNewNamespaceEd25519V1(t, testdata.Ed25519Vk), +			msg:         append(testMsg, 0x00), +			sig:         ed25519.Sign(ed25519.PrivateKey(testdata.Ed25519Sk), testMsg), +			wantErr:     true, +		}, +		{ +			description: "valid: ed25519", +			namespace:   mustNewNamespaceEd25519V1(t, testdata.Ed25519Vk), +			msg:         testMsg, +			sig:         ed25519.Sign(ed25519.PrivateKey(testdata.Ed25519Sk), testMsg), +		}, +	} { +		err := table.namespace.Verify(table.msg, table.sig) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error=%v but wanted %v in test %q: %v", got, want, table.description, err) +		} +	} +} + +func TestMarshal(t *testing.T) { +	for _, table := range []struct { +		description string +		namespace   *Namespace +		wantErr     bool +		wantBytes   []byte +	}{ +		{ +			description: "invalid ed25519: namespace size too small", +			namespace: &Namespace{ +				Format: NamespaceFormatEd25519V1, +				NamespaceEd25519V1: &NamespaceEd25519V1{ +					Namespace: testdata.Ed25519Vk[:len(testdata.Ed25519Vk)-1], +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid ed25519: namespace size too large", +			namespace: &Namespace{ +				Format: NamespaceFormatEd25519V1, +				NamespaceEd25519V1: &NamespaceEd25519V1{ +					Namespace: append(testdata.Ed25519Vk, 0x00), +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid: ed25519", +			namespace:   mustNewNamespaceEd25519V1(t, testdata.Ed25519Vk), +			// TODO: wantBytes +		}, +	} { +		_, err := table.namespace.Marshal() +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		// TODO: add check that we got the bytes we wanted also +	} +} + +func TestUnmarshal(t *testing.T) { +	// TODO +} + +func TestNewNamespacePool(t *testing.T) { +	ns1, _ := NewNamespaceEd25519V1(testdata.Ed25519Vk) +	ns2, _ := NewNamespaceEd25519V1(make([]byte, 32)) +	nsr := &Namespace{Format: NamespaceFormatReserved} +	for _, table := range []struct { +		description string +		namespaces  []*Namespace +		wantErr     bool +	}{ +		{ +			description: "invalid: duplicate namespace", +			namespaces:  []*Namespace{ns1, ns1, ns2}, +			wantErr:     true, +		}, +		{ +			description: "invalid: namespace without key", +			namespaces:  []*Namespace{ns1, nsr, ns2}, +			wantErr:     true, +		}, +		{ +			description: "valid: empty", +			namespaces:  []*Namespace{}, +		}, +		{ +			description: "valid: one namespace", +			namespaces:  []*Namespace{ns1}, +		}, +		{ +			description: "valid: two namespaces", +			namespaces:  []*Namespace{ns1, ns2}, +		}, +	} { +		_, err := NewNamespacePool(table.namespaces) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +	} +} + +func TestFind(t *testing.T) { +	ns1 := mustNewNamespaceEd25519V1(t, testdata.Ed25519Vk) +	ns2 := mustNewNamespaceEd25519V1(t, make([]byte, 32)) +	pool, _ := NewNamespacePool(nil) +	_, got := pool.Find(ns1) +	if want := false; got != want { +		t.Errorf("got %v but wanted %v in test %q", got, want, "empty pool") +	} + +	pool, _ = NewNamespacePool([]*Namespace{ns1}) +	_, got = pool.Find(ns1) +	if want := true; got != want { +		t.Errorf("got %v but wanted %v in test %q", got, want, "non-empty pool: looking for member") +	} +	_, got = pool.Find(ns2) +	if want := false; got != want { +		t.Errorf("got %v but wanted %v in test %q", got, want, "non-empty pool: looking for non-member") +	} +} + +func TestList(t *testing.T) { +	ns1 := mustNewNamespaceEd25519V1(t, testdata.Ed25519Vk) +	ns2 := mustNewNamespaceEd25519V1(t, make([]byte, 32)) +	namespaces := []*Namespace{ns1, ns2} +	pool, _ := NewNamespacePool(namespaces) +	l1 := pool.List() +	if got, want := len(l1), len(namespaces); got != want { +		t.Errorf("got len %v but wanted %v", got, want) +	} + +	l1[0] = ns2 +	l2 := pool.List() +	if bytes.Equal(l1[0].NamespaceEd25519V1.Namespace, l2[0].NamespaceEd25519V1.Namespace) { +		t.Errorf("returned list is not a copy") +	} +} + +func mustNewNamespaceEd25519V1(t *testing.T, vk []byte) *Namespace { +	namespace, err := NewNamespaceEd25519V1(vk) +	if err != nil { +		t.Fatalf("must make ed25519 namespace: %v", err) +	} +	return namespace +} diff --git a/namespace/testdata/data.go b/namespace/testdata/data.go new file mode 100644 index 0000000..2cf4b11 --- /dev/null +++ b/namespace/testdata/data.go @@ -0,0 +1,15 @@ +package testdata + +import ( +	"encoding/base64" +) + +var ( +	Ed25519Vk = mustDecodeB64("HOQFUkKNWpjYAhNKTyWCzahlI7RDtf5123kHD2LACj0=") +	Ed25519Sk = mustDecodeB64("Zaajc50Xt1tNpTj6WYkljzcVjLXL2CcQcHFT/xZqYEcc5AVSQo1amNgCE0pPJYLNqGUjtEO1/nXbeQcPYsAKPQ==") +) + +func mustDecodeB64(s string) []byte { +	b, _ := base64.StdEncoding.DecodeString(s) +	return b +} | 
