package capsule_test import ( "bytes" "context" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "testing" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/datekey" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" ) func seedFixtures(f *testing.F, part func(testkit.Parts) []byte) { for _, name := range fixtureNames { b, err := os.ReadFile(filepath.Join(fixtureDir, name+".dkc")) if err != nil { f.Fatal(err) } p, err := testkit.Split(b) if err != nil { f.Fatal(err) } f.Add(part(p)) } } // whole keeps the first 512 bytes of the payload: the pre-unlock checks read // only its age header, and small inputs keep the fuzzer fast. func whole(p testkit.Parts) []byte { payload := p.Payload if len(payload) > 512 { payload = payload[:512] } return testkit.Join(p.Prelude, p.Header, p.Sealed, payload) } func FuzzParsePrelude(f *testing.F) { seedFixtures(f, func(p testkit.Parts) []byte { return p.Prelude }) f.Fuzz(func(t *testing.T, b []byte) { p, err := capsule.ParsePrelude(b) if err != nil { if datekeys.Code(err) == "" { t.Fatalf("error without a normative code: %v", err) } return } got := p.Bytes() if !bytes.Equal(got[:], b[:capsule.PreludeSize]) { t.Fatal("accepted a prelude that does not re-encode to its input") } }) } func FuzzDecodeHeader(f *testing.F) { seedFixtures(f, func(p testkit.Parts) []byte { return p.Header }) // access_policy in a multi-byte head whose low byte is a V1 policy: a // narrowing before the check accepted them (spec §25). dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}.Compact() for _, p := range []uint64{256, 257, 1 << 32} { h, err := testkit.RawHeader([capsule.CapsuleIDSize]byte{1}, dk, p) if err != nil { f.Fatal(err) } f.Add(h) } f.Fuzz(func(t *testing.T, b []byte) { h, err := capsule.DecodeHeader(b) if err != nil { if datekeys.Code(err) == "" { t.Fatalf("error without a normative code: %v", err) } return } re, err := capsule.EncodeHeader(h) if err != nil || !bytes.Equal(re, b) { t.Fatal("accepted a PUBLIC_HEADER that does not re-encode to its input") } }) } func FuzzDecodeControl(f *testing.F) { for _, name := range fixtureNames { fx := loadFixture(f, name) b, _ := hexDecode(fx.ControlCBOR) f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { // The same bytes as the control of a capsule of each format: at most // one of the three schema versions accepts them. accepted := 0 for _, format := range []capsule.Format{capsule.Format1, capsule.Format2, capsule.Format3} { c, err := capsule.DecodeControl(b, format) if err != nil { if datekeys.Code(err) == "" { t.Fatalf("format %d: error without a normative code: %v", format, err) } continue } accepted++ re, err := capsule.EncodeControl(c, format) if err != nil || !bytes.Equal(re, b) { t.Fatalf("format %d: accepted a CONTROL_CBOR that does not re-encode to its input", format) } } if accepted > 1 { t.Fatal("two formats accept the same CONTROL_CBOR") } }) } // FuzzDecodeHead: a head that DecodeHead accepts re-encodes to its input, // and each rejection carries exactly one normative code (spec §29.4, §69.1). func FuzzDecodeHead(f *testing.F) { for _, name := range fixtureNames { if fx := loadFixture(f, name); fx.Head != "" { b, _ := hexDecode(fx.Head) f.Add(b) } } var heads testkit.HeadSchemaFile if err := testkit.ReadJSON("../testdata/vectors/head_schema.json", &heads); err != nil { f.Fatal(err) } for _, v := range heads.Heads { b, _ := hexDecode(v.Hex) f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { h, err := capsule.DecodeHead(b, nil) if err != nil { if n := codes(err); n != 1 { t.Fatalf("%d normative codes: %v", n, err) } return } if re, err := capsule.EncodeHead(h); err != nil || !bytes.Equal(re, b) { t.Fatalf("accepted a head that does not re-encode to its input: %v", err) } }) } // FuzzEvaluateSecurity: security never fails, and gives verdicts of this // version, X for both the signature and the seal or for neither (spec §29.3, // §29.7). func FuzzEvaluateSecurity(f *testing.F) { for _, name := range fixtureNames { if fx := loadFixture(f, name); fx.Security != "" { b, _ := hexDecode(fx.Security) f.Add(b) } } var security testkit.SecurityVectorFile if err := testkit.ReadJSON("../testdata/vectors/security.json", &security); err != nil { f.Fatal(err) } for _, v := range security.Vectors { b, _ := hexDecode(v.Hex) f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { v := capsule.EvaluateSecurity(b) switch { case v.Signature != capsule.VerdictUnreadable && v.Signature != capsule.VerdictNoSignature && v.Signature != capsule.VerdictSignatureUnchecked, v.Seal != capsule.VerdictUnreadable && v.Seal != capsule.VerdictNoSeal && v.Seal != capsule.VerdictSealUnsupported && v.Seal != capsule.VerdictSealUnreadable, (v.Signature == capsule.VerdictUnreadable) != (v.Seal == capsule.VerdictUnreadable), len(v.Lines()) == 0: t.Fatalf("verdicts %+v", v) } }) } // FuzzInspect feeds whole capsules to the pre-unlock validation, and to Open // with a source that never has the release: a capsule that Inspect rejects // must not cause a request, and nothing may pass the release step. The // cryptographic steps after it are exercised by the mutation corpus; keeping // them out of this target keeps it fast. func FuzzInspect(f *testing.F) { seedFixtures(f, whole) reg := testkit.Registry() far := testkit.Fixed(testkit.Genesis().AddDate(5, 0, 0)) f.Fuzz(func(t *testing.T, b []byte) { _, err := capsule.Inspect(bytes.NewReader(b), capsule.InspectOptions{Registry: reg}) if err != nil && datekeys.Code(err) == "" { t.Fatalf("error without a normative code: %v", err) } src := testkit.NewSource() o := capsule.OpenOptions{Registry: reg, Source: src, Now: far, Sink: testkit.DiscardSink{}} _, openErr := capsule.Open(context.Background(), io.Discard, bytes.NewReader(b), o) switch { case openErr == nil: t.Fatal("opened without a release") case datekeys.Code(openErr) == "": t.Fatalf("error without a normative code: %v", openErr) case err != nil && src.Calls != 0: t.Fatal("a capsule rejected by Inspect caused a release request") case err == nil && !errors.Is(openErr, datekeys.ErrReleaseUnavailable) && !errors.Is(openErr, datekeys.ErrAccessRequired): t.Fatalf("a capsule accepted by Inspect failed before the release step: %v", openErr) } }) } // FuzzEncodeImpliesDecode: whatever EncodeHeader, EncodeControl and // accesskey.Encode accept, the matching decoder accepts and re-encodes to the // same bytes. An encoder that writes what its reader rejects makes capsules // that cannot be opened (spec §72, §76 case 5). func FuzzEncodeImpliesDecode(f *testing.F) { f.Add("org.example.label", uint64(1), []byte("public label"), "org.example.note", uint64(2), []byte{0xa2, 0x00, 0x07}, uint8(0), uint8(0)) f.Add("a", uint64(1)<<32, []byte{}, "a", uint64(0), []byte{0x81, 0x81, 0x00}, uint8(0b1011), uint8(63)) f.Add("org.\xff", uint64(0), []byte{0xf6}, "z", uint64(1)<<53, []byte(nil), uint8(0b0100), uint8(64)) dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000} f.Fuzz(func(t *testing.T, id1 string, v1 uint64, d1 []byte, id2 string, v2 uint64, d2 []byte, mode, filler uint8) { // mode bit 0: first extension critical; bit 1: second critical; // bit 2: first without data; bit 3: second without data. filler // adds extensions to the noncritical array, up to past the limit. e1 := extension.Extension{ID: id1, Version: v1, Data: d1} e2 := extension.Extension{ID: id2, Version: v2, Data: d2} if mode&4 != 0 { e1.Data = nil } if mode&8 != 0 { e2.Data = nil } var crit, non []extension.Extension for i, e := range []extension.Extension{e1, e2} { if mode&(1<> 4 & 1), Critical: crit, Noncritical: non} if b, err := capsule.EncodeHeader(h); err == nil { back, err := capsule.DecodeHeader(b) if err != nil { t.Fatalf("EncodeHeader wrote a PUBLIC_HEADER that DecodeHeader rejects: %v", err) } if re, err := capsule.EncodeHeader(back); err != nil || !bytes.Equal(re, b) { t.Fatal("PUBLIC_HEADER does not re-encode to itself") } } // mode bits 5 and 6: the padding code of the format 2 control, from 0 // to 3; v2 is its payload_length, possibly above L_MAX. for _, format := range []capsule.Format{capsule.Format1, capsule.Format2} { c := &capsule.Control{Critical: crit, Noncritical: non} if format == capsule.Format2 { c.PayloadLength, c.Padding = v2, capsule.Padding(mode>>5&3) } if b, err := capsule.EncodeControl(c, format); err == nil { back, err := capsule.DecodeControl(b, format) if err != nil { t.Fatalf("EncodeControl wrote a format %d CONTROL_CBOR that DecodeControl rejects: %v", format, err) } if re, err := capsule.EncodeControl(back, format); err != nil || !bytes.Equal(re, b) { t.Fatalf("format %d CONTROL_CBOR does not re-encode to itself", format) } } } k := &accesskey.AccessKey{Type: accesskey.TypeX25519, Material: make([]byte, 32), Critical: crit, Noncritical: non} var dkk bytes.Buffer if err := accesskey.Encode(&dkk, k); err == nil { back, err := accesskey.Decode(bytes.NewReader(dkk.Bytes())) if err != nil { t.Fatalf("accesskey.Encode wrote a .dkk that Decode rejects: %v", err) } var re bytes.Buffer if err := accesskey.Encode(&re, back); err != nil || !bytes.Equal(re.Bytes(), dkk.Bytes()) { t.Fatal(".dkk does not re-encode to itself") } } }) } func hexDecode(s string) ([]byte, error) { return hex.DecodeString(s) }