You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
533 lines
18 KiB
533 lines
18 KiB
// 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<String, Object?>;
|
|
|
|
/// The names of the vector files, cms_<name>.json.
|
|
const cmsFiles = [
|
|
'ecdsa',
|
|
'rsa',
|
|
'certs',
|
|
'signatures',
|
|
'algorithms',
|
|
'tokens',
|
|
'mutations',
|
|
'corpus',
|
|
];
|
|
|
|
/// The cases of [section] of [file].
|
|
List<Json> cases(Json file, String section) =>
|
|
(file[section]! as List).cast<Json>();
|
|
|
|
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<List<int>> content) {
|
|
final c = concatBytes(content);
|
|
final n = c.length;
|
|
final List<int> 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<Uint8List> _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<int> 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<String, Uint8List>? 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<String, Uint8List> 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<int> 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<int>? spanOrNull(Uint8List base, Uint8List? view) =>
|
|
view == null ? null : spanOf(base, view);
|
|
|
|
List<Object?> algOf(Uint8List base, AlgorithmIdentifier a) => [
|
|
spanOf(base, a.oid),
|
|
spanOrNull(base, a.params),
|
|
];
|
|
|
|
List<Object?> algJson(Object? v) {
|
|
final a = v! as Json;
|
|
return [a['oid'], a['params']];
|
|
}
|
|
|
|
List<int> 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<String> 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,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|