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"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
|
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
|
|
"g.activething.com/go/DateKeys/profile"
|
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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// InspectOptions configures Inspect.
|
|
|
|
|
type InspectOptions struct {
|
|
|
|
|
// Registry holds the locally pinned profiles. Required.
|
|
|
|
|
Registry profile.Registry
|
|
|
|
|
// Extensions lists the extensions the application implements. Nil knows
|
|
|
|
|
// none, the state of the base protocol V1. When it is also an
|
|
|
|
|
// extension.DataValidator, the data of the known extensions is checked
|
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
// (spec §54). When it is also an extension.Placement, an extension it
|
|
|
|
|
// knows is known only in the objects and arrays it registers it for, and
|
|
|
|
|
// unknown elsewhere (spec §54, §72).
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckResult records one step of the flow of spec §63.
|
|
|
|
|
type CheckResult struct {
|
|
|
|
|
Step int `json:"step"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
OK bool `json:"ok"`
|
|
|
|
|
Detail string `json:"detail,omitempty"`
|
|
|
|
|
// Error is the normative code of a failed step, for example
|
|
|
|
|
// "ERR_ROUND_MISMATCH".
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StanzaInfo is the visible part of an age recipient stanza.
|
|
|
|
|
type StanzaInfo struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Args []string `json:"args"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inspection is the result of the pre-unlock validation, steps 1 to 8 of
|
|
|
|
|
// spec §63. It is produced without network access and without secrets.
|
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// Prelude.Format is the format of the capsule once step 2 has passed: a
|
|
|
|
|
// caller should show it, because format 1 does not hide the number of
|
|
|
|
|
// credentials or the exact length of the content (spec §55.2, §70).
|
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
|
|
|
type Inspection struct {
|
|
|
|
|
Prelude Prelude
|
|
|
|
|
PublicHeader []byte // exact PUBLIC_HEADER bytes
|
|
|
|
|
Header *Header
|
|
|
|
|
Profile *profile.Profile
|
|
|
|
|
UnlockAt time.Time // effective round time of the DateKey
|
|
|
|
|
PayloadOffset int64
|
|
|
|
|
OuterStanzas []StanzaInfo // OUTER_TIME_AGE
|
|
|
|
|
PayloadStanzas []StanzaInfo // PAYLOAD_AGE
|
|
|
|
|
// UnusableExtensions are the known noncritical PUBLIC_HEADER extensions
|
|
|
|
|
// whose data InspectOptions.Extensions rejects. The capsule stays valid;
|
|
|
|
|
// the application must not use them (spec §54).
|
|
|
|
|
UnusableExtensions []extension.Unusable
|
|
|
|
|
Checks []CheckResult
|
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 (in *Inspection) pass(step int, name, detail string) {
|
|
|
|
|
in.Checks = append(in.Checks, CheckResult{Step: step, Name: name, OK: true, Detail: detail})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (in *Inspection) fail(step int, name string, err error) error {
|
|
|
|
|
in.Checks = append(in.Checks, CheckResult{Step: step, Name: name, Detail: err.Error(), Error: datekeys.Code(err)})
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parsed carries what Open needs after the inspection.
|
|
|
|
|
type parsed struct {
|
|
|
|
|
prelude [PreludeSize]byte
|
|
|
|
|
sealed []byte // OUTER_TIME_AGE
|
|
|
|
|
payload io.Reader // positioned at the start of PAYLOAD_AGE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Inspect runs steps 1 to 8 of spec §63 on the .dkc read from r: framing,
|
|
|
|
|
// canonical PUBLIC_HEADER, canonical DateKey, pinned profile, known critical
|
|
|
|
|
// extensions, the stanza structure of OUTER_TIME_AGE and PAYLOAD_AGE, and the
|
|
|
|
|
// round and chain hash of the tlock stanza. It never contacts a release
|
|
|
|
|
// source and never uses a secret, so an invalid capsule is rejected before it
|
|
|
|
|
// can cause an observable query (spec §27, §63).
|
|
|
|
|
//
|
|
|
|
|
// Inspect reads the prelude, the header, SEALED_CONTROL and the age header of
|
|
|
|
|
// the payload; it does not read the rest of the payload. On failure it
|
|
|
|
|
// returns the partial Inspection together with the error.
|
|
|
|
|
func Inspect(r io.Reader, opts InspectOptions) (*Inspection, error) {
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
in, _, err := inspect(r, opts, 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
|
|
|
return in, err
|
|
|
|
|
}
|
|
|
|
|
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// inspect runs steps 1 to 8. afterPrelude, when not nil, runs right after
|
|
|
|
|
// step 2 passes: its error, a caller's one, ends inspect there.
|
|
|
|
|
func inspect(r io.Reader, opts InspectOptions, afterPrelude func(Prelude) error) (*Inspection, *parsed, error) {
|
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
|
|
|
in := &Inspection{}
|
|
|
|
|
if opts.Registry == nil {
|
|
|
|
|
return in, nil, errors.New("capsule: InspectOptions.Registry is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Steps 1 and 2: parse DKC1 and validate the prelude.
|
|
|
|
|
var pre [PreludeSize]byte
|
|
|
|
|
n, err := io.ReadFull(r, pre[:])
|
|
|
|
|
if err != nil && n >= 4 && string(pre[:4]) == Magic {
|
|
|
|
|
return in, nil, in.fail(1, "parse DKC1", fmt.Errorf("capsule: truncated prelude: %w", datekeys.ErrIntegrity))
|
|
|
|
|
}
|
|
|
|
|
prelude, err := ParsePrelude(pre[:n])
|
|
|
|
|
if errors.Is(err, datekeys.ErrInvalidMagic) {
|
|
|
|
|
return in, nil, in.fail(1, "parse DKC1", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(1, "parse DKC1", "magic DKC1")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(2, "prelude", err)
|
|
|
|
|
}
|
|
|
|
|
in.Prelude = prelude
|
|
|
|
|
in.PayloadOffset = prelude.PayloadOffset()
|
|
|
|
|
in.pass(2, "prelude", fmt.Sprintf("DKC1 v%d, PUBLIC_HEADER_LEN=%d, SEALED_CONTROL_LEN=%d",
|
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
prelude.Format, prelude.PublicHeaderLen, prelude.SealedControlLen))
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
if afterPrelude != nil {
|
|
|
|
|
if err := afterPrelude(prelude); err != nil {
|
|
|
|
|
return in, nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
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 3: read the exact PUBLIC_HEADER bytes.
|
|
|
|
|
hb, err := readExactly(r, int64(prelude.PublicHeaderLen))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(3, "public header", fmt.Errorf("capsule: truncated PUBLIC_HEADER: %w", datekeys.ErrIntegrity))
|
|
|
|
|
}
|
|
|
|
|
in.PublicHeader = hb
|
|
|
|
|
in.pass(3, "public header", fmt.Sprintf("%d bytes", len(hb)))
|
|
|
|
|
|
|
|
|
|
// Step 4: canonical CBOR, canonical DateKey, pinned profile, known
|
|
|
|
|
// critical extensions with valid data.
|
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
|
|
|
h, err := DecodeHeader(hb)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(4, "header validation", err)
|
|
|
|
|
}
|
|
|
|
|
in.Header = h
|
|
|
|
|
p, ok := opts.Registry.Lookup(h.DateKey.ProfileID)
|
|
|
|
|
if !ok {
|
|
|
|
|
return in, nil, in.fail(4, "header validation", fmt.Errorf("capsule: profile %q is not pinned: %w", h.DateKey.ProfileID, datekeys.ErrUnknownProfile))
|
|
|
|
|
}
|
|
|
|
|
in.Profile = p
|
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
if err := extension.CheckCriticalIn(extension.PublicHeader, h.Critical, opts.Extensions); err != 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
|
|
|
return in, nil, in.fail(4, "header validation", fmt.Errorf("capsule: PUBLIC_HEADER: %w", err))
|
|
|
|
|
}
|
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
in.UnusableExtensions = extension.CheckNoncriticalIn(extension.PublicHeader, h.Noncritical, opts.Extensions)
|
|
|
|
|
in.pass(4, "header validation", fmt.Sprintf("capsule_id=%s datekey=%s policy=%s profile=%s%s",
|
|
|
|
|
h.CapsuleIDHex(), h.DateKey.Compact(), h.Policy, p.ID, unusable(in.UnusableExtensions)))
|
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 5: OUTER_TIME_AGE holds exactly one stanza, of type tlock.
|
|
|
|
|
sealed, err := readExactly(r, int64(prelude.SealedControlLen))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(5, "sealed control structure", fmt.Errorf("capsule: truncated SEALED_CONTROL: %w", datekeys.ErrIntegrity))
|
|
|
|
|
}
|
|
|
|
|
outer, err := agewrap.Stanzas(bytes.NewReader(sealed))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(5, "sealed control structure", fmt.Errorf("capsule: SEALED_CONTROL: %w", err))
|
|
|
|
|
}
|
|
|
|
|
in.OuterStanzas = infos(outer)
|
|
|
|
|
if len(outer) != 1 || outer[0].Type != agewrap.StanzaTLock {
|
|
|
|
|
err := fmt.Errorf("capsule: OUTER_TIME_AGE must hold exactly one tlock stanza, found %d: %w", len(outer), datekeys.ErrPolicyStructureMismatch)
|
|
|
|
|
return in, nil, in.fail(5, "sealed control structure", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(5, "sealed control structure", "one tlock stanza")
|
|
|
|
|
|
|
|
|
|
// Step 6: PAYLOAD_AGE holds exactly one stanza, of type X25519. Only its
|
|
|
|
|
// age header is read; the bytes consumed are replayed for decryption.
|
|
|
|
|
var captured bytes.Buffer
|
|
|
|
|
payloadStanzas, err := agewrap.Stanzas(io.TeeReader(r, &captured))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(6, "payload structure", fmt.Errorf("capsule: PAYLOAD_AGE: %w", err))
|
|
|
|
|
}
|
|
|
|
|
in.PayloadStanzas = infos(payloadStanzas)
|
|
|
|
|
if err := agewrap.CheckPayloadStanzas(payloadStanzas); err != nil {
|
|
|
|
|
return in, nil, in.fail(6, "payload structure", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(6, "payload structure", "one X25519 stanza")
|
|
|
|
|
|
|
|
|
|
// Step 7: resolve and verify the time condition locally.
|
|
|
|
|
if err := h.DateKey.Validate(p); err != nil {
|
|
|
|
|
return in, nil, in.fail(7, "condition", err)
|
|
|
|
|
}
|
|
|
|
|
unlock, err := datekey.RoundTime(p, h.DateKey.Round)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return in, nil, in.fail(7, "condition", err)
|
|
|
|
|
}
|
|
|
|
|
in.UnlockAt = unlock
|
|
|
|
|
in.pass(7, "condition", fmt.Sprintf("round %d, unlock at %s", h.DateKey.Round, unlock.Format(time.RFC3339)))
|
|
|
|
|
|
|
|
|
|
// Step 8: the tlock stanza names the DateKey round and the pinned chain.
|
|
|
|
|
if err := agewrap.CheckTimeStanzas(outer, p, h.DateKey.Round); err != nil {
|
|
|
|
|
return in, nil, in.fail(8, "tlock stanza", err)
|
|
|
|
|
}
|
|
|
|
|
in.pass(8, "tlock stanza", fmt.Sprintf("round %d, chain %s", h.DateKey.Round, p.ChainHashHex()))
|
|
|
|
|
|
|
|
|
|
return in, &parsed{
|
|
|
|
|
prelude: pre,
|
|
|
|
|
sealed: sealed,
|
|
|
|
|
payload: io.MultiReader(bytes.NewReader(captured.Bytes()), r),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readExactly reads n bytes. The buffer grows with the data actually read, so
|
|
|
|
|
// a short file that declares a large length (within the §57 limits) does not
|
|
|
|
|
// force an allocation of that size.
|
|
|
|
|
func readExactly(r io.Reader, n int64) ([]byte, error) {
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
|
if _, err := io.CopyN(&b, r, n); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return b.Bytes(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// unusable describes the unusable extensions for a check detail.
|
|
|
|
|
func unusable(u []extension.Unusable) string {
|
|
|
|
|
if len(u) == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf(", %d unusable noncritical extensions", len(u))
|
|
|
|
|
}
|
|
|
|
|
|
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 infos(stanzas []*age.Stanza) []StanzaInfo {
|
|
|
|
|
out := make([]StanzaInfo, len(stanzas))
|
|
|
|
|
for i, s := range stanzas {
|
|
|
|
|
out[i] = StanzaInfo{Type: s.Type, Args: s.Args}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|