package cms_test import ( "crypto" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "path/filepath" "testing" "time" "g.activething.com/go/DateKeys/internal/cms" "g.activething.com/go/DateKeys/internal/cms/cmstest" "g.activething.com/go/DateKeys/internal/der" ) // The fuzz targets of the reader. Each one is seeded with the signatures and // the tokens of testdata/vectors/security_cms.json and with what cmstest // builds, and checks that the reader never panics and fails only with the // errors of its verdicts: ErrForm (F1, S2) or ErrAlgorithm (S1). // FuzzParseSignature reads any bytes as the CMS signature of alg 2 (spec // §29.10). What it accepts has a certificate for each SignerInfo, and its // checks, and the token of each, run without a panic. func FuzzParseSignature(f *testing.F) { for _, b := range fuzzSeeds(f) { f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { sd, err := cms.ParseSignature(b) if err != nil { if sd != nil || !errors.Is(err, cms.ErrForm) && !errors.Is(err, cms.ErrAlgorithm) { t.Fatalf("%v, with a result %v", err, sd != nil) } return } if der.Check(b) != nil || len(sd.Signers) == 0 { t.Fatal("a signature that is not DER, or without a SignerInfo") } for _, s := range sd.Signers { if s.Cert == nil || s.Cert.Hash != sha256.Sum256(s.Cert.Raw) { t.Fatal("a SignerInfo without its certificate") } switch s.Check(msg) { case cms.Valid, cms.Invalid, cms.NotVerifiable: default: t.Fatal("a result outside the three") } s.Cert.Holder() s.Cert.IssuerName() if s.Token != nil { checkToken(t, s.Token, s.Signature) } } }) } // FuzzParseToken reads any bytes as an RFC 3161 token (spec §29.11). func FuzzParseToken(f *testing.F) { for _, b := range fuzzSeeds(f) { f.Add(b) } f.Fuzz(func(t *testing.T, b []byte) { checkToken(t, b, []byte("seal subject")) }) } // maxAccuracy is the largest precision of a token: 2^31 - 1 seconds, 999 // milliseconds and 999 microseconds. const maxAccuracy = (1<<31-1)*time.Second + 999*time.Millisecond + 999*time.Microsecond func checkToken(t *testing.T, b, subject []byte) { t.Helper() tok, err := cms.ParseToken(b) if err != nil { if tok != nil || !errors.Is(err, cms.ErrForm) && !errors.Is(err, cms.ErrAlgorithm) { t.Fatalf("%v, with a result %v", err, tok != nil) } return } if tok.TSA == nil || tok.GenTime.IsZero() || tok.Accuracy < 0 || tok.Accuracy > maxAccuracy { t.Fatalf("a token of %+v", tok) } tok.Check(subject) tok.ImprintIsSHA256() tok.TSA.Holder() } // FuzzParseCert reads any bytes as a certificate with the profile of spec // §29.10. What it accepts names its holder and its issuer without a panic, // and has a valid period that it contains. func FuzzParseCert(f *testing.F) { for _, b := range fuzzSeeds(f) { if sd, err := cms.ParseSignature(b); err == nil { for _, c := range sd.Certs { f.Add(c.Raw) } } if tok, err := cms.ParseToken(b); err == nil { f.Add(tok.TSA.Raw) } } for _, spec := range []cmstest.CertSpec{ {CN: "Ana López"}, {Subject: cmstest.Name(cn(cmstest.BMPText("Ana")), given(cmstest.Printable("Ana")), surname(cmstest.Teletex("Lopez")), org(cmstest.IA5("Banco")))}, {NoVersion: true}, {NotAfter: cmstest.GeneralizedTime("20501231235959Z"), UniqueIDs: [][]byte{cmstest.TLV(0x81, []byte{0, 1})}}, {Extensions: [][]byte{cmstest.ExtSKI([]byte{1}), cmstest.ExtSKI([]byte{1})}}, } { f.Add(cert(spec)) } f.Fuzz(func(t *testing.T, b []byte) { c, err := cms.ParseCert(b) if err != nil { return } if c.Hash != sha256.Sum256(b) || c.NotBefore.IsZero() || c.NotAfter.IsZero() { t.Fatalf("a certificate of %+v", c) } if !c.NotAfter.Before(c.NotBefore) && (!c.ValidAt(c.NotBefore) || !c.ValidAt(c.NotAfter)) { t.Fatal("a period that does not contain its ends") } c.Holder() c.IssuerName() }) } // fuzzSeeds returns the signatures and the tokens of security_cms.json, and // some that cmstest builds. func fuzzSeeds(f *testing.F) [][]byte { raw, err := os.ReadFile(filepath.Join("..", "..", "testdata", "vectors", "security_cms.json")) if err != nil { f.Fatal(err) } var file struct { Cases []struct { Area string `json:"security_cbor"` } `json:"cases"` } if err := json.Unmarshal(raw, &file); err != nil { f.Fatal(err) } var out [][]byte for _, c := range file.Cases { area, err := hex.DecodeString(c.Area) if err != nil { f.Fatal(err) } out = append(out, contentInfos(area)...) } tok := func(sig []byte) []byte { return cmstest.Token(sig, now, cmstest.TokenOptions{Accuracy: 1500 * time.Millisecond, After: [][]byte{cmstest.Bool(true), cmstest.Int(7)}}, tsa) } return append(out, cmstest.Signature(msg, cmstest.Options{Token: tok, OCSP: cmstest.Seq(cmstest.Int(0))}, ana, luis), cmstest.Signature(msg, cmstest.Options{PSS: true, SKI: true, Hash: crypto.SHA384}, luis), cmstest.Signature(msg, cmstest.Options{SigCertV1: true, ESSHashAlg: cmstest.HashAlg(crypto.SHA512), ExtraAttrs: [][]byte{cmstest.BigArcAttr()}}, ana), cmstest.Token([]byte("seal subject"), now, cmstest.TokenOptions{SigCertV2: true, TSATwice: true, CRL: cmstest.Seq(cmstest.Int(1))}, tsa), ) } // contentInfos returns the outermost runs of bytes of b that are one DER // SEQUENCE of more than 127 bytes: the signatures and the tokens of an area. func contentInfos(b []byte) [][]byte { var out [][]byte for i := 0; i+4 < len(b); i++ { if b[i] != 0x30 || b[i+1] < 0x81 || b[i+1] > 0x83 { continue } n := int(b[i+1] & 0x7f) if i+2+n > len(b) { continue } l := 0 for _, c := range b[i+2 : i+2+n] { l = l<<8 | int(c) } end := i + 2 + n + l if end > len(b) || der.Check(b[i:end]) != nil { continue } out = append(out, b[i:end]) i = end - 1 } return out }