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/open.go

570 lines
24 KiB

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 release object 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 its release object (spec v0.15, §47.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
}

Powered by TurnKey Linux.