diff --git a/capsule/format3.go b/capsule/format3.go new file mode 100644 index 0000000..f1bc5a6 --- /dev/null +++ b/capsule/format3.go @@ -0,0 +1,727 @@ +package capsule + +import ( + "encoding/binary" + "fmt" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/codec" + "g.activething.com/go/DateKeys/extension" + "g.activething.com/go/DateKeys/internal/pathrule" +) + +// The content of a format 3 capsule (spec §29.2): the plaintext of +// PAYLOAD_AGE is BODY followed by its padding, and BODY is a frame of 12 +// bytes, the security area, the head and the files. +const ( + // BodyFrameSize is the size of the frame of BODY: AREA_LEN, SECURITY_LEN + // and HEAD_LEN, three unsigned 32-bit big-endian integers. + BodyFrameSize = 12 + // AreaUnit is the unit of AREA_LEN, and MaxAreaLen its maximum. + AreaUnit = 512 + MaxAreaLen = 128 * AreaUnit + // AreaLen is the size of the security area that writers of this version + // write, always, whatever the capsule holds (spec §29.2, §62.1 rule 13). + AreaLen = 512 + // MaxHeadLen is the maximum of HEAD_LEN, 16 MiB. + MaxHeadLen = 16 << 20 + + SecurityTypeTag = "datekeys-security" + SecurityVersion = 1 + HeadTypeTag = "datekeys-head" + HeadVersion = 1 + + // SaltSize is the size of the salt of the head (spec §29.4). + SaltSize = 32 + // Limits of the head, fixed with format 3 (spec §29.4). + MaxCommentLen = pathrule.MaxCommentLen + MaxAuthorLen = pathrule.MaxAuthorLen + MaxFiles = 65535 + MaxPathLen = pathrule.MaxPathLen + // MaxMTime is 9999-12-31T23:59:59Z in seconds since 1970-01-01 UTC. + MaxMTime = 253402300799 + + // maxSecurityItem bounds the byte strings of keys 2 and 3 of security. + maxSecurityItem = 65536 + // maxAlg bounds alg and seal_type. + maxAlg = 1<<32 - 1 +) + +// BodyFrame is the frame of BODY (spec §29.2). +type BodyFrame struct { + AreaLen, SecurityLen, HeadLen uint32 +} + +// Bytes returns the 12 bytes of the frame. +func (f BodyFrame) Bytes() [BodyFrameSize]byte { + var b [BodyFrameSize]byte + binary.BigEndian.PutUint32(b[0:], f.AreaLen) + binary.BigEndian.PutUint32(b[4:], f.SecurityLen) + binary.BigEndian.PutUint32(b[8:], f.HeadLen) + return b +} + +// ContentLength is C, the length of CONTENT in a BODY of length l whose frame +// is f. ParseBodyFrame has checked that it does not underflow. +func (f BodyFrame) ContentLength(l uint64) uint64 { + return l - BodyFrameSize - uint64(f.AreaLen) - uint64(f.HeadLen) +} + +// ParseBodyFrame decodes the frame of BODY from its first 12 bytes and checks +// it against L, the length of BODY (spec §29.2, §63 step 17.2). Every +// violation is ErrIntegrity. The plaintext of PAYLOAD_AGE is at least 256 +// bytes, so the 12 bytes exist even when L is shorter than the frame. +func ParseBodyFrame(b []byte, l uint64) (BodyFrame, error) { + if len(b) != BodyFrameSize { + return BodyFrame{}, fmt.Errorf("capsule: BODY: frame of %d bytes, want %d", len(b), BodyFrameSize) + } + if l < BodyFrameSize { + return BodyFrame{}, fmt.Errorf("capsule: BODY: L = %d is shorter than the frame of %d bytes: %w", l, BodyFrameSize, datekeys.ErrIntegrity) + } + f := BodyFrame{ + AreaLen: binary.BigEndian.Uint32(b[0:]), + SecurityLen: binary.BigEndian.Uint32(b[4:]), + HeadLen: binary.BigEndian.Uint32(b[8:]), + } + switch { + case f.AreaLen < AreaUnit || f.AreaLen > MaxAreaLen || f.AreaLen%AreaUnit != 0: + return f, fmt.Errorf("capsule: BODY: AREA_LEN %d is not a multiple of %d from %d to %d: %w", f.AreaLen, AreaUnit, AreaUnit, MaxAreaLen, datekeys.ErrIntegrity) + case f.SecurityLen < 1 || f.SecurityLen > f.AreaLen: + return f, fmt.Errorf("capsule: BODY: SECURITY_LEN %d is not from 1 to AREA_LEN = %d: %w", f.SecurityLen, f.AreaLen, datekeys.ErrIntegrity) + case f.HeadLen < 1 || f.HeadLen > MaxHeadLen: + return f, fmt.Errorf("capsule: BODY: HEAD_LEN %d is not from 1 to %d: %w", f.HeadLen, MaxHeadLen, datekeys.ErrIntegrity) + case BodyFrameSize+uint64(f.AreaLen)+uint64(f.HeadLen) > l: + return f, fmt.Errorf("capsule: BODY: the frame, the area of %d bytes and the head of %d bytes exceed L = %d: %w", f.AreaLen, f.HeadLen, l, datekeys.ErrIntegrity) + } + return f, nil +} + +// CheckArea checks that the bytes of the security area after SECURITY_CBOR, +// its first securityLen bytes, are zero (spec §29.2): ErrIntegrity if not. +func CheckArea(area []byte, securityLen uint32) error { + for i, c := range area[securityLen:] { + if c != 0 { + return fmt.Errorf("capsule: BODY: byte %d of the security area is not zero: %w", int(securityLen)+i, datekeys.ErrIntegrity) + } + } + return nil +} + +// Verdict is the result of evaluating the signature or the seal of the +// security area (spec §29.7). A verdict never prevents opening. +type Verdict string + +// The verdicts this version can reach (spec §29.7). It implements no alg and +// no seal_type. +const ( + // VerdictUnreadable (X): security fails its layer 2 or 3; it stands for + // both the signature and the seal. + VerdictUnreadable Verdict = "X" + // VerdictNoSignature (F0): no key 2. + VerdictNoSignature Verdict = "F0" + // VerdictSignatureUnchecked (F1): a signature that does not decode, breaks + // its schema or has an alg this reader does not implement. + VerdictSignatureUnchecked Verdict = "F1" + // VerdictNoSeal (S0): no key 3; nothing is shown about the date. + VerdictNoSeal Verdict = "S0" + // VerdictSealUnsupported (S1): a seal_type this reader does not implement. + VerdictSealUnsupported Verdict = "S1" + // VerdictSealUnreadable (S2): a seal that does not decode or breaks its + // schema. + VerdictSealUnreadable Verdict = "S2" +) + +// Text returns the text of the verdict that the official SDK shows, in +// Spanish (spec §29.7), and "" for S0, which shows nothing. +func (v Verdict) Text() string { + switch v { + case VerdictUnreadable: + return "No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada." + case VerdictNoSignature: + return "Sin firma de autor." + case VerdictSignatureUnchecked: + return "No se ha comprobado ninguna firma: trátala como no firmada." + case VerdictSealUnsupported: + return "Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada." + case VerdictSealUnreadable: + return "El sello de tiempo es ilegible: no prueba nada." + } + return "" +} + +// Verdicts are the verdicts of the security area of a format 3 capsule. +type Verdicts struct { + Signature, Seal Verdict +} + +// Lines are the verdicts as the official SDK shows them, in order: X alone, +// or the signature and then the seal, when it shows something. +func (v Verdicts) Lines() []string { + if v.Signature == VerdictUnreadable { + return []string{VerdictUnreadable.Text()} + } + lines := []string{v.Signature.Text()} + if t := v.Seal.Text(); t != "" { + lines = append(lines, t) + } + return lines +} + +// securityWire is the outer map of SECURITY_CBOR: keys 2 and 3 hold +// separately encoded CBOR (spec §29.3). +type securityWire struct { + signature, seal []byte // nil when absent +} + +func (w *securityWire) encode(e *codec.Encoder) { + n := 2 + if w.signature != nil { + n++ + } + if w.seal != nil { + n++ + } + e.Map(n) + e.Uint(0) + e.Text(SecurityTypeTag) + e.Uint(1) + e.Uint(SecurityVersion) + if w.signature != nil { + e.Uint(2) + e.Bstr(w.signature) + } + if w.seal != nil { + e.Uint(3) + e.Bstr(w.seal) + } +} + +func (w *securityWire) decode(d *codec.Decoder) error { + pairs, err := d.Map(4) + if err != nil { + return err + } + var seen uint + for range pairs { + k, err := d.Key() + if err != nil { + return err + } + switch k { + case 0: + _, err = d.Text(len(SecurityTypeTag)) + case 1: + _, err = d.Uint(SecurityVersion) + case 2: + w.signature, err = d.Bstr(1, maxSecurityItem) + case 3: + w.seal, err = d.Bstr(1, maxSecurityItem) + default: + return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR) + } + if err != nil { + return fmt.Errorf("key %d: %w", k, err) + } + seen |= 1 << k + } + if err := required(seen, 2); err != nil { + return err + } + return d.EndMap() +} + +// EncodeSecurity returns SECURITY_CBOR as a writer of this version writes it: +// empty, {0: "datekeys-security", 1: 1}, 22 bytes (spec §29.3). +func EncodeSecurity() []byte { + var e codec.Encoder + (&securityWire{}).encode(&e) + b, _ := e.Out() + return b +} + +// EncodeSecurityWith returns SECURITY_CBOR with the given contents of keys 2 +// and 3, nil when absent. This version defines no alg and no seal_type: only +// a generator of test vectors writes them (spec §62.1 rule 13). +func EncodeSecurityWith(signature, seal []byte) ([]byte, error) { + var e codec.Encoder + (&securityWire{signature: signature, seal: seal}).encode(&e) + return e.Out() +} + +// EvaluateSecurity reads SECURITY_CBOR and returns its verdicts (spec §29.3, +// §29.7). It never fails: security never decides the opening. For the +// signature and for the seal apart, the first row of the table of §29.7 that +// holds decides: alg and seal_type are read only from content that decodes +// and meets its schema. +func EvaluateSecurity(b []byte) Verdicts { + x := Verdicts{VerdictUnreadable, VerdictUnreadable} + if tag, version, err := codec.Peek(b); err != nil || tag != SecurityTypeTag || version != SecurityVersion { + return x + } + var w securityWire + if err := codec.Unmarshal(b, w.decode, w.encode); err != nil { + return x + } + v := Verdicts{Signature: VerdictNoSignature, Seal: VerdictNoSeal} + if w.signature != nil { + // A content that does not decode and an alg this version does not + // implement give the same verdict, and this version implements none. + v.Signature = VerdictSignatureUnchecked + } + if w.seal != nil { + if _, err := decodeSeal(w.seal); err != nil { + v.Seal = VerdictSealUnreadable + } else { + v.Seal = VerdictSealUnsupported + } + } + return v +} + +// authorSignature is the content of key 2 of security: {0: alg, 1: public +// key, 2: signature} (spec §29.3). +type authorSignature struct { + alg uint64 + key, value []byte +} + +func (a *authorSignature) encode(e *codec.Encoder) { + e.Map(3) + e.Uint(0) + e.Uint(a.alg) + e.Uint(1) + e.Bstr(a.key) + e.Uint(2) + e.Bstr(a.value) +} + +func (a *authorSignature) decode(d *codec.Decoder) error { + return decodeItem(d, 3, func(k uint64) (err error) { + switch k { + case 0: + a.alg, err = decodeAlg(d) + case 1: + a.key, err = d.Bstr(0, maxSecurityItem) + case 2: + a.value, err = d.Bstr(0, maxSecurityItem) + default: + err = fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR) + } + return err + }) +} + +// decodeAuthorSignature decodes the content of key 2 of security. +func decodeAuthorSignature(b []byte) (*authorSignature, error) { + var a authorSignature + if err := codec.Unmarshal(b, a.decode, a.encode); err != nil { + return nil, err + } + return &a, nil +} + +// EncodeAuthorSignature returns the content of key 2 of security for a +// generator of test vectors: this version defines no alg. +func EncodeAuthorSignature(alg uint64, key, signature []byte) ([]byte, error) { + var e codec.Encoder + (&authorSignature{alg, key, signature}).encode(&e) + return e.Out() +} + +// seal is the content of key 3 of security: {0: seal_type, 1: token}. +type seal struct { + sealType uint64 + token []byte +} + +func (s *seal) encode(e *codec.Encoder) { + e.Map(2) + e.Uint(0) + e.Uint(s.sealType) + e.Uint(1) + e.Bstr(s.token) +} + +func (s *seal) decode(d *codec.Decoder) error { + return decodeItem(d, 2, func(k uint64) (err error) { + switch k { + case 0: + s.sealType, err = decodeAlg(d) + case 1: + s.token, err = d.Bstr(0, maxSecurityItem) + default: + err = fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR) + } + return err + }) +} + +func decodeSeal(b []byte) (*seal, error) { + var s seal + if err := codec.Unmarshal(b, s.decode, s.encode); err != nil { + return nil, err + } + return &s, nil +} + +// EncodeSeal returns the content of key 3 of security for a generator of +// test vectors: this version defines no seal_type. +func EncodeSeal(sealType uint64, token []byte) ([]byte, error) { + var e codec.Encoder + (&seal{sealType, token}).encode(&e) + return e.Out() +} + +// decodeAlg reads alg or seal_type, from 1 to 2^32 - 1. +func decodeAlg(d *codec.Decoder) (uint64, error) { + v, err := d.Uint(maxAlg) + if err == nil && v == 0 { + err = fmt.Errorf("0 is not defined: %w", datekeys.ErrNonCanonicalCBOR) + } + return v, err +} + +// decodeItem reads a map of exactly n required keys, 0 to n-1, with field. +func decodeItem(d *codec.Decoder, n int, field func(k uint64) error) error { + pairs, err := d.Map(n) + if err != nil { + return err + } + var seen uint + for range pairs { + k, err := d.Key() + if err != nil { + return err + } + if err := field(k); err != nil { + return fmt.Errorf("key %d: %w", k, err) + } + seen |= 1 << k + } + if err := required(seen, n); err != nil { + return err + } + return d.EndMap() +} + +// Head is the head of a format 3 capsule (spec §29.4). +type Head struct { + Salt [SaltSize]byte + // Comment and Author are the comment and the declared author, "" when + // absent. The declared author is text of the creator and proves nothing. + Comment, Author string + // Files are the entries, in strictly ascending byte order of their paths. + Files []File + // Critical and Noncritical are the extensions of the head (keys 6 and 7). + Critical, Noncritical []extension.Extension +} + +// File is an entry of the head: a file of CONTENT. +type File struct { + Path string + Size, Start, End uint64 + SHA256 [32]byte + // MTime is the modification time of the file at its source, in seconds + // since 1970-01-01 UTC, when HasMTime. It is informative. + MTime uint64 + HasMTime bool +} + +// headWire is HEAD_CBOR as it is encoded. +type headWire struct { + h *Head +} + +func (w *headWire) encode(e *codec.Encoder) { + h := w.h + n := 3 + nonEmpty(h.Critical) + nonEmpty(h.Noncritical) + if h.Comment != "" { + n++ + } + if h.Author != "" { + n++ + } + if len(h.Files) > 0 { + n++ + } + e.Map(n) + e.Uint(0) + e.Text(HeadTypeTag) + e.Uint(1) + e.Uint(HeadVersion) + e.Uint(2) + e.Bstr(h.Salt[:]) + if h.Comment != "" { + e.Uint(3) + e.Text(h.Comment) + } + if h.Author != "" { + e.Uint(4) + e.Text(h.Author) + } + if len(h.Files) > 0 { + e.Uint(5) + e.Array(len(h.Files)) + for i := range h.Files { + encodeFile(e, &h.Files[i]) + } + } + encodeExtensions(e, 6, h.Critical, h.Noncritical) +} + +func encodeFile(e *codec.Encoder, f *File) { + n := 5 + if f.HasMTime { + n++ + } + e.Map(n) + e.Uint(0) + e.Text(f.Path) + e.Uint(1) + e.Uint(f.Size) + e.Uint(2) + e.Uint(f.Start) + e.Uint(3) + e.Uint(f.End) + e.Uint(4) + e.Bstr(f.SHA256[:]) + if f.HasMTime { + e.Uint(5) + e.Uint(f.MTime) + } +} + +// decode reads HEAD_CBOR with the rules of the third layer: the CDDL, R1 and +// R8 (spec §29.4, §29.5). The fourth layer comes after, in DecodeHead. +func (w *headWire) decode(d *codec.Decoder) error { + h := w.h + pairs, err := d.Map(8) + if err != nil { + return err + } + var seen uint + for range pairs { + k, err := d.Key() + if err != nil { + return err + } + switch k { + case 0: + _, err = d.Text(len(HeadTypeTag)) + case 1: + _, err = d.Uint(HeadVersion) + case 2: + var salt []byte + if salt, err = d.Bstr(SaltSize, SaltSize); err == nil { + copy(h.Salt[:], salt) + } + case 3: + h.Comment, err = decodeText(d, MaxCommentLen, "comment") + case 4: + h.Author, err = decodeText(d, MaxAuthorLen, "declared author") + case 5: + h.Files, err = decodeFiles(d) + case 6: + h.Critical, err = extension.DecodeArray(d) + case 7: + h.Noncritical, err = extension.DecodeArray(d) + default: + return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR) + } + if err != nil { + return fmt.Errorf("key %d: %w", k, err) + } + seen |= 1 << k + } + if err := required(seen, 3); err != nil { + return err + } + return d.EndMap() +} + +// decodeText reads a text of 1 to max bytes. +func decodeText(d *codec.Decoder, max int, what string) (string, error) { + s, err := d.Text(max) + if err == nil && s == "" { + err = fmt.Errorf("empty %s: %w", what, datekeys.ErrNonCanonicalCBOR) + } + return s, err +} + +// decodeFiles reads the array of entries: 1 to MaxFiles, each path of 1 to +// MaxPathLen bytes (R1), in strictly ascending byte order (R8). +func decodeFiles(d *codec.Decoder) ([]File, error) { + n, err := d.Array(MaxFiles) + if err != nil { + return nil, err + } + if n == 0 { + return nil, fmt.Errorf("empty array of files: %w", datekeys.ErrNonCanonicalCBOR) + } + files := make([]File, 0, min(n, 1024)) + for i := range n { + var f File + if err := decodeItemOptional(d, 5, 6, func(k uint64) (err error) { + switch k { + case 0: + f.Path, err = d.Text(MaxPathLen) + if err == nil && f.Path == "" { + err = fmt.Errorf("R1: the path is empty: %w", datekeys.ErrNonCanonicalCBOR) + } + case 1: + f.Size, err = d.Uint(MaxPayloadLength) + case 2: + f.Start, err = d.Uint(MaxPayloadLength) + case 3: + f.End, err = d.Uint(MaxPayloadLength) + case 4: + var sum []byte + if sum, err = d.Bstr(32, 32); err == nil { + copy(f.SHA256[:], sum) + } + case 5: + f.MTime, err = d.Uint(MaxMTime) + f.HasMTime = err == nil + default: + err = fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR) + } + return err + }); err != nil { + return nil, fmt.Errorf("file %d: %w", i+1, err) + } + if i > 0 && f.Path <= files[i-1].Path { + return nil, fmt.Errorf("file %d: R8: the path is not after the path of file %d in byte order: %w", i+1, i, datekeys.ErrNonCanonicalCBOR) + } + files = append(files, f) + } + return files, nil +} + +// decodeItemOptional reads a map whose keys 0 to required-1 are required and +// whose keys up to max-1 are optional. +func decodeItemOptional(d *codec.Decoder, need, max int, field func(k uint64) error) error { + pairs, err := d.Map(max) + if err != nil { + return err + } + var seen uint + for range pairs { + k, err := d.Key() + if err != nil { + return err + } + if err := field(k); err != nil { + return fmt.Errorf("key %d: %w", k, err) + } + seen |= 1 << k + } + if err := required(seen, need); err != nil { + return err + } + return d.EndMap() +} + +// EncodeHead returns HEAD_CBOR for h. The files must already be in byte +// order of their paths. The writer checks the result with DecodeHead, the +// rules of the reader (spec §62.1 rule 17). +func EncodeHead(h *Head) ([]byte, error) { + w := headWire{h: &Head{Salt: h.Salt, Comment: h.Comment, Author: h.Author, Files: h.Files}} + var err error + if w.h.Critical, err = extension.Canonical(h.Critical); err != nil { + return nil, err + } + if w.h.Noncritical, err = extension.Canonical(h.Noncritical); err != nil { + return nil, err + } + if err := extension.CheckDisjoint(w.h.Critical, w.h.Noncritical); err != nil { + return nil, err + } + if len(h.Files) > MaxFiles { + return nil, fmt.Errorf("capsule: head: %d files, more than %d", len(h.Files), MaxFiles) + } + var e codec.Encoder + w.encode(&e) + return e.Out() +} + +// DecodeHead validates and decodes HEAD_CBOR with the layers of spec §69.1 +// (spec §29.4, §63 step 17.4): the type tag and the version (layer 2), the +// CDDL with R1 and R8 (layer 3), and then, in key order, the comment and the +// declared author (§29.6), the files with R2 to R6c, R10 and their layout, +// R7 and R9 over the tree (§29.5), all ErrHeadInvalid, and the critical +// extensions with reg (layer 4). +func DecodeHead(b []byte, reg extension.Registry) (*Head, error) { + if len(b) > MaxHeadLen { + return nil, fmt.Errorf("capsule: head: %d bytes, more than %d: %w", len(b), MaxHeadLen, datekeys.ErrIntegrity) + } + if err := codec.CheckSchema(b, HeadTypeTag, HeadVersion); err != nil { + return nil, fmt.Errorf("capsule: head: %w", err) + } + h := &Head{} + w := headWire{h: h} + if err := codec.Unmarshal(b, w.decode, w.encode); err != nil { + return nil, fmt.Errorf("capsule: head: %w", err) + } + if err := extension.CheckDisjoint(h.Critical, h.Noncritical); err != nil { + return nil, fmt.Errorf("capsule: head: %w", err) + } + if err := checkHeadFields(h); err != nil { + return nil, err + } + if err := extension.CheckCriticalIn(extension.Head, h.Critical, reg); err != nil { + return nil, fmt.Errorf("capsule: head: %w", err) + } + return h, nil +} + +// checkHeadFields applies the rules of the fourth layer with a code of their +// own, ErrHeadInvalid: keys 3, 4 and 5, in that order. +func checkHeadFields(h *Head) error { + invalid := func(what string, err error) error { + return fmt.Errorf("capsule: head: %s%v: %w", what, err, datekeys.ErrHeadInvalid) + } + if h.Comment != "" { + if err := pathrule.CheckComment(h.Comment); err != nil { + return invalid("comment: ", err) + } + } + if h.Author != "" { + if err := pathrule.CheckAuthor(h.Author); err != nil { + return invalid("declared author: ", err) + } + } + var end uint64 + paths := make([]string, len(h.Files)) + for i := range h.Files { + f := &h.Files[i] + if err := pathrule.CheckPath(f.Path); err != nil { + return invalid(fmt.Sprintf("file %d: ", i+1), err) + } + switch { + case f.Start != end: + return invalid(fmt.Sprintf("file %d: ", i+1), fmt.Errorf("start %d is not %d, the end of the file before", f.Start, end)) + case f.End < f.Start || f.End-f.Start != f.Size: + return invalid(fmt.Sprintf("file %d: ", i+1), fmt.Errorf("from start %d to end %d is not the size %d", f.Start, f.End, f.Size)) + } + end = f.End + paths[i] = f.Path + } + if err := pathrule.CheckTree(paths); err != nil { + return invalid("", err) + } + return nil +} + +// CheckHeadEnd checks that the files fill CONTENT, of c bytes: the last one +// ends at C, or C is 0 without files (spec §29.4, §63 step 17.5). +// ErrIntegrity if not. +func CheckHeadEnd(h *Head, c uint64) error { + var end uint64 + if n := len(h.Files); n > 0 { + end = h.Files[n-1].End + } + if end != c { + return fmt.Errorf("capsule: BODY: the files end at byte %d of a content of %d bytes: %w", end, c, datekeys.ErrIntegrity) + } + return nil +} diff --git a/capsule/format3_test.go b/capsule/format3_test.go new file mode 100644 index 0000000..9d5c945 --- /dev/null +++ b/capsule/format3_test.go @@ -0,0 +1,281 @@ +package capsule_test + +import ( + "bytes" + "errors" + "strings" + "testing" + + datekeys "g.activething.com/go/DateKeys" + "g.activething.com/go/DateKeys/capsule" + "g.activething.com/go/DateKeys/extension" +) + +// Spec §29.2: the frame of BODY and its limits. Every violation is +// ERR_INTEGRITY. +func TestBodyFrame(t *testing.T) { + frame := func(area, sec, head uint32) []byte { + b := capsule.BodyFrame{AreaLen: area, SecurityLen: sec, HeadLen: head}.Bytes() + return b[:] + } + // Valid, at the limits. + for _, c := range []struct { + area, sec, head uint32 + l uint64 + }{ + {512, 22, 53, 577}, + {512, 512, 1, 525}, + {65536, 1, 1 << 24, 12 + 65536 + 1<<24}, + {1024, 1024, 100, 1 << 40}, + } { + f, err := capsule.ParseBodyFrame(frame(c.area, c.sec, c.head), c.l) + if err != nil { + t.Errorf("%+v: %v", c, err) + continue + } + if want := c.l - 12 - uint64(c.area) - uint64(c.head); f.ContentLength(c.l) != want { + t.Errorf("%+v: C = %d, want %d", c, f.ContentLength(c.l), want) + } + } + for name, c := range map[string]struct { + area, sec, head uint32 + l uint64 + }{ + "L shorter than the frame": {512, 22, 53, 11}, + "AREA_LEN 0": {0, 22, 53, 1000}, + "AREA_LEN 511": {511, 22, 53, 1000}, + "AREA_LEN 513": {513, 22, 53, 1000}, + "AREA_LEN 66048": {66048, 22, 53, 100000}, + "SECURITY_LEN 0": {512, 0, 53, 1000}, + "SECURITY_LEN above AREA_LEN": {512, 513, 53, 1000}, + "HEAD_LEN 0": {512, 22, 0, 1000}, + "HEAD_LEN 2^24 + 1": {512, 22, 1<<24 + 1, 1 << 30}, + "frame, area and head above L": {512, 22, 53, 576}, + } { + if _, err := capsule.ParseBodyFrame(frame(c.area, c.sec, c.head), c.l); !errors.Is(err, datekeys.ErrIntegrity) { + t.Errorf("%s: %v", name, err) + } + } + area := make([]byte, 512) + copy(area, capsule.EncodeSecurity()) + if err := capsule.CheckArea(area, 22); err != nil { + t.Fatal(err) + } + area[511] = 1 + if err := capsule.CheckArea(area, 22); !errors.Is(err, datekeys.ErrIntegrity) { + t.Fatalf("a byte of the area that is not zero: %v", err) + } +} + +// Spec §29.3, §29.7: the security area and its verdicts, which never fail. +func TestSecurityVerdicts(t *testing.T) { + empty := capsule.EncodeSecurity() + if len(empty) != 22 { + t.Fatalf("empty security is %d bytes, want 22", len(empty)) + } + sig, err := capsule.EncodeAuthorSignature(1, make([]byte, 32), make([]byte, 64)) + if err != nil { + t.Fatal(err) + } + if len(sig) != 105 { + t.Fatalf("an Ed25519 author-signature is %d bytes, want 105", len(sig)) + } + seal, err := capsule.EncodeSeal(1, []byte{1, 2, 3}) + if err != nil { + t.Fatal(err) + } + with := func(sig, seal []byte) []byte { + b, err := capsule.EncodeSecurityWith(sig, seal) + if err != nil { + t.Fatal(err) + } + return b + } + if n := len(with(sig, nil)); n != 130 { + t.Fatalf("security with a signature is %d bytes, want 130", n) + } + x := capsule.Verdicts{Signature: capsule.VerdictUnreadable, Seal: capsule.VerdictUnreadable} + for name, c := range map[string]struct { + b []byte + want capsule.Verdicts + }{ + "empty": {empty, capsule.Verdicts{Signature: "F0", Seal: "S0"}}, + "a signature of alg 1": {with(sig, nil), capsule.Verdicts{Signature: "F1", Seal: "S0"}}, + "a seal of seal_type 1": {with(nil, seal), capsule.Verdicts{Signature: "F0", Seal: "S1"}}, + "both": {with(sig, seal), capsule.Verdicts{Signature: "F1", Seal: "S1"}}, + "alg 0": {with(mustMarshal(t, map[uint64]any{0: uint64(0), 1: []byte{}, 2: []byte{}}), nil), capsule.Verdicts{Signature: "F1", Seal: "S0"}}, + "a signature that is no map": {with([]byte{0x01}, nil), capsule.Verdicts{Signature: "F1", Seal: "S0"}}, + // The first row that holds decides: a seal with an unknown key and an + // unknown seal_type breaks its schema, S2, before its type is read. + "a seal with an unknown key": {with(nil, []byte{0xa3, 0x00, 0x07, 0x01, 0x41, 0x00, 0x02, 0x00}), capsule.Verdicts{Signature: "F0", Seal: "S2"}}, + "seal_type 0": {with(nil, mustMarshal(t, map[uint64]any{0: uint64(0), 1: []byte{1}})), capsule.Verdicts{Signature: "F0", Seal: "S2"}}, + "a seal that is not CBOR": {with(sig, []byte{0xff}), capsule.Verdicts{Signature: "F1", Seal: "S2"}}, + "version 2": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(2)}), x}, + "another type tag": {mustMarshal(t, map[uint64]any{0: "datekeys-head", 1: uint64(1)}), x}, + "an unknown key 4": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(1), 4: []byte{1}}), x}, + "key 2 not a byte string": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(1), 2: uint64(1)}), x}, + "an empty key 2": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(1), 2: []byte{}}), x}, + "a byte more": {append(bytes.Clone(empty), 0), x}, + "not CBOR": {[]byte("security"), x}, + } { + if got := capsule.EvaluateSecurity(c.b); got != c.want { + t.Errorf("%s: %+v, want %+v", name, got, c.want) + } + } + if got := x.Lines(); len(got) != 1 || !strings.HasPrefix(got[0], "No se han podido") { + t.Errorf("X shows %q", got) + } + if got := (capsule.Verdicts{Signature: "F0", Seal: "S0"}).Lines(); len(got) != 1 || got[0] != "Sin firma de autor." { + t.Errorf("F0 and S0 show %q", got) + } + if got := (capsule.Verdicts{Signature: "F1", Seal: "S1"}).Lines(); len(got) != 2 { + t.Errorf("F1 and S1 show %q", got) + } +} + +func sampleHead() *capsule.Head { + h := &capsule.Head{ + Comment: "Para ti ❤️", + Author: "Ana López", + Files: []capsule.File{ + {Path: "fotos/playa.jpg", Size: 10, Start: 0, End: 10, MTime: 1759190400, HasMTime: true}, + {Path: "nota.txt", Size: 5, Start: 10, End: 15}, + }, + } + h.Salt[0] = 1 + h.Files[0].SHA256[0] = 2 + return h +} + +// Spec §29.4: a head round-trips, and its sizes are those of §29.2. +func TestHeadRoundTrip(t *testing.T) { + h := sampleHead() + b, err := capsule.EncodeHead(h) + if err != nil { + t.Fatal(err) + } + got, err := capsule.DecodeHead(b, nil) + if err != nil { + t.Fatal(err) + } + if got.Comment != h.Comment || got.Author != h.Author || len(got.Files) != 2 || got.Files[0] != h.Files[0] || got.Files[1] != h.Files[1] || got.Salt != h.Salt { + t.Fatalf("round trip: %+v", got) + } + if err := capsule.CheckHeadEnd(got, 15); err != nil { + t.Fatal(err) + } + if err := capsule.CheckHeadEnd(got, 16); !errors.Is(err, datekeys.ErrIntegrity) { + t.Fatalf("files that do not fill the content: %v", err) + } + if err := capsule.CheckHeadEnd(&capsule.Head{}, 0); err != nil { + t.Fatal(err) + } + for _, c := range []struct { + h capsule.Head + size int + }{ + {capsule.Head{}, 53}, + {capsule.Head{Comment: "x"}, 56}, + {capsule.Head{Files: []capsule.File{{Path: "nota.txt", Size: 1000, End: 1000, MTime: 1759190400, HasMTime: true}}}, 117}, + {capsule.Head{Files: []capsule.File{{Path: "a"}}}, 100}, + } { + b, err := capsule.EncodeHead(&c.h) + if err != nil { + t.Fatal(err) + } + if len(b) != c.size { + t.Errorf("%+v: %d bytes, want %d", c.h, len(b), c.size) + } + } +} + +// head builds HEAD_CBOR with the test encoder, for heads that the capsule +// encoder refuses to write. +func head(t *testing.T, fields map[uint64]any) []byte { + m := map[uint64]any{0: "datekeys-head", 1: uint64(1), 2: make([]byte, 32)} + for k, v := range fields { + if v == nil { + delete(m, k) + } else { + m[k] = v + } + } + return mustMarshal(t, m) +} + +func file(path string, size, start, end uint64) map[uint64]any { + return map[uint64]any{0: path, 1: size, 2: start, 3: end, 4: make([]byte, 32)} +} + +// Spec §29.4, §69.1: the layers of the head and their codes, and the first +// failing layer decides. +func TestDecodeHeadLayers(t *testing.T) { + many := make([]any, 65536) + for i := range many { + many[i] = file(strings.Repeat("a", 1)+string(rune('a'+i%26))+strings.Repeat("x", i/26%3), 0, 0, 0) + } + for name, c := range map[string]struct { + b []byte + want error + }{ + // Layer 2. + "another type tag": {mustMarshal(t, map[uint64]any{0: "datekeys-control", 1: uint64(1), 2: make([]byte, 32)}), datekeys.ErrNonCanonicalCBOR}, + "version 2": {mustMarshal(t, map[uint64]any{0: "datekeys-head", 1: uint64(2), 2: make([]byte, 32)}), datekeys.ErrUnsupportedVersion}, + "version 2 and ..": {mustMarshal(t, map[uint64]any{0: "datekeys-head", 1: uint64(2), 2: make([]byte, 32), 5: []any{file("..", 0, 0, 0)}}), datekeys.ErrUnsupportedVersion}, + // Layer 3. + "no salt": {head(t, map[uint64]any{2: nil}), datekeys.ErrNonCanonicalCBOR}, + "a salt of 31 bytes": {head(t, map[uint64]any{2: make([]byte, 31)}), datekeys.ErrNonCanonicalCBOR}, + "an empty comment": {head(t, map[uint64]any{3: ""}), datekeys.ErrNonCanonicalCBOR}, + "a comment of 16385 bytes": {head(t, map[uint64]any{3: strings.Repeat("a", 16385)}), datekeys.ErrNonCanonicalCBOR}, + "an author of 257 bytes": {head(t, map[uint64]any{4: strings.Repeat("a", 257)}), datekeys.ErrNonCanonicalCBOR}, + "an empty array of files": {head(t, map[uint64]any{5: []any{}}), datekeys.ErrNonCanonicalCBOR}, + "65536 files": {head(t, map[uint64]any{5: many}), datekeys.ErrNonCanonicalCBOR}, + "R1: an empty path": {head(t, map[uint64]any{5: []any{file("", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR}, + "R1: a path of 1025 bytes": {head(t, map[uint64]any{5: []any{file(strings.Repeat("a", 1025), 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR}, + "R8: b before a": {head(t, map[uint64]any{5: []any{file("b", 0, 0, 0), file("a", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR}, + "R8: a repeated path": {head(t, map[uint64]any{5: []any{file("a", 0, 0, 0), file("a", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR}, + "R8 before R3: b/.. and a": {head(t, map[uint64]any{5: []any{file("b/..", 0, 0, 0), file("a", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR}, + "a size above L_MAX": {head(t, map[uint64]any{5: []any{file("a", capsule.MaxPayloadLength+1, 0, 0)}}), datekeys.ErrNonCanonicalCBOR}, + "an mtime after 9999": {head(t, map[uint64]any{5: []any{map[uint64]any{0: "a", 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: make([]byte, 32), 5: uint64(253402300800)}}}), datekeys.ErrNonCanonicalCBOR}, + "an unknown key 8": {head(t, map[uint64]any{8: uint64(1)}), datekeys.ErrNonCanonicalCBOR}, + "a byte more": {append(head(t, nil), 0), datekeys.ErrNonCanonicalCBOR}, + // Layer 4, in key order. + "a comment with U+202E": {head(t, map[uint64]any{3: "a‮b"}), datekeys.ErrHeadInvalid}, + "a comment with the tag U+E0041": {head(t, map[uint64]any{3: "a\U000E0041"}), datekeys.ErrHeadInvalid}, + "an author with LF": {head(t, map[uint64]any{4: "a\nb"}), datekeys.ErrHeadInvalid}, + "R3: ..": {head(t, map[uint64]any{5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "R2: /a": {head(t, map[uint64]any{5: []any{file("/a", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "R4b: a and VS16": {head(t, map[uint64]any{5: []any{file("a️", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "R6: CON.txt": {head(t, map[uint64]any{5: []any{file("CON.txt", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "R10: .datekeys-x": {head(t, map[uint64]any{5: []any{file(".datekeys-x", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "layout: a first start that is not 0": {head(t, map[uint64]any{5: []any{file("a", 1, 1, 2)}}), datekeys.ErrHeadInvalid}, + "layout: end minus start is not size": {head(t, map[uint64]any{5: []any{file("a", 2, 0, 1)}}), datekeys.ErrHeadInvalid}, + "layout: a gap": {head(t, map[uint64]any{5: []any{file("a", 1, 0, 1), file("b", 1, 2, 3)}}), datekeys.ErrHeadInvalid}, + "R7: A.txt and a.txt": {head(t, map[uint64]any{5: []any{file("A.txt", 0, 0, 0), file("a.txt", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "a comment and a path that break": {head(t, map[uint64]any{3: "a‮", 5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid}, + "a path that breaks, then an unknown critical extension": {head(t, map[uint64]any{5: []any{file("..", 0, 0, 0)}, 6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}), datekeys.ErrHeadInvalid}, + "an unknown critical extension": {head(t, map[uint64]any{6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}), datekeys.ErrExtensionCriticalUnknown}, + } { + if _, err := capsule.DecodeHead(c.b, nil); !errors.Is(err, c.want) { + t.Errorf("%s: %v, want %v", name, err, c.want) + } else if n := codes(err); n != 1 { + t.Errorf("%s: %d normative codes in %v", name, n, err) + } + } + // A known critical extension of the head passes. + b := head(t, map[uint64]any{6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}) + if _, err := capsule.DecodeHead(b, extension.Set{"x.example": {1}}); err != nil { + t.Fatal(err) + } +} + +// codes counts the normative errors err wraps, ERR_HEAD_INVALID included. +func codes(err error) int { + n := 0 + for _, e := range append(datekeys.All(), datekeys.ErrHeadInvalid) { + if errors.Is(err, e) { + n++ + } + } + return n +} diff --git a/capsule/framing.go b/capsule/framing.go index df58e96..c54695e 100644 --- a/capsule/framing.go +++ b/capsule/framing.go @@ -51,9 +51,17 @@ const ( // Format2 is the format of spec v0.9, the one Encrypt writes: exactly 16 // stanzas in INNER_ACCESS_AGE and a padded payload (spec §29.1, §39). Format2 Format = 2 + // Format3 is the format of spec v0.10: the padded plaintext of + // PAYLOAD_AGE is BODY, with the security area, the head and several + // files (spec §29.2 to §29.7). + Format3 Format = 3 ) -func (f Format) valid() bool { return f == Format1 || f == Format2 } +func (f Format) valid() bool { return f >= Format1 && f <= Format3 } + +// padded reports whether the payload of format f is padded, with L and the +// padding code in keys 6 and 7 of its control (spec §29.1, §31). +func (f Format) padded() bool { return f == Format2 || f == Format3 } // Policy is the declared access policy of PUBLIC_HEADER (spec §25). type Policy uint8 @@ -126,7 +134,9 @@ func ParsePrelude(b []byte) (Prelude, error) { if len(b) < PreludeSize { return Prelude{}, fmt.Errorf("capsule: truncated prelude: %w", datekeys.ErrIntegrity) } - if !Format(b[4]).valid() { + // Format 3 is read from step 3 of the plan of format 3 on, together with + // the test data that expect VERSION 3 to be rejected here. + if f := Format(b[4]); !f.valid() || f == Format3 { return Prelude{}, fmt.Errorf("capsule: framing version %d: %w", b[4], datekeys.ErrUnsupportedVersion) } if b[5] != 0 || b[6] != 0 || b[7] != 0 { @@ -378,7 +388,7 @@ type controlWire struct { func (w *controlWire) encode(e *codec.Encoder) { n := 4 + nonEmpty(w.Critical) + nonEmpty(w.Noncritical) - if w.Format == Format2 { + if w.Format.padded() { n += 2 } e.Map(n) @@ -391,7 +401,7 @@ func (w *controlWire) encode(e *codec.Encoder) { e.Uint(3) e.Bstr(w.PayloadIdentity) encodeExtensions(e, 4, w.Critical, w.Noncritical) - if w.Format == Format2 { + if w.Format.padded() { e.Uint(6) e.Bstr(w.PayloadLength) e.Uint(7) @@ -404,7 +414,7 @@ func (w *controlWire) encode(e *codec.Encoder) { // version 1. The caller wipes PayloadIdentity, whatever the result. func (w *controlWire) decode(d *codec.Decoder) error { maxPairs := 6 - if w.Format == Format2 { + if w.Format.padded() { maxPairs = 8 } pairs, err := d.Map(maxPairs) @@ -417,7 +427,7 @@ func (w *controlWire) decode(d *codec.Decoder) error { if err != nil { return err } - if (k == 6 || k == 7) && w.Format != Format2 { + if (k == 6 || k == 7) && !w.Format.padded() { return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR) } switch k { @@ -456,7 +466,7 @@ func (w *controlWire) decode(d *codec.Decoder) error { if err := required(seen, 4); err != nil { return err } - if w.Format == Format2 { + if w.Format.padded() { for _, k := range []uint{6, 7} { if seen&(1<