You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/accesskey/accesskey.go

244 lines
8.4 KiB

// Package accesskey implements the DateKeys Access Key, the portable .dkk
// credential (spec §38, §40-§44).
//
// A .dkk is a sensitive capability (spec §7.4). Its X25519 identity is stored
// as 32 raw bytes; the Bech32 AGE-SECRET-KEY-1... form is only an export
// format for humans (spec §38). No type in this package prints the material.
package accesskey
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/extension"
)
// Framing and schema constants (spec §40, §41).
const (
Magic = "DKK1"
FramingVersion = 1
PreludeSize = 12
// MaxBodyLen is the parser limit of spec §57, checked before allocating.
MaxBodyLen = 16 << 20
TypeTag = "datekeys-access-key"
SchemaVersion = 1
// TypeX25519 is the only access_type of V1 (spec §41).
TypeX25519 = "x25519"
idSize = 16
digestSize = 32
x25519Size = 32
)
// AccessKey is a decoded .dkk.
type AccessKey struct {
CredentialID [16]byte // key 2, random and opaque (spec §42)
CapsuleID [16]byte // key 3, the only capsule this credential is for (spec §38)
Type string // key 4, access_type
Material []byte // key 5, access_material: 32 raw X25519 identity bytes. SECRET.
// Verification is key 6, optional; nil when absent (spec §43, §58.1).
Verification *Verification
Critical []extension.Extension // key 7
Noncritical []extension.Extension // key 8
}
// Verification is verification_metadata (spec §43). It supports fast failure
// and UX only; it is not a security property.
type Verification struct {
CapsuleDigest []byte // key 0, SHA-256 of the exact .dkc bytes
}
type bodyWire struct {
Type string `cbor:"0,keyasint"`
Version uint64 `cbor:"1,keyasint"`
CredentialID []byte `cbor:"2,keyasint"`
CapsuleID []byte `cbor:"3,keyasint"`
AccessType string `cbor:"4,keyasint"`
Material []byte `cbor:"5,keyasint"`
Verification *verificationWire `cbor:"6,keyasint,omitempty"`
Critical []extension.Wire `cbor:"7,keyasint,omitempty"`
Noncritical []extension.Wire `cbor:"8,keyasint,omitempty"`
}
type verificationWire struct {
CapsuleDigest []byte `cbor:"0,keyasint,omitempty"`
}
// String describes k without its material.
func (k AccessKey) String() string {
return fmt.Sprintf("AccessKey{credential_id=%x capsule_id=%x type=%s material=REDACTED}", k.CredentialID, k.CapsuleID, k.Type)
}
// GoString describes k without its material.
func (k AccessKey) GoString() string { return k.String() }
// Identity returns the age identity of an x25519 access key.
func (k *AccessKey) Identity() (age.Identity, error) {
if err := k.validateMaterial(); err != nil {
return nil, err
}
id, err := agewrap.X25519IdentityFromRaw(k.Material)
if err != nil {
return nil, fmt.Errorf("accesskey: %v: %w", err, datekeys.ErrAccessInvalid)
}
return id, nil
}
// Wipe overwrites the material in place. It is best effort: Go may have made
// copies that cannot be reached.
func (k *AccessKey) Wipe() { clear(k.Material) }
func (k *AccessKey) validateMaterial() error {
if k.Type != TypeX25519 {
return fmt.Errorf("accesskey: access_type %q is not supported by V1: %w", k.Type, datekeys.ErrAccessInvalid)
}
if len(k.Material) != x25519Size {
return fmt.Errorf("accesskey: x25519 access_material is %d bytes, want %d: %w", len(k.Material), x25519Size, datekeys.ErrAccessInvalid)
}
return nil
}
// MarshalBody returns BODY_CBOR, the Deterministic CBOR body of k (spec §41).
func (k *AccessKey) MarshalBody() ([]byte, error) {
if err := k.validateMaterial(); err != nil {
return nil, err
}
w := bodyWire{
Type: TypeTag,
Version: SchemaVersion,
CredentialID: k.CredentialID[:],
CapsuleID: k.CapsuleID[:],
AccessType: k.Type,
Material: k.Material,
}
if k.Verification != nil {
if len(k.Verification.CapsuleDigest) != digestSize {
// An empty map is not a canonical representation of absence (spec §43).
return nil, fmt.Errorf("accesskey: capsule_digest must be %d bytes: %w", digestSize, datekeys.ErrNonCanonicalCBOR)
}
w.Verification = &verificationWire{CapsuleDigest: k.Verification.CapsuleDigest}
}
var err error
if w.Critical, err = extension.Encode(k.Critical); err != nil {
return nil, err
}
if w.Noncritical, err = extension.Encode(k.Noncritical); err != nil {
return nil, err
}
if err := extension.CheckDisjoint(k.Critical, k.Noncritical); err != nil {
return nil, err
}
b, err := codec.Marshal(w)
if err != nil {
return nil, err
}
if len(b) > MaxBodyLen {
return nil, fmt.Errorf("accesskey: body of %d bytes exceeds %d", len(b), MaxBodyLen)
}
return b, nil
}
// Encode writes k as a complete .dkk: prelude and BODY_CBOR (spec §40).
func Encode(w io.Writer, k *AccessKey) error {
body, err := k.MarshalBody()
if err != nil {
return err
}
var pre [PreludeSize]byte
copy(pre[0:4], Magic)
pre[4] = FramingVersion
binary.BigEndian.PutUint32(pre[8:12], uint32(len(body)))
if _, err := w.Write(pre[:]); err != nil {
return err
}
_, err = w.Write(body)
return err
}
// Decode reads exactly one .dkk from r and validates its framing, its
// canonical body and its fields. Bytes after BODY_CBOR are rejected.
//
// Decode does not decide whether critical extensions are known; the consumer
// checks them against its extension.Registry (capsule.Open does).
func Decode(r io.Reader) (*AccessKey, error) {
var pre [PreludeSize]byte
n, err := io.ReadFull(r, pre[:])
if n < 4 || string(pre[0:4]) != Magic {
return nil, fmt.Errorf("accesskey: %w", datekeys.ErrInvalidMagic)
}
if err != nil {
return nil, fmt.Errorf("accesskey: truncated prelude: %w", datekeys.ErrIntegrity)
}
if pre[4] != FramingVersion {
return nil, fmt.Errorf("accesskey: framing version %d: %w", pre[4], datekeys.ErrUnsupportedVersion)
}
if pre[5] != 0 || pre[6] != 0 || pre[7] != 0 {
return nil, fmt.Errorf("accesskey: flags %#x, reserved %#x%02x: %w", pre[5], pre[6], pre[7], datekeys.ErrInvalidFlags)
}
bodyLen := binary.BigEndian.Uint32(pre[8:12])
if bodyLen > MaxBodyLen {
return nil, fmt.Errorf("accesskey: BODY_LEN %d exceeds the %d-byte limit: %w", bodyLen, MaxBodyLen, datekeys.ErrIntegrity)
}
// The buffer grows with the data actually read, so a short file that
// declares a large BODY_LEN does not force an allocation of that size.
var buf bytes.Buffer
if _, err := io.CopyN(&buf, r, int64(bodyLen)); err != nil {
return nil, fmt.Errorf("accesskey: truncated body: %w", datekeys.ErrIntegrity)
}
body := buf.Bytes()
var extra [1]byte
switch n, err := io.ReadFull(r, extra[:]); {
case n != 0:
return nil, fmt.Errorf("accesskey: data after BODY_CBOR: %w", datekeys.ErrIntegrity)
case !errors.Is(err, io.EOF):
return nil, fmt.Errorf("accesskey: reading after BODY_CBOR: %w", err)
}
return DecodeBody(body)
}
// DecodeBody validates and decodes BODY_CBOR.
func DecodeBody(body []byte) (*AccessKey, error) {
if err := codec.CheckSchema(body, TypeTag, SchemaVersion); err != nil {
return nil, fmt.Errorf("accesskey: %w", err)
}
var w bodyWire
if err := codec.Unmarshal(body, &w); err != nil {
return nil, fmt.Errorf("accesskey: %w", err)
}
if len(w.CredentialID) != idSize || len(w.CapsuleID) != idSize {
return nil, fmt.Errorf("accesskey: credential_id and capsule_id must be %d bytes: %w", idSize, datekeys.ErrNonCanonicalCBOR)
}
k := &AccessKey{Type: w.AccessType, Material: bytes.Clone(w.Material)}
copy(k.CredentialID[:], w.CredentialID)
copy(k.CapsuleID[:], w.CapsuleID)
if w.Verification != nil {
if len(w.Verification.CapsuleDigest) != digestSize {
return nil, fmt.Errorf("accesskey: verification_metadata must hold a %d-byte capsule_digest: %w", digestSize, datekeys.ErrNonCanonicalCBOR)
}
k.Verification = &Verification{CapsuleDigest: bytes.Clone(w.Verification.CapsuleDigest)}
}
var err error
if k.Critical, err = extension.Decode(w.Critical); err != nil {
return nil, fmt.Errorf("accesskey: critical_extensions: %w", err)
}
if k.Noncritical, err = extension.Decode(w.Noncritical); err != nil {
return nil, fmt.Errorf("accesskey: noncritical_extensions: %w", err)
}
if err := extension.CheckDisjoint(k.Critical, k.Noncritical); err != nil {
return nil, fmt.Errorf("accesskey: %w", err)
}
if err := k.validateMaterial(); err != nil {
return nil, err
}
clear(w.Material)
return k, nil
}

Powered by TurnKey Linux.