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" ) // 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. Extensions extension.Registry // Source fetches the release, unless Release is set. Its answer is always verified // locally, at step 10. A source that fetches releases over a network, // like provider/drand.Client, verifies each response itself and discards // the invalid ones, so that Open reports ErrReleaseUnavailable at step 9 // when none is valid; a release that a source hands over as the caller // supplied it gets the codes of step 10 (spec §63). Any error of Source // is reported at step 9 with ErrReleaseUnavailable as its only normative // code: the text of an error that carries another code, or none, is // kept, not its code. Source provider.ReleaseSource // Release is a release the caller has in hand, the alternative to // Source (spec v0.15, §63 step 9.c): a .dkr file or drand's JSON read // with provider.Encoded, or a local archive with provider.Archive. It // makes no network request, so Open asks it for the release without // comparing Now with the round time, and reports in // Opened.ClockBehind a clock that is behind it. An error of Release is // reported at step 9 with ErrReleaseUnavailable as its only code, as one // of Source; the release it supplies is decoded and verified at step 10, // with the codes of that step. Exactly one of Source and Release must be // set. Release provider.Supplier // Identities are the caller's own X25519 identities, for time_and_key // capsules encrypted to known recipients. Nil entries are ignored. Identities []age.Identity // AccessKey is a portable .dkk already decoded, for time_and_key // capsules. AccessKey *accesskey.AccessKey // AccessKeyFile is a portable .dkk still encoded, the alternative to // AccessKey: Open decodes it at step 9.a, and reads it only for a // time_and_key capsule, so that its errors, framing included, are // reported in the order of spec §63 (§69.1). A caller that decodes a .dkk // itself reports its decoding errors first, whatever the capsule. Open // wipes the key it decodes. At most one of AccessKey and AccessKeyFile // may be set. AccessKeyFile io.Reader // 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; it does not compare it with a Release in hand. Now func() time.Time // Sink receives the files of a format 3 capsule; dst receives the // content of formats 1 and 2. Open fails right after step 2 with // ErrSinkRequired when a format 3 capsule has no Sink. Sink Sink // AuthorKeys are the author keys that the person saved, by their // dkauthor1… string, with the label she gave each: a valid signature of // one of them is F3 and not F4 (spec v0.11, §29.7). Nil for none. AuthorKeys map[string]string // Accept, when set, receives the verdicts of a format 3 capsule after // every check of step 17 and before step 18. An error of Accept is // returned as it is, without a normative code, and the Sink is aborted: // nothing is published. A caller that expects a signature uses it so that // files it would not trust are never written where they would be used. Accept func(Verdicts) error } // Opened describes a capsule that Open decrypted completely. type Opened struct { Inspection *Inspection // Release is the release that opened the capsule, verified at step 10, // with the chain hash of the pinned profile: provider.EncodeRelease // gives the .dkr that a reader saves next to the capsule (spec v0.15, // §62.1). Release provider.Release // ClockBehind reports that the release was in the caller's hand and that // Now was before the round time of the DateKey: the release proves the // round was published, so the clock is probably behind, which a reader // may say (spec v0.15, §63 step 9.c). ClockBehind bool // Format is the format of the capsule (spec §22). A caller should show // it: format 1 does not hide the number of credentials or the exact // length of the content (spec §55.2, §70). Format Format // PayloadLength is L, the number of bytes of content written to dst. In // format 2, Padding is the padding rule sealed in the control and // PaddedLength is P = rule(L), the length of the plaintext of // PAYLOAD_AGE, whose padding is checked and never written (spec §29.1); // in format 1 both are zero. PayloadLength uint64 Padding Padding PaddedLength uint64 // In format 3, Head is the head, as step 17.4 validated it, Verdicts // are the verdicts of the security area, which a caller shows before the // declared author and the comment (spec §29.7), and AreaLen is the size // of that area. PayloadLength is then the length of BODY. Head *Head Verdicts Verdicts AreaLen uint32 // UnusableHeadExtensions are the known noncritical extensions of the // head that OpenOptions.Extensions rejects, as UnusableControlExtensions // are for the control (spec §29.4, §54). UnusableHeadExtensions []extension.Unusable // 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 } // 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 and, // in format 2, after the length and the padding of its plaintext have been // checked against L and the padding rule of the control (steps 17 and 18). In // format 2 only the first L bytes, the content, are written to dst; the // padding never is. On error, dst may hold a partial plaintext that MUST be // discarded, and that must not be presented as valid: 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) == (opts.Release == nil) { return nil, errors.New("capsule: set exactly one of OpenOptions.Source and OpenOptions.Release") } if opts.Now == nil { return nil, errors.New("capsule: OpenOptions.Now is required") } if opts.AccessKey != nil && opts.AccessKeyFile != nil { return nil, errors.New("capsule: set OpenOptions.AccessKey or AccessKeyFile, not both") } 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. Right after step 2, the format decides where the content // goes: a format 3 capsule needs a Sink, and the others dst. in, st, err := inspect(r, InspectOptions{Registry: opts.Registry, Extensions: opts.Extensions}, func(p Prelude) error { switch { case p.Format == Format3 && opts.Sink == nil: return ErrSinkRequired case p.Format != Format3 && dst == nil: return errWriterRequired } return nil }) out := &Opened{Inspection: in, Format: in.Prelude.Format} if err != nil { return out, err } h, p, format := in.Header, in.Profile, in.Prelude.Format // Access credentials are checked before any network request (spec §63 // step 9), and only for time_and_key: under time_only they play no part. // The .dkk as an object first, decoded here when it is still encoded, // then its bindings to this capsule, the capsule_id and, when the reader // is seekable, the capsule_digest (spec §69.1). Then at least one // credential (9.b); nil identities are not credentials. var ids []age.Identity if h.Policy == TimeAndKey { for _, id := range opts.Identities { if id != nil { ids = append(ids, id) } } k := opts.AccessKey if opts.AccessKeyFile != nil { if k, err = accesskey.Decode(opts.AccessKeyFile); err != nil { return out, in.fail(9, "access credential", err) } defer k.Wipe() } if k != nil { id, err := checkAccessKey(k, h, opts.Extensions) if err != nil { return out, in.fail(9, "access credential", err) } out.UnusableAccessKeyExtensions = extension.CheckNoncriticalIn(extension.AccessKey, k.Noncritical, opts.Extensions) 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 } 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))) } // Step 9: obtain the release. A network source is never asked before the // round time (9.c); a release in hand is not compared with the clock, // whose being behind it is only reported (spec v0.15). A network source // has verified each response with the rules of step 10 and discarded the // invalid ones. Whatever the failure of the source, none valid included, // it is ErrReleaseUnavailable here. cond := provider.Condition{Round: h.DateKey.Round} now := opts.Now() var release provider.Release if opts.Release != nil { encoded, err := opts.Release.Supply(p, cond) if err != nil { return out, in.fail(9, "release", sourceFailure(err)) } out.ClockBehind = now.Before(in.UnlockAt) detail := "release supplied by the caller" if out.ClockBehind { detail = fmt.Sprintf("release supplied by the caller; round %d is published at %s and the clock says %s: it may be behind", cond.Round, in.UnlockAt.Format(time.RFC3339), now.UTC().Format(time.RFC3339)) } in.pass(9, "release", detail) // Step 10 starts with the layers of the release object. if release, err = provider.ParseRelease(encoded); err != nil { return out, in.fail(10, "release verification", err) } } else { if 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) } var err error if release, err = opts.Source.Fetch(ctx, p, cond); err != nil { return out, in.fail(9, "release", sourceFailure(err)) } in.pass(9, "release", fmt.Sprintf("round %d obtained", release.Round)) } // Step 10: verify the release locally: the chain it names, its round and // its signature. if err := provider.Verify(p, cond, release); err != nil { return out, in.fail(10, "release verification", err) } release.ChainHash = bytes.Clone(p.ChainHash[:]) 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), and in // format 2 INNER_ACCESS_AGE holds exactly 16 stanzas (spec §39). 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)) } slots := accessSlots(format) if err := agewrap.CheckAccessStanzas(stanzas, slots); 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(slots, 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, whose schema version is the // format of the capsule. control, err := DecodeControl(controlBytes, format) if err != nil { return out, in.fail(14, "control", err) } defer clear(control.PayloadIdentity[:]) if err := extension.CheckCriticalIn(extension.Control, 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.CheckNoncriticalIn(extension.Control, control.Noncritical, opts.Extensions) in.pass(14, "control", "canonical CONTROL_CBOR"+unusable(out.UnusableControlExtensions)) // 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") // Step 16: recover I_PAYLOAD and, in format 2, L and the padding rule, // which step 14 validated, so that P = rule(L) cannot fail. L and P are // not frame lengths: nothing is allocated according to them (spec §57). payloadID, err := agewrap.NewPayloadIdentity(control.PayloadIdentity[:]) if err != nil { return out, in.fail(16, "payload identity", err) } detail := "I_PAYLOAD recovered" if format.padded() { if out.PaddedLength, err = PaddedLength(control.PayloadLength, control.Padding); err != nil { return out, in.fail(16, "payload identity", err) } out.Padding = control.Padding detail = fmt.Sprintf("I_PAYLOAD recovered; L = %d, padding %s, P = %d", control.PayloadLength, control.Padding, out.PaddedLength) } in.pass(16, "payload identity", detail) // Step 17: open PAYLOAD_AGE with I_PAYLOAD. In format 2 the plaintext is // exactly P bytes and its bytes L to P-1 are zero; only the first L are // written, and the padding is read and checked, never written. pr, err := age.Decrypt(st.payload, payloadID) if err != nil { return out, in.fail(17, "open payload", classify("PAYLOAD_AGE", ageHeaderFailure, err)) } if format == Format3 { out.PayloadLength = control.PayloadLength if err := openBody(pr, control.PayloadLength, out.PaddedLength, opts.Sink, opts.Extensions, newSecurityContext(control, format, in.UnlockAt, opts.AuthorKeys), opts.Accept, out); err != nil { var r *refused if errors.As(err, &r) { in.pass(17, "open payload", "payload authenticated; the caller refused its verdicts and nothing was published") return out, r.err } return out, in.fail(17, "open payload", err) } in.pass(17, "open payload", fmt.Sprintf("payload authenticated; BODY of %d bytes, %d files, area of %d bytes", control.PayloadLength, len(out.Head.Files), out.AreaLen)) in.pass(18, "commit", fmt.Sprintf("%d files", len(out.Head.Files))) return out, nil } w := &plaintextWriter{w: dst} var n int64 if format == Format2 { n, err = io.CopyN(w, pr, int64(control.PayloadLength)) if err == io.EOF { err = fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, shorter than P = %d: %w", n, out.PaddedLength, datekeys.ErrIntegrity) } else if err == nil { err = checkPadding(pr, uint64(n), out.PaddedLength) } } else { n, err = io.Copy(w, pr) } out.PayloadLength = uint64(n) if err != nil { switch { case w.err != nil: // dst failed, not age: the caller's own error keeps its text. err = fmt.Errorf("capsule: PAYLOAD_AGE: writing the plaintext: %w: %w", w.err, datekeys.ErrIntegrity) case datekeys.Code(err) == "": err = classify("PAYLOAD_AGE", ageStreamFailure, err) } return out, in.fail(17, "open payload", err) } if format == Format2 { in.pass(17, "open payload", fmt.Sprintf("payload authenticated; %d bytes of plaintext, zero after byte %d", out.PaddedLength, n)) } else { in.pass(17, "open payload", "payload authenticated") } // Step 18: every check of step 17 passed. in.pass(18, "commit", fmt.Sprintf("%d bytes of content", n)) return out, nil } // accessSlots is the number of stanzas INNER_ACCESS_AGE must hold in a // capsule of format f, 0 meaning one or more (spec §33, §39). func accessSlots(f Format) int { if f.padded() { return agewrap.AccessSlots } return 0 } // checkPadding reads the rest of the plaintext of a format 2 PAYLOAD_AGE, // after its first l bytes, and checks that it is the padding: exactly p - l // zero bytes (spec §29.1, §63 step 17). It fails as soon as the plaintext // goes past P or a byte is not zero; a plaintext shorter than P is found at // the end of the STREAM. Every failure is ErrIntegrity, whatever the moment // it is found (spec §69.1). func checkPadding(r io.Reader, l, p uint64) error { buf := make([]byte, 16<<10) for at := l; ; { n, err := r.Read(buf) for i, b := range buf[:n] { switch { case at+uint64(i) >= p: return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is longer than P = %d: %w", p, datekeys.ErrIntegrity) case b != 0: return fmt.Errorf("capsule: PAYLOAD_AGE: byte %d of the plaintext is padding and is not zero: %w", at+uint64(i), datekeys.ErrIntegrity) } } at += uint64(n) switch { case err == io.EOF && at < p: return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, shorter than P = %d: %w", at, p, datekeys.ErrIntegrity) case err == io.EOF: return nil case err != nil: return err } } } // sourceFailure is the error of a release source as step 9 reports it: // ErrReleaseUnavailable, the only code of spec §63 step 9 for a source that // delivers no release. An error that wraps it and no other normative error // is kept as it is, with any other cause it wraps, such as a context error. // Any other error, whether it carries another normative code or none, is // kept as text only, so that the result wraps exactly one code. func sourceFailure(err error) error { if onlyCode(err, datekeys.ErrReleaseUnavailable) { return err } return fmt.Errorf("capsule: release source: %v: %w", err, datekeys.ErrReleaseUnavailable) } // onlyCode reports whether code is the one normative error that err wraps. func onlyCode(err error, code *datekeys.Error) bool { for _, e := range datekeys.All() { if errors.Is(err, e) != (e == code) { return false } } return true } // checkAccessKey validates a .dkk before it is used and returns its // identity. The .dkk is checked as an object first, its semantic fields in // ascending key order (spec §69.1, layer 4): access_type and access_material // (keys 4 and 5), then its critical extensions as in step 4 (key 7). Only // then its binding to this capsule: the capsule_id (key 3). The // capsule_digest (key 6), a binding too, is checked last, by the caller. func checkAccessKey(k *accesskey.AccessKey, h *Header, reg extension.Registry) (age.Identity, error) { id, err := k.Identity() if err != nil { return nil, err } if err := extension.CheckCriticalIn(extension.AccessKey, k.Critical, reg); err != nil { return nil, fmt.Errorf("capsule: .dkk: %w", err) } if k.CapsuleID != h.CapsuleID { return nil, fmt.Errorf("capsule: the .dkk is for capsule %x, this is %x: %w", k.CapsuleID, h.CapsuleID, datekeys.ErrAccessInvalid) } return id, 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. func decryptAll(ciphertext []byte, id age.Identity) ([]byte, error) { r, err := age.Decrypt(bytes.NewReader(ciphertext), id) if err != nil { return nil, classify("age", ageHeaderFailure, 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", ageStreamFailure, err) case n == len(out): return out, nil } } } // The failures of age that no identity reports, by the phase in which they // happen: age.Decrypt, which parses the header and checks its MAC once an // identity has unwrapped the file key, and then reading the STREAM. const ( ageHeaderFailure = "the age header is malformed or truncated, or its MAC does not verify" ageStreamFailure = "the age payload is truncated, has trailing data or fails STREAM authentication" ) // classify keeps the normative error an identity returned from Unwrap, and // maps every other failure of age to ErrIntegrity with the fixed reason of // its phase. The text of age's errors is not copied, as in package agewrap. func classify(what, reason string, err error) error { if datekeys.Code(err) != "" { return fmt.Errorf("capsule: %s: %w", what, err) } return fmt.Errorf("capsule: %s: %s: %w", what, reason, datekeys.ErrIntegrity) } // plaintextWriter records the first error of the writer of the plaintext, so // that a failure of the caller's writer is not reported as one of age. type plaintextWriter struct { w io.Writer err error } func (p *plaintextWriter) Write(b []byte) (int, error) { n, err := p.w.Write(b) if err != nil && p.err == nil { p.err = err } return n, err }