package main import ( "bytes" "crypto" "crypto/elliptic" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "time" "g.activething.com/go/DateKeys/authorkey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/datekey" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/internal/cms/cmstest" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/locator" "g.activething.com/go/DateKeys/profile" ) // frozenVectors writes testdata/vectors/security_cms.json and locator.json // when they are missing: their bytes hold the randomness of certificates, of // age and of tlock, so they are made once and kept, as the fixtures are. // Delete a file to make it again. func frozenVectors(dir string) error { for name, gen := range map[string]func() (any, error){ "security_cms.json": securityCMSVectors, "locator.json": locatorVectors, } { path := filepath.Join(dir, name) if _, err := os.Stat(path); err == nil { continue } v, err := gen() if err != nil { return fmt.Errorf("%s: %w", name, err) } if err := testkit.WriteJSON(path, v); err != nil { return err } } return nil } var ( vecRound = time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC) vecSigned = time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC) ) func hex32(b [32]byte) string { return hex.EncodeToString(b[:]) } // cmsArea builds SECURITY_CBOR with an alg 2 signature by the signers over // the context c, sealed by tsa at when; required lists who must sign. func cmsArea(c *capsule.SecurityContext, required, signers []cmstest.Signer, tsa cmstest.Signer, when time.Time, seal []byte) ([]byte, error) { var hashes [][32]byte for _, s := range required { hashes = append(hashes, sum256(s.Cert.Raw)) } list, err := capsule.EncodeSigners(hashes) if err != nil { return nil, err } msg := capsule.AuthorMessage(c.ControlCommit, c.HeadDigest, capsule.SignersDigest(capsule.AlgCMS, list)) opts := cmstest.Options{} if tsa.Key != nil { opts.Token = func(sig []byte) []byte { return cmstest.Token(sig, when, cmstest.TokenOptions{Accuracy: time.Second}, tsa) } } content, err := capsule.EncodeAuthorSignature(capsule.AlgCMS, list, cmstest.Signature(msg, opts, signers...)) if err != nil { return nil, err } return capsule.EncodeSecurityWith(content, seal) } func securityCMSVectors() (any, error) { ana := cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo) luis := cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo) otro := cmstest.NewECDSA("Otro", elliptic.P384(), certFrom, certTo) tsa := cmstest.NewECDSA("Autoridad de Sellado de prueba", elliptic.P256(), certFrom, certTo) ctx := func() *capsule.SecurityContext { return &capsule.SecurityContext{ControlCommit: sha256.Sum256([]byte("control")), HeadDigest: sha256.Sum256([]byte("head")), RoundTime: vecRound} } key, err := authorkey.NewFromSeed(bytes.Repeat([]byte{7}, 32)) if err != nil { return nil, err } f := testkit.CMSVectorFile{ Spec: testkit.SpecVersion, Description: "SECURITY_CBOR with an author signature of alg 2 or a time seal of seal_type 2, its context and the verdicts of spec v0.11 29.7, 29.10 and 29.11. " + "Certificates and tokens are made once with test keys and the file is frozen. See testdata/README.md.", } var errs []error add := func(name string, area []byte, c *capsule.SecurityContext, wantSig, wantSeal capsule.Verdict) { var v capsule.Verdicts vc := testkit.CMSVectorCase{Name: name, SecurityCBOR: hex.EncodeToString(area)} if c == nil { v, vc.NoContext = capsule.EvaluateSecurity(area), true c = ctx() } else { v = capsule.EvaluateSecurityIn(area, c) } vc.Context = testkit.CMSVectorContext{ControlCommit: hex32(c.ControlCommit), HeadDigest: hex32(c.HeadDigest), RoundTime: c.RoundTime.UTC().Format(time.RFC3339)} vc.Signature, vc.Seal = string(v.Signature), string(v.Seal) if v.Signature != wantSig || v.Seal != wantSeal { errs = append(errs, fmt.Errorf("%s: verdicts %s and %s, want %s and %s", name, v.Signature, v.Seal, wantSig, wantSeal)) } if d := v.Detail; d != nil { vc.Signers, vc.Foreign = signerResults(d.Signers), signerResults(d.Foreign) if !d.SealTime.IsZero() { vc.SealHolder, vc.SealTime = d.SealHolder, d.SealTime.UTC().Format(time.RFC3339) } } f.Cases = append(f.Cases, vc) } must := func(b []byte, err error) []byte { if err != nil { errs = append(errs, err) } return b } both := []cmstest.Signer{ana, luis} c := ctx() add("alg 2: two signers, each sealed before the round time", must(cmsArea(c, both, both, tsa, vecSigned, nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal) add("alg 2: a seal after the round time proves nothing before it", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecRound.Add(time.Hour), nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal) add("alg 2: a signer who is not required shows apart", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana, otro}, tsa, vecSigned, nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal) add("alg 2: a required signer is absent", must(cmsArea(c, both, []cmstest.Signer{ana}, tsa, vecSigned, nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal) add("alg 2: no seal", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, cmstest.Signer{}, vecSigned, nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal) add("alg 2: a seal from before the certificate was valid", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, certFrom.AddDate(-1, 0, 0), nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal) add("alg 2: a key 3 beside it", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, must(capsule.EncodeSeal(1, []byte{1})))), c, capsule.VerdictSignedIncomplete, capsule.VerdictSealUnsupported) other := ctx() other.HeadDigest[5] ^= 9 add("alg 2: another head", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, nil)), other, capsule.VerdictSignatureInvalid, capsule.VerdictNoSeal) add("alg 2: without the context of a capsule", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, nil)), nil, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal) one, two := sum256([]byte("a")), sum256([]byte("b")) if bytes.Compare(one[:], two[:]) > 0 { one, two = two, one } unsorted := append(append([]byte{0x82, 0x58, 0x20}, two[:]...), append([]byte{0x58, 0x20}, one[:]...)...) for name, signers := range map[string][]byte{"alg 2: SIGNERS out of order": unsorted, "alg 2: an empty SIGNERS": {0x80}} { content, _ := capsule.EncodeAuthorSignature(capsule.AlgCMS, signers, []byte{0x30, 0x00}) add(name, must(capsule.EncodeSecurityWith(content, nil)), c, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal) } { list, _ := capsule.EncodeSigners([][32]byte{sum256(ana.Cert.Raw)}) content, _ := capsule.EncodeAuthorSignature(capsule.AlgCMS, list, []byte("not DER")) add("alg 2: not a CMS", must(capsule.EncodeSecurityWith(content, nil)), c, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal) } // Seals of seal_type 2 over an alg 1 signature. msg := capsule.AuthorMessage(c.ControlCommit, c.HeadDigest, capsule.SignersDigest(capsule.AlgEd25519, nil)) sig := must(capsule.EncodeAuthorSignature(capsule.AlgEd25519, key.Public(), key.Sign(msg))) subject := capsule.SealSubject(c.ControlCommit, c.HeadDigest, capsule.SigPart(sig)) sealed := func(token []byte) []byte { return must(capsule.EncodeSecurityWith(sig, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, token)))) } tok := func(subject []byte, when time.Time, o cmstest.TokenOptions, s cmstest.Signer) []byte { return cmstest.Token(subject, when, o, s) } okSig := capsule.VerdictSignedOther add("seal: before the round time", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealed) add("seal: after the round time", sealed(tok(subject[:], vecRound.Add(time.Minute), cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealedLate) add("seal: the accuracy reaches the round time", sealed(tok(subject[:], vecRound.Add(-time.Second), cmstest.TokenOptions{Accuracy: 2 * time.Second}, tsa)), c, okSig, capsule.VerdictSealedLate) add("seal: over another subject", sealed(tok([]byte("other"), vecSigned, cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealInvalid) add("seal: the authority had expired at its time", sealed(tok(subject[:], certTo.AddDate(1, 0, 0), cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealInvalid) add("seal: a TSTInfo of version 2", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{Version: 2}, tsa)), c, okSig, capsule.VerdictSealUnreadable) add("seal: not DER", sealed([]byte("not DER")), c, okSig, capsule.VerdictSealUnreadable) add("seal: SHA-384 in the imprint", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{Hash: crypto.SHA384}, tsa)), c, okSig, capsule.VerdictSealUnsupported) add("seal: without a context, as a reader of v0.10", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{}, tsa)), nil, capsule.VerdictSignatureUnchecked, capsule.VerdictSealUnsupported) noSig := capsule.SealSubject(c.ControlCommit, c.HeadDigest, capsule.SigPart(nil)) add("seal: over a capsule without a signature", must(capsule.EncodeSecurityWith(nil, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, tok(noSig[:], vecSigned, cmstest.TokenOptions{}, tsa))))), c, capsule.VerdictNoSignature, capsule.VerdictSealed) if err := errors.Join(errs...); err != nil { return nil, err } return f, nil } // locatorVectors makes the vector of the extension datekeys.capsule: a .dkc of // patterned bytes in an envelope, hidden in a host, its locator sealed with // tlock for round 1000, and the data of the extension with a note. func locatorVectors() (any, error) { p := profile.Quicknet() dkc := patterned("locator dkc", 5000) loc, rest, err := locator.NewEnvelope(dkc) if err != nil { return nil, err } host := patterned("host file", 3000) file, offset := locator.Hide(host, rest) const cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" loc.Addresses = []locator.Address{{URI: "https://ejemplo.org/foto.jpg", Offset: offset}, {URI: "ipfs://" + cid}} plain, err := loc.Marshal() if err != nil { return nil, err } const round = 1000 sealed, err := locator.Seal(p, round, loc) if err != nil { return nil, err } dk, err := datekey.Resolve(p, mustRoundTime(p, round)) if err != nil { return nil, err } const note = "Cartas del viaje a Lisboa" x, err := (&locator.Info{Note: note, DateKey: dk, Sealed: sealed}).Extension() if err != nil { return nil, err } if x.ID != extension.CapsuleID { return nil, errors.New("not datekeys.capsule") } v := testkit.LocatorVectorFile{ Spec: testkit.SpecVersion, Description: "The extension datekeys.capsule of a .dkk and what it points to (spec v0.11, 44.1): an envelope of age with its header apart from its rest, " + "the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. Frozen. See testdata/README.md.", Round: round, DateKey: dk.Compact(), Note: note, DKC: hex.EncodeToString(dkc), Rest: hex.EncodeToString(rest), Header: hex.EncodeToString(loc.EnvelopeHeader), Host: hex.EncodeToString(file), HostOffset: offset, Plaintext: hex.EncodeToString(plain), Sealed: hex.EncodeToString(sealed), Extension: hex.EncodeToString(x.Data), EnvelopeKey: hex.EncodeToString(loc.EnvelopeKey[:]), RestDigest: hex32(loc.RestDigest), RestSize: loc.RestSize, CapsuleDigest: hex32(loc.CapsuleDigest), } for _, a := range loc.Addresses { v.Addresses = append(v.Addresses, testkit.LocatorVectorAddress{URI: a.URI, Offset: a.Offset, Host: a.Host()}) } for _, base := range []int{100, 3000, 4000, 4060, 4066, 4067, 4068, 4069, 4070, 4071, 4072, 4090, 4094, 4095, 4096, 4097, 4100, 8160, 8190, 8192, 8193, 12000} { v.PaddingCases = append(v.PaddingCases, testkit.LocatorPaddingCase{Base: base, Total: locator.PlaintextLength(base)}) } for _, c := range []struct { uri string ok bool }{ {"https://ejemplo.org/a.bin", true}, {"https://ejemplo.org:8443/x?y=1", true}, {"ipfs://" + cid, true}, {"ipfs://" + cid + "/ruta", true}, {"", false}, {"http://ejemplo.org/a", false}, {"file:///etc/passwd", false}, {"ftp://x/y", false}, {"https://user:pass@ejemplo.org/", false}, {"https://", false}, {"ipfs://notacid", false}, {"https://ejemplo.org/ñ", false}, {"https://ejemplo.org/a b", false}, {"https://%D0%B0pple.com/x", false}, {"https://ejemplo.org%E2%80%AEtxt.exe/", false}, {"https://127.0.0.1/", false}, {"https://[::1]/", false}, {"https://0x7f000001/", false}, {"https://a.com:99999999/", false}, {"https://a.com:0/", false}, {"https://xn--pple-43d.com/", true}, } { if (locator.CheckURI(c.uri) == nil) != c.ok { return nil, fmt.Errorf("the address %q: accepted %v, want %v", c.uri, !c.ok, c.ok) } v.URICases = append(v.URICases, testkit.LocatorURICase{URI: c.uri, OK: c.ok}) } return v, nil } func mustRoundTime(p *profile.Profile, round uint64) time.Time { t, err := datekey.RoundTime(p, round) if err != nil { panic(err) } return t }