// The differential of the CMS reader against the Go reference: the cases of // test/vectors/cms_*.json, which tool/cms_go_vectors_test.go writes with // internal/cms of datekeys-go and the ECDSA and RSA of Go, run through // cms.dart, ecdsa.dart, rsa.dart and nist_curves.dart, with the same // results and the same texts. It reads no file itself, so that the tests // compiled to JavaScript run it too, on the part of the cases that // cms_vectors.g.dart holds; the cases of the fixtures run only when a // reader of fixtures is given. library; import 'dart:typed_data'; import 'package:datekeys/src/bytes.dart'; import 'package:datekeys/src/cms.dart'; import 'package:datekeys/src/datekey.dart'; import 'package:datekeys/src/der.dart' as der; import 'package:datekeys/src/ecdsa.dart'; import 'package:datekeys/src/nist_curves.dart'; import 'package:datekeys/src/rsa.dart'; import 'package:datekeys/src/sha256.dart'; import 'package:test/test.dart'; typedef Json = Map; /// The names of the vector files, cms_.json. const cmsFiles = [ 'ecdsa', 'rsa', 'certs', 'signatures', 'algorithms', 'tokens', 'mutations', 'corpus', ]; /// The cases of [section] of [file]. List cases(Json file, String section) => (file[section]! as List).cast(); Uint8List hexOf(Object? v) => fromHex(v! as String); // --------------------------------------------------------------------------- // The bytes of a case /// An element of [tag] with the encodings of [content], its length in the /// shortest form, as tlv of Go's cmstest. Uint8List tlv(int tag, List> content) { final c = concatBytes(content); final n = c.length; final List header; if (n < 0x80) { header = [tag, n]; } else if (n < 0x100) { header = [tag, 0x81, n]; } else if (n < 0x10000) { header = [tag, 0x82, n >> 8, n & 0xff]; } else { header = [tag, 0x83, n >> 16, n >> 8 & 0xff, n & 0xff]; } return concatBytes([header, c]); } List _children(Uint8List b) => der.split(b).children; // cmstest.Edit of Go: b with the element at path replaced by what f // returns for it, and the lengths of its ancestors written again. Uint8List _edit(Uint8List b, List path, Uint8List Function(Uint8List) f) { if (path.isEmpty) return f(b); final kids = [..._children(b)]; kids[path[0]] = _edit(kids[path[0]], path.sublist(1), f); return tlv(b[0], kids); } /// The edit of [c] applied to [base], as tool/cms_go_vectors_test.go does /// it. Uint8List applyEdit(Uint8List base, Json c) { final path = [for (final i in (c['path'] as List?) ?? const []) i as int]; final op = c['op']! as String; final arg = c['arg'] as int?; if (op == 'raw') { return Uint8List.fromList(base)..[arg!] ^= 1 << (c['bit']! as int); } return _edit(base, path, (old) { switch (op) { case 'retag': return concatBytes([ [arg!], old.sublist(1), ]); case 'remove': return Uint8List(0); case 'dup': return concatBytes([old, old]); case 'null': return Uint8List.fromList(const [5, 0]); case 'append': return tlv(old[0], [ ..._children(old), const [5, 0], ]); case 'reverse': return tlv(old[0], _children(old).reversed.toList()); } var content = Uint8List.fromList(der.content(old)); switch (op) { case 'flip': content[arg!] ^= 1 << (c['bit']! as int); case 'trunc': content = content.sublist(0, content.length - 1); case 'extend': content = concatBytes([ content, [arg!], ]); case 'empty': content = Uint8List(0); default: throw StateError('an edit $op'); } return tlv(old[0], [content]); }); } /// The bytes of the case [c] of [file]: its der, in hex or in pieces of hex /// and certificates of the file, or the edit of one of [bases]; checked /// against its SHA-256 when it has one. Uint8List caseBytes(Json file, Json c, {Map? bases}) { final Uint8List b; final der = c['der']; if (c['base'] != null) { b = applyEdit(bases![c['base']! as String]!, c); } else if (der is List) { final certs = [for (final h in file['certificates']! as List) hexOf(h)]; b = concatBytes([for (final p in der) p is int ? certs[p] : hexOf(p)]); } else { b = der == null ? Uint8List(0) : hexOf(der); } final sum = c['sha256']; if (sum != null) expect(toHex(sha256(b)), sum, reason: 'the bytes of $c'); return b; } /// The bases of [section] of [file], by their names. Map basesOf(Json file, String section) => { for (final c in cases(file, section)) c['name']! as String: caseBytes(file, c), }; /// A short name of the case [c], for the reasons of the expectations. String caseName(Json c) => (c['name'] ?? (c['names'] as List?)?.first ?? c['fixture'] ?? '${c['base']} ${c['path']} ${c['op']} ${c['arg'] ?? ''}') .toString(); // --------------------------------------------------------------------------- // Spans /// [view], a view of the bytes of [base], as the vectors place it: /// [offset, length]. List spanOf(Uint8List base, Uint8List view) { expect(view.buffer == base.buffer, isTrue, reason: 'not a view'); final off = view.offsetInBytes - base.offsetInBytes; expect(off >= 0 && off + view.length <= base.length, isTrue); return [off, view.length]; } List? spanOrNull(Uint8List base, Uint8List? view) => view == null ? null : spanOf(base, view); List algOf(Uint8List base, AlgorithmIdentifier a) => [ spanOf(base, a.oid), spanOrNull(base, a.params), ]; List algJson(Object? v) { final a = v! as Json; return [a['oid'], a['params']]; } List timeOf(Instant t) => [t.seconds, t.nanos]; // --------------------------------------------------------------------------- // The outcomes /// Expects cms.dart to read the certificate [b] as Go read it: the error of /// [c], or its fields, names and validity. void expectCert(Json c, Uint8List b) { final Cert cert; try { cert = parseCert(b); } on CertificateException catch (e) { expect(e.message, c['error'], reason: caseName(c)); return; } final name = caseName(c); expect(c['error'], isNull, reason: name); expect(cert.raw, b, reason: name); expect(cert.hash, sha256(b), reason: name); expect(spanOf(b, cert.serial), c['serial'], reason: name); expect(spanOf(b, cert.rawIssuer), c['issuer'], reason: name); expect(spanOf(b, cert.rawSubject), c['subject'], reason: name); expect(spanOrNull(b, cert.ski), c['ski'], reason: name); expect(spanOf(b, cert.spki), c['spki'], reason: name); expect(timeOf(cert.notBefore), c['not_before'], reason: name); expect(timeOf(cert.notAfter), c['not_after'], reason: name); expect(cert.holder, c['holder'], reason: name); expect(cert.issuerName, c['issuer_name'], reason: name); for (final v in (c['valid_at'] as List?) ?? const []) { final at = v! as List; expect( cert.validAt(Instant(at[0]! as int, at[1]! as int)), at[2], reason: '$name: valid at $at', ); } } String resultName(CmsResult r) => switch (r) { CmsResult.valid => 'valid', CmsResult.invalid => 'invalid', CmsResult.notVerifiable => 'not verifiable', }; /// Expects cms.dart to read the signature [b] as Go read it: the error of /// [c], or its certificates, OCSP responses and signers, each with the /// result of its check over [message] and [other]; and in a case of the /// corpus the names of each certificate and the token of each signer. void expectSignature(Json c, Uint8List b, Uint8List message, Uint8List other) { final name = caseName(c); final SignedData sd; try { sd = parseSignature(b); } on CmsFormException catch (e) { expect(e.message, c['error'], reason: name); return; } expect(c['error'], isNull, reason: name); expect(sd.eContent, isNull, reason: name); expect( [for (final x in sd.certs) spanOf(b, x.raw)], c['certs'], reason: name, ); expect( [for (final x in sd.ocsp) spanOf(b, x)], c['ocsp'] ?? const [], reason: name, ); final signers = cases(c, 'signers'); expect(sd.signers, hasLength(signers.length), reason: name); for (var i = 0; i < signers.length; i++) { final s = sd.signers[i]; final w = signers[i]; final where = '$name, signer $i'; expect(sd.certs.indexWhere((x) => identical(x, s.cert)), w['cert']); expect(algOf(b, s.digestAlg), algJson(w['digest_alg']), reason: where); expect(algOf(b, s.sigAlg), algJson(w['sig_alg']), reason: where); expect(spanOf(b, s.signedAttrs), w['signed_attrs'], reason: where); expect(spanOf(b, s.messageDigest), w['message_digest'], reason: where); expect(spanOf(b, s.signature), w['signature'], reason: where); expect(spanOrNull(b, s.token), w['token'], reason: where); expect(resultName(s.check(message)), w['result'], reason: where); expect(resultName(s.check(other)), w['other_result'], reason: where); if (w.containsKey('holder')) { expect(s.cert.holder, w['holder'], reason: where); expect(s.cert.issuerName, w['issuer'], reason: where); final seal = w['seal'] as Json?; expect(seal == null, s.token == null, reason: where); if (seal != null) { final t = expectToken(seal, s.token!, s.signature, corpus: true); if (t != null) { expect( s.cert.validAt(t.genTime), seal['signer_valid_at_gen_time'], reason: where, ); } } } } } /// Expects cms.dart to read the token [b] as Go read it: the error of [c] /// and its kind, or its fields and its check over [subject]. Returns the /// token when it reads. Token? expectToken( Json c, Uint8List b, Uint8List subject, { bool corpus = false, }) { final name = caseName(c); final Token t; try { t = parseToken(b); } on CmsException catch (e) { expect(e.message, c['error'], reason: name); expect( switch (e) { CmsFormException() => 'form', CmsAlgorithmException() => 'algorithm', }, c['kind'], reason: name, ); return null; } expect(c['error'], isNull, reason: name); expect(timeOf(t.genTime), c['gen_time'], reason: name); expect(t.accuracy.inMicroseconds, c['accuracy_us'], reason: name); expect( timeOf(addDuration(t.genTime, t.accuracy)), c['gen_time_plus_accuracy'], reason: name, ); expect(algOf(b, t.imprintAlg), algJson(c['imprint_alg']), reason: name); expect(spanOf(b, t.imprint), c['imprint'], reason: name); expect(spanOf(b, t.tsa.raw), c['tsa'], reason: name); expect(t.imprintIsSha256, c['imprint_sha256'], reason: name); expect(t.check(subject), c['check'], reason: name); if (corpus) expect(t.tsa.holder, c['tsa_holder'], reason: name); return t; } // --------------------------------------------------------------------------- // The tests of each file final _curves = {'P-256': p256, 'P-384': p384, 'P-521': p521}; final _hashes = { 'SHA-256': Sha2.sha256, 'SHA-384': Sha2.sha384, 'SHA-512': Sha2.sha512, }; /// The tests of cms_ecdsa.json. void ecdsaTests(Json file) { test('the curves are those of Go', () { for (final c in cases(file, 'curves')) { final curve = _curves[c['name']]!; expect(curve.p, BigInt.parse(c['p']! as String, radix: 16)); expect(curve.n, BigInt.parse(c['n']! as String, radix: 16)); expect(curve.b, BigInt.parse(c['b']! as String, radix: 16)); expect(curve.gx, BigInt.parse(c['gx']! as String, radix: 16)); expect(curve.gy, BigInt.parse(c['gy']! as String, radix: 16)); expect(curve.p.bitLength, c['bits']); expect(curve.byteLength, ((c['bits']! as int) + 7) ~/ 8); } }); test('a point reads as ecdsa.ParseUncompressedPublicKey reads it', () { for (final c in cases(file, 'points')) { final p = decodeUncompressed(_curves[c['curve']]!, hexOf(c['point'])); expect(p != null, c['ok'], reason: caseName(c)); } }); test('a signature verifies as ecdsa.VerifyASN1 verifies it', () { final keys = [ for (final k in cases(file, 'keys')) decodeUncompressed(_curves[k['curve']]!, hexOf(k['point']))!, ]; for (final c in cases(file, 'verify')) { expect( verifyEcdsaAsn1( keys[c['key']! as int], hexOf(c['hash']), hexOf(c['sig']), ), c['valid'], reason: caseName(c), ); } }); } /// The tests of cms_rsa.json. void rsaTests(Json file) { test('the DigestInfo of each hash is that of Go', () { final info = file['digest_info']! as Json; for (final MapEntry(:key, :value) in _hashes.entries) { expect(toHex(value.digestInfoPrefix), info[key], reason: key); } }); test( 'a signature verifies as rsa.VerifyPKCS1v15 and VerifyPSS verify it', () { final keys = [ for (final k in cases(file, 'keys')) RsaPublicKey( BigInt.parse(k['n']! as String, radix: 16), k['e']! as int, ), ]; for (final (i, k) in cases(file, 'keys').indexed) { expect(keys[i].n.bitLength, k['bits'], reason: caseName(k)); } for (final c in [...cases(file, 'valid'), ...cases(file, 'verify')]) { final key = keys[c['key']! as int]; final hash = _hashes[c['hash']]!; final digest = hexOf(c['digest']); final sig = hexOf(c['sig']); final valid = c['scheme'] == 'pss' ? verifyPss(key, hash, digest, sig) : verifyPkcs1v15(key, hash, digest, sig); expect(valid, c['valid'], reason: caseName(c)); } }, ); } /// The tests of cms_certs.json. void certTests(Json file) { test('a certificate reads as cms.ParseCert reads it', () { for (final c in cases(file, 'named')) { expectCert(c, caseBytes(file, c)); } }); test('each certificate edited node by node reads as in Go', () { final bases = basesOf(file, 'bases'); for (final c in cases(file, 'bases')) { expectCert(c, bases[c['name']]!); } for (final c in cases(file, 'edits')) { expectCert(c, caseBytes(file, c, bases: bases)); } }); } /// The tests of cms_signatures.json and cms_algorithms.json, whose cases /// are in [sections]. void signatureTests(Json file, List sections) { final message = hexOf(file['message']); final other = hexOf(file['other_message']); for (final section in sections) { test('$section: a signature reads and checks as in Go', () { for (final c in cases(file, section)) { expectSignature(c, caseBytes(file, c), message, other); } }); } if (file.containsKey('oids')) { test('the object identifiers are those of internal/cms', () { final oids = file['oids']! as Json; expect(profileOids.keys.toSet(), oids.keys.toSet()); for (final MapEntry(:key, :value) in profileOids.entries) { expect(toHex(value), oids[key], reason: key); } }); } } /// The tests of cms_tokens.json. void tokenTests(Json file) { final subject = hexOf(file['subject']); for (final section in const ['tokens', 'tstinfo']) { test('$section: a token reads and checks as in Go', () { for (final c in cases(file, section)) { expectToken(c, caseBytes(file, c), subject); } }); } } /// The tests of cms_mutations.json. void mutationTests(Json file) { final message = hexOf(file['message']); final other = hexOf(file['other_message']); final subject = hexOf(file['subject']); test('each signature edited node by node reads and checks as in Go', () { final bases = basesOf(file, 'signature_bases'); for (final c in cases(file, 'signature_bases')) { expectSignature(c, bases[c['name']]!, message, other); } for (final c in cases(file, 'signatures')) { expectSignature(c, caseBytes(file, c, bases: bases), message, other); } }); test('each token edited node by node reads and checks as in Go', () { final bases = basesOf(file, 'token_bases'); for (final c in cases(file, 'token_bases')) { expectToken(c, bases[c['name']]!, subject); } for (final c in cases(file, 'tokens')) { expectToken(c, caseBytes(file, c, bases: bases), subject); } }); } /// The tests of cms_corpus.json; those of the fixtures need [fixture], the /// record of a fixture of testdata/ by its name. void corpusTests(Json file, {Json Function(String name)? fixture}) { final other = hexOf(file['other_message']); test('the signatures of security_cms.json read and check as in Go', () { for (final c in cases(file, 'signatures')) { final b = caseBytes(file, c); expectSignature(c, b, hexOf(c['message']), other); } }); test('the seals of security_cms.json read and check as in Go', () { for (final c in cases(file, 'seals')) { expectToken(c, caseBytes(file, c), hexOf(c['subject']), corpus: true); } }); if (fixture == null) return; test('the signature and the seal of the fixtures read as in Go', () { for (final c in cases(file, 'fixtures')) { final area = hexOf(fixture(c['fixture']! as String)['security_cbor']); final span = c['span']! as List; final start = span[0]! as int; final b = Uint8List.sublistView(area, start, start + (span[1]! as int)); expect(toHex(sha256(b)), c['sha256']); final name = {'name': c['fixture']}; if (c['kind'] == 'signature') { expectSignature( {...c['signature']! as Json, ...name}, b, hexOf(c['message']), other, ); } else { expectToken( {...c['seal']! as Json, ...name}, b, hexOf(c['subject']), corpus: true, ); } } }); }