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.
dateKeys-dart/test/cms_support.dart

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,
);
}
}
});
}

Powered by TurnKey Linux.