Commit 301958e3 authored by ale's avatar ale

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.
parent 2934fd63
......@@ -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.
// 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,
}
}
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)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment