aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus.dahlberg@kau.se>2021-01-26 20:10:27 +0100
committerRasmus Dahlberg <rasmus.dahlberg@kau.se>2021-01-26 20:10:27 +0100
commitf5ad698cdb0fc9ecd8ad4c7b2cb7ec11ac0435ef (patch)
treee7c7e27981c7be6bbaecdca892cb2a8808d9a900
parent40b29c54a7cfd459d60aa854558876872a1dd527 (diff)
added namespace package
-rw-r--r--namespace/namespace.go150
-rw-r--r--namespace/namespace_test.go218
-rw-r--r--namespace/testdata/data.go15
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
+}