diff --git a/README.md b/README.md index 534838bfb168f391f01354ac1d74d976bd66489b..756a743b6f9008194e37d701fd4a23eb818fee6e 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 0000000000000000000000000000000000000000..32a1978c066daad0bf8edc452e61f73f8ac5d818 --- /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 ®, 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 0000000000000000000000000000000000000000..a02b8357068dae7d311dbb944decc1c390429a94 --- /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) + } +}