package cms_test import ( "bytes" "crypto/elliptic" "crypto/sha256" "encoding/asn1" "errors" "testing" "time" "g.activething.com/go/DateKeys/internal/cms" "g.activething.com/go/DateKeys/internal/cms/cmstest" "g.activething.com/go/DateKeys/internal/der" ) var oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} // tstInfo builds a TSTInfo of RFC 3161 over subject, with the fields after // genTime that the test gives. func tstInfo(subject []byte, genTime []byte, after ...[]byte) []byte { return cmstest.Seq(append(tstFields(subject, genTime), after...)...) } // tstFields are the five required fields of a TSTInfo. func tstFields(subject []byte, genTime []byte) [][]byte { h := sha256.Sum256(subject) return [][]byte{ cmstest.Int(1), cmstest.OID(asn1.ObjectIdentifier{1, 2, 3, 4}), cmstest.Seq(cmstest.Seq(cmstest.OID(oidSHA256)), cmstest.Octets(h[:])), cmstest.Int(42), genTime, } } func parses(t *testing.T, tsa cmstest.Signer, info []byte, subject []byte) (*cms.Token, error) { t.Helper() tok, err := cms.ParseToken(cmstest.TokenRaw(info, tsa)) if err == nil && !tok.Check(subject) { t.Fatal("a token that parses does not verify") } return tok, err } // Review of the CMS reader: the TSTInfo is an OCTET STRING, so it is checked // field by field, and what DER forbids, or what would make a seal after the // opening date look before it, is a form error (spec ยง29.11, S2). func TestTSTInfoStrict(t *testing.T) { tsa := cmstest.NewECDSA("TSA", elliptic.P256(), from, to) subject := []byte("seal subject") good := cmstest.GeneralizedTime("20260930120000Z") if tok, err := parses(t, tsa, tstInfo(subject, good), subject); err != nil || !tok.GenTime.Equal(now) || tok.Accuracy != 0 { t.Fatalf("the baseline: %v", err) } if tok, err := parses(t, tsa, tstInfo(subject, cmstest.GeneralizedTime("20260930120000.5Z"), cmstest.Seq(cmstest.Int(2), cmstest.TLV(0x80, []byte{5}))), subject); err != nil || tok.GenTime.Sub(now) != 500*time.Millisecond || tok.Accuracy != 2*time.Second+5*time.Millisecond { t.Fatalf("a fraction and an accuracy: %v", err) } // The optional fields, in their order, each once. tsaName := cmstest.TLV(0xa0, cmstest.TLV(0xa4, cmstest.Seq())) exts := cmstest.TLV(0xa1, cmstest.Seq(cmstest.OID(asn1.ObjectIdentifier{1, 2, 3}), cmstest.Octets(nil))) ms := func(c ...byte) []byte { return cmstest.TLV(0x80, c) } us := func(c ...byte) []byte { return cmstest.TLV(0x81, c) } for name, tc := range map[string]struct { after [][]byte accuracy time.Duration }{ "an empty accuracy": {[][]byte{cmstest.Seq()}, 0}, "only millis": {[][]byte{cmstest.Seq(ms(5))}, 5 * time.Millisecond}, "only micros": {[][]byte{cmstest.Seq(us(7))}, 7 * time.Microsecond}, "millis and micros of 999": {[][]byte{cmstest.Seq(ms(0x03, 0xe7), us(0x03, 0xe7))}, 999*time.Millisecond + 999*time.Microsecond}, "millis of 128": {[][]byte{cmstest.Seq(ms(0, 0x80))}, 128 * time.Millisecond}, "millis of 300": {[][]byte{cmstest.Seq(ms(0x01, 0x2c))}, 300 * time.Millisecond}, "seconds, millis and micros": {[][]byte{cmstest.Seq(cmstest.Int(1), ms(5), us(7))}, time.Second + 5*time.Millisecond + 7*time.Microsecond}, "2^31 - 1 seconds": {[][]byte{cmstest.Seq(cmstest.Int(1<<31 - 1))}, (1<<31 - 1) * time.Second}, "ordering TRUE": {[][]byte{cmstest.Bool(true)}, 0}, "an accuracy and ordering TRUE": {[][]byte{cmstest.Seq(cmstest.Int(1)), cmstest.Bool(true)}, time.Second}, "a nonce": {[][]byte{cmstest.Int(99)}, 0}, "ordering TRUE and a nonce": {[][]byte{cmstest.Bool(true), cmstest.Int(99)}, 0}, "a tsa": {[][]byte{tsaName}, 0}, "extensions": {[][]byte{exts}, 0}, "every field": {[][]byte{cmstest.Seq(cmstest.Int(3)), cmstest.Bool(true), cmstest.Int(99), tsaName, exts}, 3 * time.Second}, } { tok, err := parses(t, tsa, tstInfo(subject, good, tc.after...), subject) if err != nil || tok.Accuracy != tc.accuracy { t.Errorf("%s: %v", name, err) } } fields := tstFields(subject, good) with := func(i int, v []byte) []byte { f := append([][]byte(nil), fields...) f[i] = v return cmstest.Seq(f...) } h := sha256.Sum256(subject) big := []byte{1, 0, 0, 0, 0, 0, 0, 0, 5} // 2^64 + 5, which wraps around to 5 in 64 bits for name, info := range map[string][]byte{ "a negative accuracy": tstInfo(subject, good, cmstest.Seq(cmstest.Int(-31536000))), "an accuracy of -1": tstInfo(subject, good, cmstest.Seq(cmstest.Int(-1))), "an accuracy that overflows": tstInfo(subject, good, cmstest.Seq(cmstest.Int(9223372037))), "an accuracy of 2^31 seconds": tstInfo(subject, good, cmstest.Seq(cmstest.Int(1<<31))), "an accuracy of 2^64 + 5 seconds": tstInfo(subject, good, cmstest.Seq(cmstest.IntBytes(big))), "millis of 0": tstInfo(subject, good, cmstest.Seq(ms(0))), "millis of 1000": tstInfo(subject, good, cmstest.Seq(ms(0x03, 0xe8))), "millis of 5000": tstInfo(subject, good, cmstest.Seq(ms(0x13, 0x88))), "millis of -1": tstInfo(subject, good, cmstest.Seq(ms(0xff))), "millis of 5 in two bytes": tstInfo(subject, good, cmstest.Seq(ms(0, 5))), "millis of 2^64 + 5": tstInfo(subject, good, cmstest.Seq(ms(big...))), "empty millis": tstInfo(subject, good, cmstest.Seq(ms())), "millis constructed": tstInfo(subject, good, cmstest.Seq(cmstest.TLV(0xa0, cmstest.Int(5)))), "micros of 0": tstInfo(subject, good, cmstest.Seq(us(0))), "micros of 1000": tstInfo(subject, good, cmstest.Seq(us(0x03, 0xe8))), "micros before millis": tstInfo(subject, good, cmstest.Seq(us(1), ms(1))), "a field after the micros": tstInfo(subject, good, cmstest.Seq(us(1), cmstest.TLV(0x82, []byte{1}))), "millis as an INTEGER": tstInfo(subject, good, cmstest.Seq(cmstest.Int(1), cmstest.Int(5))), "genTime with an offset": tstInfo(subject, cmstest.GeneralizedTime("20260930130000+0100")), "genTime with a trailing zero": tstInfo(subject, cmstest.GeneralizedTime("20260930120000.50Z")), "genTime without seconds": tstInfo(subject, cmstest.GeneralizedTime("202609301200Z")), "genTime as a UTCTime": tstInfo(subject, cmstest.UTCTime("260930120000Z")), "ordering FALSE written": tstInfo(subject, good, cmstest.Bool(false)), "ordering of two bytes": tstInfo(subject, good, cmstest.TLV(0x01, []byte{0xff, 0xff})), "two accuracies": tstInfo(subject, good, cmstest.Seq(cmstest.Int(1)), cmstest.Seq(cmstest.Int(1))), "two orderings": tstInfo(subject, good, cmstest.Bool(true), cmstest.Bool(true)), "an extra INTEGER at the end": tstInfo(subject, good, cmstest.Int(7), cmstest.Int(8), cmstest.Int(9)), "two tsa fields": tstInfo(subject, good, tsaName, tsaName), "two extensions": tstInfo(subject, good, exts, exts), "a field after the extensions": tstInfo(subject, good, exts, cmstest.Int(1)), "a field out of order": tstInfo(subject, good, cmstest.Int(7), cmstest.Seq(cmstest.Int(1))), "a reserved tag in the extensions": tstInfo(subject, good, cmstest.TLV(0xa1, cmstest.TLV(0x0e, []byte{0x41}))), "an unused-bits BIT STRING inside": tstInfo(subject, good, cmstest.TLV(0xa1, cmstest.TLV(0x03, []byte{7, 0xff}))), "version 2": cmstest.Seq(cmstest.Int(2)), "version 2, the fields complete": with(0, cmstest.Int(2)), "version 2^64 + 1": with(0, cmstest.IntBytes([]byte{1, 0, 0, 0, 0, 0, 0, 0, 1})), "the version as an ENUMERATED": with(0, cmstest.TLV(0x0a, []byte{1})), "the policy as an INTEGER": with(1, cmstest.Int(1)), "the serialNumber as OCTETS": with(3, cmstest.Octets([]byte{42})), "the messageImprint as a SET": with(2, cmstest.TLV(0x31, cmstest.Seq(cmstest.OID(oidSHA256)), cmstest.Octets(h[:]))), "a messageImprint of three fields": with(2, cmstest.Seq(cmstest.Seq(cmstest.OID(oidSHA256)), cmstest.Octets(h[:]), cmstest.Null())), "a messageImprint of one field": with(2, cmstest.Seq(cmstest.Seq(cmstest.OID(oidSHA256)))), "a messageImprint algorithm as a SET": with(2, cmstest.Seq(cmstest.TLV(0x31, cmstest.OID(oidSHA256)), cmstest.Octets(h[:]))), "a hashedMessage as a BIT STRING": with(2, cmstest.Seq(cmstest.Seq(cmstest.OID(oidSHA256)), cmstest.BitString(h[:]))), "four fields": cmstest.Seq(fields[:4]...), "a SET": cmstest.TLV(0x31, fields...), "not a SEQUENCE": cmstest.Int(1), "not DER": append(tstInfo(subject, good), 0), } { if _, err := cms.ParseToken(cmstest.TokenRaw(info, tsa)); !errors.Is(err, cms.ErrForm) { t.Errorf("%s: %v", name, err) } } } // retag changes the identifier octet of the first element of the content of // the last constructed child of der, a SET OF SignerInfo. func signerInfoTag(t *testing.T, b []byte, tag byte) []byte { t.Helper() _, ci, _ := der.Split(b) _, content, _ := der.Split(ci[1]) _, sd, _ := der.Split(content[0]) signerInfos := sd[len(sd)-1] _, infos, _ := der.Split(signerInfos) at := bytes.Index(b, infos[0]) if at < 0 { t.Fatal("no SignerInfo") } out := bytes.Clone(b) out[at] = tag return out } func TestSignatureStrictness(t *testing.T) { a := cmstest.NewECDSA("Ana", elliptic.P256(), from, to) good := cmstest.Signature(msg, cmstest.Options{}, a) for name, b := range map[string][]byte{ "ContentInfo as a SET": append([]byte{0x31}, good[1:]...), "ContentInfo as [3]": append([]byte{0xa3}, good[1:]...), "SignerInfo as a SET": signerInfoTag(t, good, 0x31), "SignerInfo as [5]": signerInfoTag(t, good, 0xa5), "an attribute without a value": cmstest.Signature(msg, cmstest.Options{ExtraAttrs: [][]byte{cmstest.Seq(cmstest.OID(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}), cmstest.Set(0x31))}}, a), "a second content-type": cmstest.Signature(msg, cmstest.Options{ExtraAttrs: [][]byte{cmstest.Seq(cmstest.OID(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}), cmstest.Set(0x31, cmstest.OID(cmstest.OIDData)))}}, a), } { if _, err := cms.ParseSignature(b); !errors.Is(err, cms.ErrForm) { t.Errorf("%s: %v", name, err) } } // Both signing-certificate attributes: the v2 counts, the other decides nothing. both := cmstest.Signature(msg, cmstest.Options{SigCertV1: true}, a) if sd, err := cms.ParseSignature(both); err != nil || sd.Signers[0].Check(msg) != cms.Valid { t.Errorf("signing-certificate beside the v2: %v", err) } // PSS parameters that write their default are not accepted as verifiable. rsa := cmstest.NewRSA("Luis", 2048, from, to) sd, err := cms.ParseSignature(cmstest.Signature(msg, cmstest.Options{PSS: true, PSSTrailer: true}, rsa)) if err != nil || sd.Signers[0].Check(msg) != cms.NotVerifiable { t.Errorf("a PSS trailerField written: %v", err) } sd, err = cms.ParseSignature(cmstest.Signature(msg, cmstest.Options{PSS: true}, rsa)) if err != nil || sd.Signers[0].Check(msg) != cms.Valid { t.Errorf("PSS without it: %v", err) } }