Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
package capsule
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/hmac"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
|
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
|
|
"g.activething.com/go/DateKeys/profile"
|
|
|
|
|
"g.activething.com/go/DateKeys/provider"
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// OpenOptions configures Open.
|
|
|
|
|
type OpenOptions struct {
|
|
|
|
|
// Registry holds the locally pinned profiles. Required.
|
|
|
|
|
Registry profile.Registry
|
|
|
|
|
// Extensions lists the extensions the application implements; see
|
|
|
|
|
// InspectOptions.Extensions.
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
Extensions extension.Registry
|
|
|
|
|
// Source fetches the release. Required. Its answer is always verified
|
|
|
|
|
// locally.
|
|
|
|
|
Source provider.ReleaseSource
|
|
|
|
|
// Identities are the caller's own X25519 identities, for time_and_key
|
|
|
|
|
// capsules encrypted to known recipients.
|
|
|
|
|
Identities []age.Identity
|
|
|
|
|
// AccessKey is a portable .dkk, for time_and_key capsules.
|
|
|
|
|
AccessKey *accesskey.AccessKey
|
|
|
|
|
// Now is the clock. Required: no package of this module reads the wall
|
|
|
|
|
// clock on its own. Open does not ask Source for a round whose time has
|
|
|
|
|
// not been reached.
|
|
|
|
|
Now func() time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Opened describes a capsule that Open decrypted completely.
|
|
|
|
|
type Opened struct {
|
|
|
|
|
Inspection *Inspection
|
|
|
|
|
Release provider.Release
|
|
|
|
|
// ControlCritical and ControlNoncritical are the extensions of the sealed
|
|
|
|
|
// CONTROL_CBOR, only visible after opening.
|
|
|
|
|
ControlCritical []extension.Extension
|
|
|
|
|
ControlNoncritical []extension.Extension
|
|
|
|
|
// UnusableControlExtensions and UnusableAccessKeyExtensions are the known
|
|
|
|
|
// noncritical extensions of CONTROL_CBOR and of the .dkk whose data
|
|
|
|
|
// OpenOptions.Extensions rejects. They do not fail Open; the application
|
|
|
|
|
// must not use them (spec §54). The PUBLIC_HEADER ones are in Inspection.
|
|
|
|
|
UnusableControlExtensions []extension.Unusable
|
|
|
|
|
UnusableAccessKeyExtensions []extension.Unusable
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open runs the complete decryption flow of spec §63 and streams the
|
|
|
|
|
// plaintext to dst.
|
|
|
|
|
//
|
|
|
|
|
// Everything verifiable locally is checked before a release is requested or a
|
|
|
|
|
// secret is used (steps 1 to 8, the presence and capsule binding of access
|
|
|
|
|
// credentials, and the .dkk capsule_digest when r is seekable). The stanza
|
|
|
|
|
// rules are enforced again, as a MUST, by the identities that open each age
|
|
|
|
|
// file (steps 11, 13 and 17).
|
|
|
|
|
//
|
|
|
|
|
// Open returns nil only after age has authenticated the whole payload
|
|
|
|
|
// (step 18). On error, dst may hold a partial plaintext that MUST be
|
|
|
|
|
// discarded: write to a temporary file and publish it only on success
|
|
|
|
|
// (spec §56), as the datekeys CLI does.
|
|
|
|
|
func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*Opened, error) {
|
|
|
|
|
if opts.Source == nil {
|
|
|
|
|
return nil, errors.New("capsule: OpenOptions.Source is required")
|
|
|
|
|
}
|
|
|
|
|
if opts.Now == nil {
|
|
|
|
|
return nil, errors.New("capsule: OpenOptions.Now is required")
|
|
|
|
|
}
|
|
|
|
|
start, seekable := int64(0), false
|
|
|
|
|
if s, ok := r.(io.Seeker); ok {
|
|
|
|
|
if pos, err := s.Seek(0, io.SeekCurrent); err == nil {
|
|
|
|
|
start, seekable = pos, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Steps 1 to 8.
|
|
|
|
|
in, st, err := inspect(r, InspectOptions{Registry: opts.Registry, Extensions: opts.Extensions})
|
|
|
|
|
out := &Opened{Inspection: in}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
h, p := in.Header, in.Profile
|
|
|
|
|
|
|
|
|
|
// Access credentials are checked before any network request.
|
|
|
|
|
var ids []age.Identity
|
|
|
|
|
if h.Policy == TimeAndKey {
|
|
|
|
|
ids = append(ids, opts.Identities...)
|
|
|
|
|
if k := opts.AccessKey; k != nil {
|
|
|
|
|
if err := checkAccessKey(k, h, opts.Extensions); err != nil {
|
|
|
|
|
return out, in.fail(9, "access credential", err)
|
|
|
|
|
}
|
|
|
|
|
out.UnusableAccessKeyExtensions = extension.CheckNoncritical(k.Noncritical, opts.Extensions)
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
if k.Verification != nil && seekable {
|
|
|
|
|
payload, err := checkCapsuleDigest(r.(io.ReadSeeker), start, in.PayloadOffset, k.Verification.CapsuleDigest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(9, "access credential", err)
|
|
|
|
|
}
|
|
|
|
|
st.payload = payload
|
|
|
|
|
}
|
|
|
|
|
id, err := k.Identity()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(9, "access credential", err)
|
|
|
|
|
}
|
|
|
|
|
ids = append(ids, id)
|
|
|
|
|
}
|
|
|
|
|
if len(ids) == 0 {
|
|
|
|
|
return out, in.fail(9, "access credential", fmt.Errorf("capsule: time_and_key capsule and no identity or .dkk supplied: %w", datekeys.ErrAccessRequired))
|
|
|
|
|
}
|
|
|
|
|
in.pass(9, "access credential", fmt.Sprintf("%d identities to try%s", len(ids), unusable(out.UnusableAccessKeyExtensions)))
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 9: obtain the release, never before its round time.
|
|
|
|
|
cond := provider.Condition{Round: h.DateKey.Round}
|
|
|
|
|
if now := opts.Now(); now.Before(in.UnlockAt) {
|
|
|
|
|
err := fmt.Errorf("capsule: round %d is published at %s, it is %s: %w", cond.Round,
|
|
|
|
|
in.UnlockAt.Format(time.RFC3339), now.UTC().Format(time.RFC3339), datekeys.ErrReleaseUnavailable)
|
|
|
|
|
return out, in.fail(9, "release", err)
|
|
|
|
|
}
|
|
|
|
|
release, err := opts.Source.Fetch(ctx, p, cond)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
err = fmt.Errorf("capsule: %v: %w", err, datekeys.ErrReleaseUnavailable)
|
|
|
|
|
}
|
|
|
|
|
return out, in.fail(9, "release", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(9, "release", fmt.Sprintf("round %d obtained", release.Round))
|
|
|
|
|
|
|
|
|
|
// Step 10: verify the release locally.
|
|
|
|
|
if err := provider.Verify(p, cond, release); err != nil {
|
|
|
|
|
return out, in.fail(10, "release verification", err)
|
|
|
|
|
}
|
|
|
|
|
out.Release = release
|
|
|
|
|
in.pass(10, "release verification", "BLS signature valid under the pinned key")
|
|
|
|
|
|
|
|
|
|
// Step 11: open OUTER_TIME_AGE with the strict tlock identity.
|
|
|
|
|
timeID, err := agewrap.NewTimeIdentity(p, cond.Round, release)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(11, "open sealed control", err)
|
|
|
|
|
}
|
|
|
|
|
inner, err := decryptAll(st.sealed, timeID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(11, "open sealed control", err)
|
|
|
|
|
}
|
|
|
|
|
defer clear(inner)
|
|
|
|
|
in.pass(11, "open sealed control", "one tlock stanza, header MAC valid")
|
|
|
|
|
|
|
|
|
|
// Step 12: the structure must match access_policy (spec §36).
|
|
|
|
|
var controlBytes []byte
|
|
|
|
|
switch h.Policy {
|
|
|
|
|
case TimeOnly:
|
|
|
|
|
if looksLikeAge(inner) {
|
|
|
|
|
return out, in.fail(12, "policy structure", fmt.Errorf("capsule: time_only capsule seals an age file: %w", datekeys.ErrPolicyStructureMismatch))
|
|
|
|
|
}
|
|
|
|
|
controlBytes = inner
|
|
|
|
|
in.pass(12, "policy structure", "time_only: CONTROL_CBOR sealed directly")
|
|
|
|
|
case TimeAndKey:
|
|
|
|
|
stanzas, err := agewrap.Stanzas(bytes.NewReader(inner))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(12, "policy structure", fmt.Errorf("capsule: time_and_key capsule does not seal an age file: %w", datekeys.ErrPolicyStructureMismatch))
|
|
|
|
|
}
|
|
|
|
|
if err := agewrap.CheckAccessStanzas(stanzas); err != nil {
|
|
|
|
|
return out, in.fail(12, "policy structure", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(12, "policy structure", fmt.Sprintf("time_and_key: INNER_ACCESS_AGE with %d X25519 stanzas", len(stanzas)))
|
|
|
|
|
|
|
|
|
|
// Step 13: open INNER_ACCESS_AGE with the caller's identities.
|
|
|
|
|
accessID, err := agewrap.NewAccessIdentity(ids...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(13, "open access layer", err)
|
|
|
|
|
}
|
|
|
|
|
if controlBytes, err = decryptAll(inner, accessID); err != nil {
|
|
|
|
|
return out, in.fail(13, "open access layer", err)
|
|
|
|
|
}
|
|
|
|
|
defer clear(controlBytes)
|
|
|
|
|
in.pass(13, "open access layer", "identity matched exactly one stanza")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 14: parse the canonical CONTROL_CBOR.
|
|
|
|
|
control, err := DecodeControl(controlBytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(14, "control", err)
|
|
|
|
|
}
|
|
|
|
|
defer clear(control.PayloadIdentity[:])
|
|
|
|
|
if err := extension.CheckCritical(control.Critical, opts.Extensions); err != nil {
|
|
|
|
|
return out, in.fail(14, "control", fmt.Errorf("capsule: CONTROL_CBOR: %w", err))
|
|
|
|
|
}
|
|
|
|
|
out.ControlCritical, out.ControlNoncritical = control.Critical, control.Noncritical
|
|
|
|
|
out.UnusableControlExtensions = extension.CheckNoncritical(control.Noncritical, opts.Extensions)
|
|
|
|
|
in.pass(14, "control", "canonical CONTROL_CBOR"+unusable(out.UnusableControlExtensions))
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
|
|
|
|
|
// Step 15: verify header_binding over the exact stored bytes.
|
|
|
|
|
binding := HeaderBinding(st.prelude, in.PublicHeader)
|
|
|
|
|
if !hmac.Equal(binding[:], control.HeaderBinding[:]) {
|
|
|
|
|
return out, in.fail(15, "header binding", fmt.Errorf("capsule: header_binding does not match PRELUDE || PUBLIC_HEADER: %w", datekeys.ErrHeaderBinding))
|
|
|
|
|
}
|
|
|
|
|
in.pass(15, "header binding", "matches")
|
|
|
|
|
|
|
|
|
|
// Steps 16 and 17: recover I_PAYLOAD and open PAYLOAD_AGE with it.
|
|
|
|
|
payloadID, err := agewrap.NewPayloadIdentity(control.PayloadIdentity[:])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(16, "payload identity", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(16, "payload identity", "I_PAYLOAD recovered")
|
|
|
|
|
pr, err := age.Decrypt(st.payload, payloadID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, in.fail(17, "open payload", classify("PAYLOAD_AGE", err))
|
|
|
|
|
}
|
|
|
|
|
if _, err := io.Copy(dst, pr); err != nil {
|
|
|
|
|
return out, in.fail(17, "open payload", classify("PAYLOAD_AGE", err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 18: age completed without error.
|
|
|
|
|
in.pass(18, "commit", "payload authenticated completely")
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkAccessKey validates a .dkk against the capsule before it is used: the
|
|
|
|
|
// capsule_id, and its critical extensions as in step 4.
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
func checkAccessKey(k *accesskey.AccessKey, h *Header, reg extension.Registry) error {
|
|
|
|
|
if k.CapsuleID != h.CapsuleID {
|
|
|
|
|
return fmt.Errorf("capsule: the .dkk is for capsule %x, this is %x: %w", k.CapsuleID, h.CapsuleID, datekeys.ErrAccessInvalid)
|
|
|
|
|
}
|
|
|
|
|
if err := extension.CheckCritical(k.Critical, reg); err != nil {
|
|
|
|
|
return fmt.Errorf("capsule: .dkk: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkCapsuleDigest compares the .dkk capsule_digest with SHA-256 of the
|
|
|
|
|
// .dkc (spec §43), then repositions r at the payload. The digest is a fast
|
|
|
|
|
// failure for a wrong file, not a security property.
|
|
|
|
|
func checkCapsuleDigest(r io.ReadSeeker, start, payloadOffset int64, want []byte) (io.Reader, error) {
|
|
|
|
|
if _, err := r.Seek(start, io.SeekStart); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("capsule: %w", err)
|
|
|
|
|
}
|
|
|
|
|
sum := sha256.New()
|
|
|
|
|
if _, err := io.Copy(sum, r); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("capsule: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !hmac.Equal(sum.Sum(nil), want) {
|
|
|
|
|
return nil, fmt.Errorf("capsule: the .dkk capsule_digest does not match this .dkc: %w", datekeys.ErrAccessInvalid)
|
|
|
|
|
}
|
|
|
|
|
if _, err := r.Seek(start+payloadOffset, io.SeekStart); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("capsule: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return r, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decryptAll opens a bounded, in-memory age file. The plaintext is never
|
|
|
|
|
// longer than the ciphertext, so it is read into one buffer of that size:
|
|
|
|
|
// the plaintext may hold I_PAYLOAD, and no outgrown buffer is left behind.
|
|
|
|
|
// The caller wipes the result; on error it is wiped here.
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
func decryptAll(ciphertext []byte, id age.Identity) ([]byte, error) {
|
|
|
|
|
r, err := age.Decrypt(bytes.NewReader(ciphertext), id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, classify("age", err)
|
|
|
|
|
}
|
|
|
|
|
// Not io.ReadFull: it would turn the age reader's io.ErrUnexpectedEOF,
|
|
|
|
|
// a truncated STREAM, into the end of a short read.
|
|
|
|
|
out := make([]byte, len(ciphertext))
|
|
|
|
|
for n := 0; ; {
|
|
|
|
|
m, err := r.Read(out[n:])
|
|
|
|
|
n += m
|
|
|
|
|
switch {
|
|
|
|
|
case err == io.EOF:
|
|
|
|
|
clear(out[n:]) // Read may use the whole of its argument as scratch space.
|
|
|
|
|
return out[:n], nil
|
|
|
|
|
case err != nil:
|
|
|
|
|
clear(out)
|
|
|
|
|
return nil, classify("age", err)
|
|
|
|
|
case n == len(out):
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// classify keeps the normative error an identity returned from Unwrap, and
|
|
|
|
|
// maps every other age failure (malformed header, bad header MAC, STREAM
|
|
|
|
|
// authentication, truncation, trailing data) to ErrIntegrity.
|
|
|
|
|
func classify(what string, err error) error {
|
|
|
|
|
if datekeys.Code(err) != "" {
|
|
|
|
|
return fmt.Errorf("capsule: %s: %w", what, err)
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("capsule: %s: %v: %w", what, err, datekeys.ErrIntegrity)
|
|
|
|
|
}
|