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/capsule/framing.go

315 lines
11 KiB

// Package capsule implements the DateKeyCap .dkc container (spec §20-§39):
// framing, PUBLIC_HEADER, CONTROL_CBOR, header_binding, the time_only and
// time_and_key constructions, and the encryption (spec §61, §62) and
// decryption (spec §63) flows.
//
// A .dkc is PRELUDE || PUBLIC_HEADER || SEALED_CONTROL || PAYLOAD_AGE, where
// SEALED_CONTROL and PAYLOAD_AGE are complete standard age files and the
// payload runs to EOF (spec §22, §28-§34).
package capsule
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
datekeys "github.com/datekeys/datekeys-go"
"github.com/datekeys/datekeys-go/codec"
"github.com/datekeys/datekeys-go/datekey"
"github.com/datekeys/datekeys-go/extension"
)
// Framing constants (spec §22, §23) and parser limits (spec §57).
const (
Magic = "DKC1"
FramingVersion = 1
PreludeSize = 16
MaxPublicHeaderLen = 1 << 20 // 1 MiB
MaxSealedControlLen = 64 << 20 // 64 MiB
HeaderTypeTag = "datekeycap"
HeaderVersion = 1
ControlTypeTag = "datekeys-control"
ControlVersion = 1
CapsuleIDSize = 16
)
// Policy is the declared access policy of PUBLIC_HEADER (spec §25).
type Policy uint8
// Access policies of V1.
const (
TimeOnly Policy = 0
TimeAndKey Policy = 1
)
func (p Policy) String() string {
switch p {
case TimeOnly:
return "time_only"
case TimeAndKey:
return "time_and_key"
}
return fmt.Sprintf("policy(%d)", uint8(p))
}
// ParsePolicy parses "time_only" or "time_and_key".
func ParsePolicy(s string) (Policy, error) {
switch s {
case "time_only":
return TimeOnly, nil
case "time_and_key":
return TimeAndKey, nil
}
return 0, fmt.Errorf("capsule: unknown access policy %q", s)
}
func (p Policy) valid() bool { return p == TimeOnly || p == TimeAndKey }
// ---------------------------------------------------------------------------
// PRELUDE
// Prelude is the fixed 16-byte PRELUDE (spec §22, §23).
type Prelude struct {
PublicHeaderLen uint32
SealedControlLen uint32
}
// Bytes returns the exact 16 prelude bytes, the ones covered by
// header_binding.
func (p Prelude) Bytes() [PreludeSize]byte {
var b [PreludeSize]byte
copy(b[0:4], Magic)
b[4] = FramingVersion
// FLAGS (b[5]) and RESERVED (b[6:8]) are zero in V1.
binary.BigEndian.PutUint32(b[8:12], p.PublicHeaderLen)
binary.BigEndian.PutUint32(b[12:16], p.SealedControlLen)
return b
}
// PayloadOffset is where PAYLOAD_AGE starts:
// 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN (spec §63).
func (p Prelude) PayloadOffset() int64 {
return PreludeSize + int64(p.PublicHeaderLen) + int64(p.SealedControlLen)
}
// ParsePrelude validates the prelude (spec §22, §63 step 2): magic, version,
// FLAGS == 0, RESERVED == 0 and the length limits of spec §57.
func ParsePrelude(b []byte) (Prelude, error) {
if len(b) < 4 || string(b[0:4]) != Magic {
return Prelude{}, fmt.Errorf("capsule: %w", datekeys.ErrInvalidMagic)
}
if len(b) < PreludeSize {
return Prelude{}, fmt.Errorf("capsule: truncated prelude: %w", datekeys.ErrIntegrity)
}
if b[4] != FramingVersion {
return Prelude{}, fmt.Errorf("capsule: framing version %d: %w", b[4], datekeys.ErrUnsupportedVersion)
}
if b[5] != 0 || b[6] != 0 || b[7] != 0 {
return Prelude{}, fmt.Errorf("capsule: flags %#x, reserved %#02x%02x: %w", b[5], b[6], b[7], datekeys.ErrInvalidFlags)
}
p := Prelude{
PublicHeaderLen: binary.BigEndian.Uint32(b[8:12]),
SealedControlLen: binary.BigEndian.Uint32(b[12:16]),
}
if p.PublicHeaderLen == 0 || p.PublicHeaderLen > MaxPublicHeaderLen {
return Prelude{}, fmt.Errorf("capsule: PUBLIC_HEADER_LEN %d outside 1..%d: %w", p.PublicHeaderLen, MaxPublicHeaderLen, datekeys.ErrIntegrity)
}
if p.SealedControlLen == 0 || p.SealedControlLen > MaxSealedControlLen {
return Prelude{}, fmt.Errorf("capsule: SEALED_CONTROL_LEN %d outside 1..%d: %w", p.SealedControlLen, MaxSealedControlLen, datekeys.ErrIntegrity)
}
return p, nil
}
// HeaderBinding returns SHA-256(PRELUDE || PUBLIC_HEADER_BYTES) over the exact
// stored bytes; the header is never re-serialized for it (spec §26).
func HeaderBinding(prelude [PreludeSize]byte, publicHeader []byte) [32]byte {
h := sha256.New()
h.Write(prelude[:])
h.Write(publicHeader)
var out [32]byte
h.Sum(out[:0])
return out
}
// ---------------------------------------------------------------------------
// PUBLIC_HEADER
// Header is PUBLIC_HEADER (spec §24). There is no separate profile_id: the
// profile comes from the DateKey, the single source of truth.
type Header struct {
CapsuleID [CapsuleIDSize]byte // key 2
DateKey datekey.DateKey // key 3, canonical dk1_
Policy Policy // key 4, access_policy
Critical []extension.Extension // key 5
Noncritical []extension.Extension // key 6
}
type headerWire struct {
Type string `cbor:"0,keyasint"`
Version uint64 `cbor:"1,keyasint"`
CapsuleID []byte `cbor:"2,keyasint"`
DateKey string `cbor:"3,keyasint"`
Policy uint64 `cbor:"4,keyasint"`
Critical []extension.Wire `cbor:"5,keyasint,omitempty"`
Noncritical []extension.Wire `cbor:"6,keyasint,omitempty"`
}
// CapsuleIDHex returns the capsule_id in hexadecimal.
func (h *Header) CapsuleIDHex() string { return hex.EncodeToString(h.CapsuleID[:]) }
// EncodeHeader returns the Deterministic CBOR bytes of h.
func EncodeHeader(h *Header) ([]byte, error) {
compact := h.DateKey.Compact()
if compact == "" {
return nil, fmt.Errorf("capsule: invalid DateKey: %w", datekeys.ErrDateKeyInvalid)
}
if !h.Policy.valid() {
return nil, fmt.Errorf("capsule: unknown access policy %d", h.Policy)
}
w := headerWire{
Type: HeaderTypeTag,
Version: HeaderVersion,
CapsuleID: h.CapsuleID[:],
DateKey: compact,
Policy: uint64(h.Policy),
}
var err error
if w.Critical, err = extension.Encode(h.Critical); err != nil {
return nil, err
}
if w.Noncritical, err = extension.Encode(h.Noncritical); err != nil {
return nil, err
}
if err := extension.CheckDisjoint(h.Critical, h.Noncritical); err != nil {
return nil, err
}
return codec.Marshal(w)
}
// DecodeHeader validates and decodes PUBLIC_HEADER bytes (spec §24, §27,
// §63 step 4): canonical CBOR, the schema, a 16-byte capsule_id, a canonical
// DateKey, a V1 access policy and well-formed extension arrays. Whether the
// profile is pinned and the critical extensions known is decided by the
// caller.
func DecodeHeader(b []byte) (*Header, error) {
if err := codec.CheckSchema(b, HeaderTypeTag, HeaderVersion); err != nil {
return nil, fmt.Errorf("capsule: PUBLIC_HEADER: %w", err)
}
var w headerWire
if err := codec.Unmarshal(b, &w); err != nil {
return nil, fmt.Errorf("capsule: PUBLIC_HEADER: %w", err)
}
if len(w.CapsuleID) != CapsuleIDSize {
return nil, fmt.Errorf("capsule: capsule_id is %d bytes, want %d: %w", len(w.CapsuleID), CapsuleIDSize, datekeys.ErrNonCanonicalCBOR)
}
dk, err := datekey.Parse(w.DateKey)
if err != nil {
return nil, fmt.Errorf("capsule: PUBLIC_HEADER: %w", err)
}
if w.Policy > 1 {
return nil, fmt.Errorf("capsule: access_policy %d is not defined in V1: %w", w.Policy, datekeys.ErrNonCanonicalCBOR)
}
h := &Header{DateKey: dk, Policy: Policy(w.Policy)}
copy(h.CapsuleID[:], w.CapsuleID)
if h.Critical, err = extension.Decode(w.Critical); err != nil {
return nil, fmt.Errorf("capsule: PUBLIC_HEADER critical_extensions: %w", err)
}
if h.Noncritical, err = extension.Decode(w.Noncritical); err != nil {
return nil, fmt.Errorf("capsule: PUBLIC_HEADER noncritical_extensions: %w", err)
}
if err := extension.CheckDisjoint(h.Critical, h.Noncritical); err != nil {
return nil, fmt.Errorf("capsule: PUBLIC_HEADER: %w", err)
}
return h, nil
}
// ---------------------------------------------------------------------------
// CONTROL_CBOR
// Control is CONTROL_CBOR (spec §31). PayloadIdentity is I_PAYLOAD, a secret.
type Control struct {
HeaderBinding [32]byte // key 2
PayloadIdentity [32]byte // key 3, raw X25519 identity bytes. SECRET.
Critical []extension.Extension // key 4
Noncritical []extension.Extension // key 5
}
type controlWire struct {
Type string `cbor:"0,keyasint"`
Version uint64 `cbor:"1,keyasint"`
HeaderBinding []byte `cbor:"2,keyasint"`
PayloadIdentity []byte `cbor:"3,keyasint"`
Critical []extension.Wire `cbor:"4,keyasint,omitempty"`
Noncritical []extension.Wire `cbor:"5,keyasint,omitempty"`
}
// String describes c without I_PAYLOAD.
func (c Control) String() string {
return fmt.Sprintf("Control{header_binding=%x payload_identity=REDACTED}", c.HeaderBinding)
}
// GoString describes c without I_PAYLOAD.
func (c Control) GoString() string { return c.String() }
// EncodeControl returns the Deterministic CBOR bytes of c. The caller must
// wipe the result: it contains I_PAYLOAD.
func EncodeControl(c *Control) ([]byte, error) {
w := controlWire{
Type: ControlTypeTag,
Version: ControlVersion,
HeaderBinding: c.HeaderBinding[:],
PayloadIdentity: c.PayloadIdentity[:],
}
var err error
if w.Critical, err = extension.Encode(c.Critical); err != nil {
return nil, err
}
if w.Noncritical, err = extension.Encode(c.Noncritical); err != nil {
return nil, err
}
if err := extension.CheckDisjoint(c.Critical, c.Noncritical); err != nil {
return nil, err
}
return codec.Marshal(w)
}
// DecodeControl validates and decodes CONTROL_CBOR (spec §31, §63 step 14).
// A non-canonical encoding is rejected even though CONTROL_CBOR is not hashed.
func DecodeControl(b []byte) (*Control, error) {
if err := codec.CheckSchema(b, ControlTypeTag, ControlVersion); err != nil {
return nil, fmt.Errorf("capsule: CONTROL_CBOR: %w", err)
}
var w controlWire
if err := codec.Unmarshal(b, &w); err != nil {
return nil, fmt.Errorf("capsule: CONTROL_CBOR: %w", err)
}
defer clear(w.PayloadIdentity)
if len(w.HeaderBinding) != 32 || len(w.PayloadIdentity) != 32 {
return nil, fmt.Errorf("capsule: header_binding and payload_identity must be 32 bytes: %w", datekeys.ErrNonCanonicalCBOR)
}
c := &Control{}
copy(c.HeaderBinding[:], w.HeaderBinding)
copy(c.PayloadIdentity[:], w.PayloadIdentity)
var err error
if c.Critical, err = extension.Decode(w.Critical); err != nil {
return nil, fmt.Errorf("capsule: CONTROL_CBOR critical_extensions: %w", err)
}
if c.Noncritical, err = extension.Decode(w.Noncritical); err != nil {
return nil, fmt.Errorf("capsule: CONTROL_CBOR noncritical_extensions: %w", err)
}
if err := extension.CheckDisjoint(c.Critical, c.Noncritical); err != nil {
return nil, fmt.Errorf("capsule: CONTROL_CBOR: %w", err)
}
return c, nil
}
// looksLikeAge reports whether b starts with the age v1 intro line.
func looksLikeAge(b []byte) bool {
return bytes.HasPrefix(b, []byte("age-encryption.org/v1\n"))
}

Powered by TurnKey Linux.