From 301958e3493e263eb6ea269bf7b8644fbcd97394 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Thu, 21 Mar 2019 10:42:03 +0000
Subject: [PATCH] Add library for common composite types used in LDAP backend

This is shared code used by id/auth, id/keystore, and others. Unifying
it into its own package helps prevent drift among implementations.
---
 README.md                                   |   9 +-
 ldap/compositetypes/composite_types.go      | 158 ++++++++++++++++++++
 ldap/compositetypes/composite_types_test.go |  54 +++++++
 3 files changed, 218 insertions(+), 3 deletions(-)
 create mode 100644 ldap/compositetypes/composite_types.go
 create mode 100644 ldap/compositetypes/composite_types_test.go

diff --git a/README.md b/README.md
index 534838b..756a743 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,13 @@ A quick overview of the contents:
   "RPC" implementation, just JSON POST requests but with retries,
   backoff, timeouts, tracing, etc.
 
-* [server implementation of a line-based protocol over a UNIX socket](unix/)
+* [server implementation of a generic line-based protocol over a UNIX
+  socket](unix/).
 
-* a [LDAP connection pool](ldap/)
+* a [LDAP connection pool](ldap/).
+
+* utilities to [serialize composite data types](ldap/compositetypes/)
+  used in our LDAP database.
 
 * a [password hashing library](pwhash/) that uses fancy advanced
   crypto by default but is also backwards compatible with old
@@ -19,4 +23,3 @@ A quick overview of the contents:
 
 * utilities to [manage encryption keys](userenckey/), themselves
   encrypted with a password and a KDF.
-
diff --git a/ldap/compositetypes/composite_types.go b/ldap/compositetypes/composite_types.go
new file mode 100644
index 0000000..32a1978
--- /dev/null
+++ b/ldap/compositetypes/composite_types.go
@@ -0,0 +1,158 @@
+// Package compositetypes provides Go types for the composite values
+// stored in our LDAP database, so that various authentication
+// packages can agree on their serialized representation.
+//
+// These are normally 1-to-many associations that are wrapped into
+// repeated LDAP attributes instead of separate nested objects, for
+// simplicity and latency reasons.
+//
+// Whenever there is an 'id' field, it's a unique (per-user)
+// identifier used to recognize a specific entry on modify/delete.
+//
+// The serialized values can be arbitrary []byte sequences (the LDAP
+// schema should specify the right types for the associated
+// attributes).
+//
+package compositetypes
+
+import (
+	"crypto/elliptic"
+	"errors"
+	"strings"
+
+	"github.com/tstranex/u2f"
+)
+
+// AppSpecificPassword stores information on an application-specific
+// password.
+//
+// Serialized as colon-separated fields with the format:
+//
+//     id:service:encrypted_password:comment
+//
+// Where 'comment' is free-form and can contain colons, no escaping is
+// performed.
+type AppSpecificPassword struct {
+	ID                string `json:"id"`
+	Service           string `json:"service"`
+	EncryptedPassword string `json:"encrypted_password"`
+	Comment           string `json:"comment"`
+}
+
+// Marshal returns the serialized format.
+func (p *AppSpecificPassword) Marshal() string {
+	return strings.Join([]string{
+		p.ID,
+		p.Service,
+		p.EncryptedPassword,
+		p.Comment,
+	}, ":")
+}
+
+// UnmarshalAppSpecificPassword parses a serialized representation of
+// an AppSpecificPassword.
+func UnmarshalAppSpecificPassword(s string) (*AppSpecificPassword, error) {
+	parts := strings.SplitN(s, ":", 4)
+	if len(parts) != 4 {
+		return nil, errors.New("badly encoded app-specific password")
+	}
+	return &AppSpecificPassword{
+		ID:                parts[0],
+		Service:           parts[1],
+		EncryptedPassword: parts[2],
+		Comment:           parts[3],
+	}, nil
+}
+
+// EncryptedKey stores a password-encrypted secret key.
+//
+// Serialized as colon-separated fields with the format:
+//
+//     id:encrypted_key
+//
+// The encrypted key is stored as a raw, unencoded byte sequence.
+type EncryptedKey struct {
+	ID           string `json:"id"`
+	EncryptedKey []byte `json:"encrypted_key"`
+}
+
+// Marshal returns the serialized format.
+func (k *EncryptedKey) Marshal() string {
+	var b []byte
+	b = append(b, []byte(k.ID)...)
+	b = append(b, ':')
+	b = append(b, k.EncryptedKey...)
+	return string(b)
+}
+
+// UnmarshalEncryptedKey parses the serialized representation of an
+// EncryptedKey.
+func UnmarshalEncryptedKey(s string) (*EncryptedKey, error) {
+	idx := strings.IndexByte(s, ':')
+	if idx < 0 {
+		return nil, errors.New("badly encoded key")
+	}
+	return &EncryptedKey{
+		ID:           s[:idx],
+		EncryptedKey: []byte(s[idx+1:]),
+	}, nil
+}
+
+// U2FRegistration stores information on a single U2F device
+// registration.
+//
+// The serialized format follows part of the U2F standard and just
+// stores 64 bytes of the public key immediately followed by the key
+// handle data, with no encoding.
+//
+// The data in U2FRegistration is still encoded, but it can be turned
+// into a usable form (github.com/tstranex/u2f.Registration) later.
+type U2FRegistration struct {
+	KeyHandle []byte `json:"key_handle"`
+	PublicKey []byte `json:"public_key"`
+}
+
+// Marshal returns the serialized format.
+func (r *U2FRegistration) Marshal() string {
+	var b []byte
+	b = append(b, r.PublicKey...)
+	b = append(b, r.KeyHandle...)
+	return string(b)
+}
+
+// UnmarshalU2FRegistration parses a U2FRegistration from its serialized format.
+func UnmarshalU2FRegistration(s string) (*U2FRegistration, error) {
+	if len(s) < 64 {
+		return nil, errors.New("badly encoded u2f registration")
+	}
+	b := []byte(s)
+	return &U2FRegistration{
+		PublicKey: b[:64],
+		KeyHandle: b[64:],
+	}, nil
+}
+
+// Decode returns a u2f.Registration object with the decoded public
+// key ready for use in verification.
+func (r *U2FRegistration) Decode() (*u2f.Registration, error) {
+	x, y := elliptic.Unmarshal(elliptic.P256(), r.PublicKey)
+	if x == nil {
+		return nil, errors.New("invalid public key")
+	}
+	var reg u2f.Registration
+	reg.PubKey.Curve = elliptic.P256()
+	reg.PubKey.X = x
+	reg.PubKey.Y = y
+	reg.KeyHandle = r.KeyHandle
+	return &reg, nil
+}
+
+// NewU2FRegistrationFromData creates a U2FRegistration from a
+// u2f.Registration object.
+func NewU2FRegistrationFromData(reg *u2f.Registration) *U2FRegistration {
+	pk := elliptic.Marshal(reg.PubKey.Curve, reg.PubKey.X, reg.PubKey.Y)
+	return &U2FRegistration{
+		PublicKey: pk,
+		KeyHandle: reg.KeyHandle,
+	}
+}
diff --git a/ldap/compositetypes/composite_types_test.go b/ldap/compositetypes/composite_types_test.go
new file mode 100644
index 0000000..a02b835
--- /dev/null
+++ b/ldap/compositetypes/composite_types_test.go
@@ -0,0 +1,54 @@
+package compositetypes
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestAppSpecificPassword_Serialization(t *testing.T) {
+	asp := &AppSpecificPassword{
+		ID:                "abc",
+		Service:           "service",
+		EncryptedPassword: "$1$1234$5678abcdef",
+		Comment:           "this: is a comment with a colon in it",
+	}
+
+	out, err := UnmarshalAppSpecificPassword(asp.Marshal())
+	if err != nil {
+		t.Fatalf("Unmarshal: %v", err)
+	}
+	if diffs := cmp.Diff(asp, out); diffs != "" {
+		t.Fatalf("result differs: %s", diffs)
+	}
+}
+
+func TestEncryptedKey_Serialization(t *testing.T) {
+	key := &EncryptedKey{
+		ID:           "main",
+		EncryptedKey: []byte("this is a very secret key\x00"),
+	}
+
+	out, err := UnmarshalEncryptedKey(key.Marshal())
+	if err != nil {
+		t.Fatalf("Unmarshal: %v", err)
+	}
+	if diffs := cmp.Diff(key, out); diffs != "" {
+		t.Fatalf("result differs: %s", diffs)
+	}
+}
+
+func TestU2FRegistration_Serialization(t *testing.T) {
+	key := &U2FRegistration{
+		KeyHandle: []byte("\x04\xc8\x1d\xd3\x9e~/\xb8\xedG(\xcb\x82\xf1\x0f\xb5\xac\xd6\xaf~\xc7\xfa\xb8\x96P\x91\xecJ\xa1,TRF\x88\xd1\x1a\xdaQ<\xd8-a\xd7\xb0\xd9v\xd7\xe8f\x8e\xab\xf6\x10\x895\xe6\x9f\xf3\x86;\xab\xc1\xae\x83^"),
+		PublicKey: []byte("w'Ra\xd8\x17\xdf\x86\x06\xb0\xd0\x8f\x0eI\x98\xd7\xc1\xf7\xb0}j\xc3\x1c8\xf0\x8fh\xcf\xe0\x84W\xc6\xa3\x1d\xc8e/\xa5]v \xfa]\xa5\xfb\xd5c\xbe\xc72\xb9\x80\xa9\xc0O\xd1\xe5\x9d\xe0\xcd\x19q@\xeb"),
+	}
+
+	out, err := UnmarshalU2FRegistration(key.Marshal())
+	if err != nil {
+		t.Fatalf("Unmarshal: %v", err)
+	}
+	if diffs := cmp.Diff(key, out); diffs != "" {
+		t.Fatalf("result differs: %s", diffs)
+	}
+}
-- 
GitLab