Stage 5c: the verdicts of alg 2 and seal_type 2, and the opening gives them

securitycms.dart ports evaluateCMS, signerLine and evaluateSeal of
signature2.go of Go at the draft v0.12, on the reader of CMS of stage 5a,
with the same order of checks: SIGNERS with its profile and at most 16
entries, then the SignedData; each required signer in the order of
SIGNERS and each foreign one in the order of the encoding, valid,
invalid, absent, not verifiable, without seal, with an invalid seal or
out of validity at the time of its seal; F2, F5 and F6 with their
detail; and the seal over SEAL_SUBJECT, S1 to S5 with the authority and
t. A round time at Go's zero time is no round time, as IsZero, and
Verdicts.sealedAt skips a seal at that time, as SealedAt.

cmsReader is the default CmsEvaluator of evaluateSecurity, and so of
evaluateSecurityInput and the opening: nothing that Go evaluates is left
not evaluated; a caller that passes cms: null still gets the parts
without CMS alone. encodeSigners and maxSigners are exported, as
EncodeSigners and MaxSigners of Go.

The tests compare every part with Go: the 135 cases of security_cms.json
with the result of each signer, the 24 of security.json, the 56
signatures of alg 2 and 105 seals of seal_type 2 of security_vectors.json,
the 755 cases of securitycms_vectors.json with their detail and earliest
seal, the fixtures format3_signed_cms and format3_sealed, and their
openings in open_cases.json. On Node.js, a part of the vectors and the
two fixtures opened in full. 1572 tests on the VM and 332 on Node.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 2 days ago
parent 6ad251bb25
commit 8dc45c2712

@ -2,28 +2,28 @@
/// DateKeys Access Key (`.dkk`), as `datekeys-go` and `datekeys-ts` implement
/// them, checked against the same test data.
///
/// Stages 0 to 4 of docs/PLAN_dart.md and part 5b of stage 5: the package and
/// its test data, the normative errors of spec §69, byte helpers and the CBOR
/// profile of spec §58, the primitives and the reading of age files, then
/// BLS12-381 and tlock: the check of a compressed point and the verification
/// of releases, with the sources that deliver them. Then the key of words of
/// spec §38.1, and the formats: the frames of the .dkc and the .dkk,
/// PUBLIC_HEADER, CONTROL_CBOR of the three formats, the .dkk, the
/// extensions with their registries, the Provider Profile with the pinned
/// Quicknet, the DateKey with its rounds and times, the padding, the head of
/// format 3 and the public note. And the reading of a capsule: its
/// inspection, steps 1 to 8 of spec §63, and its opening, steps 9 to 18, in
/// memory or from a source read by ranges, to a sink of bytes or of files.
/// Stages 0 to 5 of docs/PLAN_dart.md: the package and its test data, the
/// normative errors of spec §69, byte helpers and the CBOR profile of spec
/// §58, the primitives and the reading of age files, then BLS12-381 and
/// tlock: the check of a compressed point and the verification of releases,
/// with the sources that deliver them. Then the key of words of spec §38.1,
/// and the formats: the frames of the .dkc and the .dkk, PUBLIC_HEADER,
/// CONTROL_CBOR of the three formats, the .dkk, the extensions with their
/// registries, the Provider Profile with the pinned Quicknet, the DateKey
/// with its rounds and times, the padding, the head of format 3 and the
/// public note. And the reading of a capsule: its inspection, steps 1 to 8
/// of spec §63, and its opening, steps 9 to 18, in memory or from a source
/// read by ranges, to a sink of bytes or of files.
/// And the security area of format 3: what an author signs and a seal seals,
/// SECURITY_CBOR, the signature of alg 1 and the verdicts with their texts
/// and lines, which the opening evaluates as Go; the signature of alg 2 and
/// the seal of seal_type 2 wait for the reader of CMS of stage 5c, behind
/// CmsEvaluator.
/// SECURITY_CBOR, the signature of alg 1, the signature of alg 2 with
/// certificates and the seal of seal_type 2 with the reader of CMS, and the
/// verdicts with their texts and lines, which the opening evaluates as Go.
///
/// DER, the primitives, age, agewrap, the curve arithmetic, the IBE of tlock
/// and its stanza, the rules of paths and texts on the Unicode tables, the
/// frame of BODY, the digest of a capsule and the steps of the opening are
/// internal, as in the Go reference and datekeys-ts.
/// frame of BODY, the digest of a capsule, the steps of the opening, and
/// the reader of CMS with its ECDSA and RSA are internal, as in the Go
/// reference and datekeys-ts.
library;
export 'src/accesskey.dart';
@ -65,6 +65,7 @@ export 'src/release.dart'
suppliedRelease,
verifyRelease;
export 'src/security.dart';
export 'src/securitycms.dart';
export 'src/sink.dart';
export 'src/source.dart' show ByteSource, BytesSource;
export 'src/verdicts.dart';

@ -132,10 +132,9 @@ final class OpenOptions {
/// Evaluates the security area of a capsule of format 3 (spec §29.7), as
/// Go evaluates it at step 17: [evaluateSecurityInput] by default, which
/// checks the signature of alg 1 and every verdict of the form, and leaves
/// the signature of alg 2 and the seal of seal_type 2 not evaluated until
/// a reader of CMS is given (see [CmsEvaluator]). Whatever it throws, the
/// capsule opens with [Verdicts.failed] (spec §29.3).
/// checks every part as Go, the signature of alg 2 and the seal of
/// seal_type 2 with the reader of CMS ([cmsReader]). Whatever it throws,
/// the capsule opens with [Verdicts.failed] (spec §29.3).
final SecurityEvaluator evaluator;
/// Receives the verdicts of a capsule of format 3 after every check of

@ -21,8 +21,10 @@
/// The signature of alg 1 is checked here, with the strict profile of
/// verifyStrict. The signature of alg 2, a CMS SignedData with certificates,
/// and the seal of seal_type 2, an RFC 3161 token, are checked by a
/// [CmsEvaluator], the reader of CMS of stage 5c of docs/PLAN_dart.md:
/// without one, their verdict is not evaluated, never guessed.
/// [CmsEvaluator]: [cmsReader] of securitycms.dart by default, which joins
/// the reader of CMS of cms.dart to these verdicts, as signature2.go of Go.
/// A caller that passes none leaves their verdict not evaluated, never
/// guessed.
library;
import 'dart:typed_data';
@ -36,6 +38,7 @@ import 'datekey.dart';
import 'errors.dart';
import 'pathrule.dart';
import 'schema.dart';
import 'securitycms.dart';
import 'verdicts.dart';
/// The type tag of SECURITY_CBOR (spec §29.3).
@ -429,10 +432,11 @@ final class CmsSealVerdict {
/// The reader of CMS that evaluates a signature of alg 2 and a seal of
/// seal_type 2 (spec §29.10, §29.11), as evaluateCMS and evaluateSeal of Go
/// (signature2.go), which stage 5c of docs/PLAN_dart.md implements. Without
/// one, [evaluateSecurity] does not evaluate them: their verdict is null.
/// What it throws, or a verdict out of its range, is a failure of its own
/// part only: F1 for the signature and S2 for the seal, as a panic in Go.
/// (signature2.go): [cmsReader] of securitycms.dart, the default of
/// [evaluateSecurity]. Without one, [evaluateSecurity] does not evaluate
/// them: their verdict is null. What it throws, or a verdict out of its
/// range, is a failure of its own part only: F1 for the signature and S2 for
/// the seal, as a panic in Go.
abstract interface class CmsEvaluator {
/// The verdict of a signature of alg 2 whose key 1 is [signers], SIGNERS,
/// and whose key 2 is [value], the DER of its SignedData, in [context]
@ -515,13 +519,15 @@ const _Signature _unchecked = (
///
/// Without a context, as EvaluateSecurity of Go and as a reader of v0.10,
/// it checks only the structure: any signature is F1, and a seal of
/// seal_type 2, S1. In a context it checks the signature of alg 1, and
/// [cms], the reader of CMS, the signature of alg 2 and the seal of
/// seal_type 2; without [cms] their verdict is null, not evaluated.
/// seal_type 2, S1. In a context it checks every part as Go: the signature
/// of alg 1 here, and the signature of alg 2 and the seal of seal_type 2
/// with [cms], the reader of CMS, [cmsReader] by default. A caller that
/// passes null for [cms] leaves those two not evaluated, null, as a reader
/// without CMS would.
Verdicts evaluateSecurity(
List<int> security, {
SecurityContext? context,
CmsEvaluator? cms,
CmsEvaluator? cms = cmsReader,
}) {
// A failure of any kind is a failure of the form of its own part, as Go
// recovers a panic: X for the outer map, F1 for the signature and S2 for
@ -577,11 +583,9 @@ Verdicts evaluateSecurity(
}
/// The evaluator of the opening, the default of OpenOptions.evaluator: the
/// verdicts of [input] in its [securityContext], with the signature of alg 1
/// and every verdict of the form. The signature of alg 2 and the seal of
/// seal_type 2 are not evaluated until the reader of CMS of stage 5c is
/// given; an application that has one evaluates them with
/// [evaluateSecurity] and its [CmsEvaluator], in an evaluator of its own.
/// verdicts of [input] in its [securityContext], every part as Go's
/// newSecurityContext and EvaluateSecurityIn give them, the signature of
/// alg 2 and the seal of seal_type 2 with [cmsReader].
Verdicts evaluateSecurityInput(SecurityInput input) =>
evaluateSecurity(input.security, context: securityContext(input));

@ -0,0 +1,271 @@
/// The verdicts of a signature of alg 2, a CMS signature with X.509
/// certificates, and of a seal of seal_type 2, an RFC 3161 time-stamp token
/// (spec v0.12 §29.7, §29.10, §29.11), as signature2.go of package capsule of
/// datekeys-go at the draft v0.12 (c531e93) and securitycms.ts of
/// datekeys-ts: the same order of checks, the same verdicts, the same result
/// for each signer and the same detail, which the lines of verdicts.dart
/// write as Go writes them.
///
/// It joins the reader of CMS of cms.dart, stage 5a of docs/PLAN_dart.md,
/// to the verdicts of security.dart, stage 5b, through [CmsEvaluator]:
/// [cmsReader] is the evaluator that [evaluateSecurity] uses by default, and
/// so does the opening. What cms.dart throws for a signature or a token that
/// breaks its profile is a verdict here; anything else it throws reaches
/// [evaluateSecurity], which makes it a failure of its own part only, F1 or
/// S2, as Go recovers a panic.
///
/// The public part is that of Go's package capsule: [maxSigners],
/// [encodeSigners] and the evaluator. cms.dart stays internal, as
/// internal/cms of Go.
library;
import 'dart:typed_data';
import 'author.dart';
import 'bytes.dart';
import 'cbor.dart';
import 'cms.dart' as cms;
import 'datekey.dart';
import 'errors.dart';
import 'security.dart';
import 'sha256.dart';
import 'verdicts.dart';
/// The most required signers of a signature of alg 2 (spec §29.10), as
/// MaxSigners of Go.
const maxSigners = 16;
/// The size of each element of SIGNERS: the SHA-256 of a certificate.
const _signerHashSize = 32;
/// SIGNERS, the content of key 1 of an author-signature of alg 2, as
/// EncodeSigners of Go: a CBOR array of 1 to [maxSigners] byte strings of 32
/// bytes, [hashes], the SHA-256 of the certificate of each required signer,
/// in strictly ascending order of bytes (spec §29.10). It sorts them, and
/// throws an [ArgumentError] with the text of Go when there are none, too
/// many, or two that are equal, and when one is not 32 bytes, which Go's
/// type [32]byte rules out.
Uint8List encodeSigners(List<List<int>> hashes) {
if (hashes.isEmpty || hashes.length > maxSigners) {
throw ArgumentError('capsule: SIGNERS holds from 1 to 16 certificates');
}
for (final h in hashes) {
if (h.length != _signerHashSize) {
throw ArgumentError(
'capsule: a SHA-256 of ${h.length} bytes, want $_signerHashSize',
);
}
}
final sorted = [for (final h in hashes) Uint8List.fromList(h)]
..sort(compareBytes);
for (var i = 1; i < sorted.length; i++) {
if (equalBytes(sorted[i - 1], sorted[i])) {
throw ArgumentError('capsule: SIGNERS names a certificate twice');
}
}
final e = CborEncoder()..array(sorted.length);
for (final h in sorted) {
e.bstr(h);
}
return Uint8List.fromList(e.out());
}
// decodeSigners of Go: SIGNERS with the profile of spec §29.10, 1 to 16
// strings of 32 bytes in strictly ascending order, in the CBOR profile of
// §58. Throws a DateKeysException otherwise, the verdict F1.
List<Uint8List> _decodeSigners(Uint8List b) {
final out = <Uint8List>[];
unmarshalCbor(
b,
(d) {
final n = d.array(maxSigners);
if (n < 1) {
throw DateKeysException(ErrorCode.nonCanonicalCbor, 'SIGNERS is empty');
}
for (var i = 0; i < n; i++) {
final h = d.bstr(_signerHashSize, _signerHashSize);
if (out.isNotEmpty && compareBytes(out.last, h) >= 0) {
throw DateKeysException(
ErrorCode.nonCanonicalCbor,
'SIGNERS is not in strictly ascending order',
);
}
out.add(h);
}
},
(e) {
e.array(out.length);
for (final h in out) {
e.bstr(h);
}
},
);
return out;
}
/// The reader of CMS of the verdicts: evaluateCMS and evaluateSeal of Go
/// (signature2.go). The default of [evaluateSecurity], and so of the
/// opening; an evaluator of one's own can wrap it.
const CmsEvaluator cmsReader = _CmsReader();
final class _CmsReader implements CmsEvaluator {
const _CmsReader();
// evaluateCMS of Go (spec §29.10): null, F1, for SIGNERS or a SignedData
// that break their profile; then each required signer, in the order of
// SIGNERS, and each SignerInfo of another certificate, in the order of the
// encoding, every one checked over AUTHOR_MESSAGE with the signers_digest
// of these very SIGNERS. F2 when a required signer is invalid; F5 when one
// is anything else but valid, or when hasSeal; F6 otherwise.
@override
CmsSignatureVerdict? evaluateSignature(
Uint8List signers,
Uint8List value,
bool hasSeal,
SecurityContext context,
) {
final List<Uint8List> required;
final cms.SignedData sd;
try {
required = _decodeSigners(signers);
sd = cms.parseSignature(value);
} on DateKeysException {
return null;
} on cms.CmsException {
return null;
}
final message = authorMessage(
context.controlCommit,
context.headDigest,
signersDigest(algCms, signers),
);
// ParseSignature gives each certificate one SignerInfo at most, and two
// certificates of the same bytes are one.
final byHash = {for (final s in sd.signers) toHex(s.cert.hash): s};
var invalid = false;
var incomplete = hasSeal;
final lines = <SignerLine>[];
for (final h in required) {
final s = byHash[toHex(h)];
if (s == null) {
// Named by the hash that SIGNERS gives, without an issuer.
lines.add(SignerLine(holder: toHex(h), result: SignerResult.absent));
incomplete = true;
continue;
}
final line = _signerLine(s, message, context.roundTime);
switch (line.result) {
case SignerResult.invalid:
invalid = true;
case SignerResult.valid:
break;
default:
incomplete = true;
}
lines.add(line);
}
final foreign = [
for (final s in sd.signers)
if (!required.any((h) => equalBytes(h, s.cert.hash)))
_signerLine(s, message, context.roundTime),
];
return CmsSignatureVerdict(
invalid
? Verdict.signatureInvalid
: incomplete
? Verdict.signedIncomplete
: Verdict.signedComplete,
Detail(signers: lines, foreign: foreign),
);
}
// evaluateSeal of Go (spec §29.11): S2 for a token whose form breaks the
// profile, S1 for an algorithm outside the table or an imprint that is not
// SHA-256, S3 when it does not verify over SEAL_SUBJECT with the SIG_PART
// of signature, and S4 or S5 when it does, with the authority and t.
@override
CmsSealVerdict evaluateSeal(
Uint8List token,
Uint8List? signature,
SecurityContext context,
) {
final cms.Token tok;
try {
tok = cms.parseToken(token);
} on cms.CmsFormException {
return const CmsSealVerdict(Verdict.sealUnreadable);
} on cms.CmsAlgorithmException {
return const CmsSealVerdict(Verdict.sealUnsupported);
}
if (!tok.imprintIsSha256) {
return const CmsSealVerdict(Verdict.sealUnsupported);
}
final subject = sealSubject(
context.controlCommit,
context.headDigest,
sigPart(signature),
);
if (!tok.check(subject)) return const CmsSealVerdict(Verdict.sealInvalid);
return CmsSealVerdict(
_before(tok, context.roundTime) ? Verdict.sealed : Verdict.sealedLate,
holder: holderText(tok.tsa.holder, tok.tsa.hash),
time: tok.genTime,
);
}
}
// signerLine of Go: one SignerInfo as spec §29.10 orders its checks, not
// verifiable, invalid, without seal, with an invalid seal, out of validity,
// or valid with the authority and t of its seal. The certificate is valid
// or not at t, the time of the seal, both ends included. The issuer is text
// of the certificate, as the holder is: it gets the same rules, and the
// SHA-256 of the DER of its Name when it fails them, so that no escape, no
// control and no bidirectional character reaches a line of the verdicts.
SignerLine _signerLine(cms.SignerInfo s, Uint8List message, Instant? round) {
final holder = holderText(s.cert.holder, s.cert.hash);
final issuer = holderText(s.cert.issuerName, sha256(s.cert.rawIssuer));
SignerLine line(SignerResult result) =>
SignerLine(holder: holder, issuer: issuer, result: result);
switch (s.check(message)) {
case cms.CmsResult.notVerifiable:
return line(SignerResult.notVerifiable);
case cms.CmsResult.invalid:
return line(SignerResult.invalid);
case cms.CmsResult.valid:
break;
}
final token = s.token;
if (token == null) return line(SignerResult.withoutSeal);
// The token of a signer is read with the profile of §29.11 over the value
// of its signature: S2, S1 or S3 is an invalid seal. Its imprint may be of
// any hash of the table.
final cms.Token tok;
try {
tok = cms.parseToken(token);
} on cms.CmsException {
return line(SignerResult.invalidSeal);
}
if (!tok.check(s.signature)) return line(SignerResult.invalidSeal);
if (!s.cert.validAt(tok.genTime)) return line(SignerResult.outOfValidity);
return SignerLine(
holder: holder,
issuer: issuer,
result: SignerResult.valid,
sealHolder: holderText(tok.tsa.holder, tok.tsa.hash),
sealTime: tok.genTime,
before: _before(tok, round),
);
}
// Whether t plus the accuracy of tok is before round_time (spec §29.7): the
// seal proves something before the capsule could open. A round time that is
// not known, null or Go's zero time, which IsZero reports, has no seal
// before it.
bool _before(cms.Token tok, Instant? round) =>
round != null &&
!_isGoZero(round) &&
compareInstants(cms.addDuration(tok.genTime, tok.accuracy), round) < 0;
// Go's zero time.Time, 0001-01-01T00:00:00Z, which SecurityContext of Go
// reads as no round time.
bool _isGoZero(Instant t) => t.seconds == -62135596800 && t.nanos == 0;

@ -8,9 +8,9 @@
/// area require (spec §29.2), and hands it, with what a verdict needs
/// besides, to a [SecurityEvaluator]: Go's newSecurityContext and
/// EvaluateSecurityIn. The default one, evaluateSecurityInput of
/// security.dart, evaluates the signature of alg 1 and every verdict of the
/// form; the signature of alg 2 and the seal of seal_type 2 need the reader
/// of CMS (stage 5c of docs/PLAN_dart.md), and without it are not evaluated.
/// security.dart, evaluates every part as Go: the signature of alg 1, the
/// signature of alg 2 and the seal of seal_type 2, these two with the reader
/// of CMS of securitycms.dart, and every verdict of the form.
///
/// The texts and the lines are those of Verdict.Text and Verdicts.Lines of
/// Go (format3.go) at the draft v0.12, byte for byte: the names of a
@ -237,8 +237,9 @@ final class Detail {
/// as Verdicts of Go: one for the signature and one for the seal. Either is
/// null when it was not evaluated: both when the evaluator failed, or when
/// there was none to evaluate them, and one alone when it needs the reader
/// of CMS, alg 2 or seal_type 2, and none was given (see security.dart). A
/// verdict that is not evaluated is never guessed.
/// of CMS, alg 2 or seal_type 2, and the caller of evaluateSecurity gave
/// none (see security.dart). A verdict that is not evaluated is never
/// guessed.
final class Verdicts {
/// The verdicts [signature] and [seal], with the public key of a valid
/// signature of alg 1 ([authorKey], F3 and F4), the label of the saved key
@ -377,13 +378,16 @@ final class Verdicts {
/// of a required signer of a signature of alg 2, and null when there is
/// none (spec §29.7), as SealedAt of Go. A reader shows an mtime later
/// than it as an inconsistency: whoever made the capsule claims a file
/// that is newer than the proof that it existed.
/// that is newer than the proof that it existed. As in Go, where a time
/// that IsZero is no time, a seal of 0001-01-01T00:00:00Z gives none.
Instant? get sealedAt {
final d = detail;
if (d == null) return null;
Instant? best;
void take(Instant? t) {
if (t != null && (best == null || compareInstants(t, best!) < 0)) {
if (t != null &&
!_isGoZero(t) &&
(best == null || compareInstants(t, best!) < 0)) {
best = t;
}
}
@ -416,6 +420,10 @@ String _quoted(String name) => '«$name»';
String _instant(Instant? t) =>
t == null ? '0001-01-01T00:00:00Z' : formatRfc3339Nano(t);
/// Whether [t] is Go's zero time, 0001-01-01T00:00:00Z, which IsZero reports
/// as no time.
bool _isGoZero(Instant t) => t.seconds == -62135596800 && t.nanos == 0;
/// What the verdicts of the security area need besides it (spec §29.7,
/// §29.8): what Go's newSecurityContext and EvaluateSecurityIn take, given
/// at step 17 once the head is read, before it is decoded, as in Go.

@ -74,7 +74,6 @@ void main() {
verdictDifferences(
c['verdicts']! as Json,
o.opened.verdicts!,
cmsParts(security),
keys: false,
),
isEmpty,

@ -338,15 +338,7 @@ List<String> differences(Json c, Outcome o) {
same('verdicts recorded', verdicts != null, o.result == 'ok' && format3);
if (verdicts != null && o.opened.verdicts != null) {
same('evaluations', o.securities.length, 1);
if (o.securities.length == 1) {
out.addAll(
verdictDifferences(
verdicts,
o.opened.verdicts!,
cmsParts(o.securities.single),
),
);
}
out.addAll(verdictDifferences(verdicts, o.opened.verdicts!));
}
return out;
}

@ -1,10 +1,8 @@
// Helpers of the tests of the security area against the vectors of Go: the
// verdicts of an area as the vectors record them, compared part by part.
// The signature of alg 2 and the seal of seal_type 2 need the reader of CMS
// of stage 5c, which this library does not have yet: such a part must be not
// evaluated, never guessed, and Go's verdict for it one that such a part can
// give. They read no file, so that the tests that run on Node.js can use
// them.
// verdicts of an area as the vectors record them, compared part by part,
// the signature of alg 2 and the seal of seal_type 2 with the reader of CMS
// of securitycms.dart, as evaluateSecurity does by default. They read no
// file, so that the tests that run on Node.js can use them.
library;
import 'dart:typed_data';
@ -30,26 +28,18 @@ import 'tlock_support.dart' show applyEdits;
);
}
/// The verdicts that a signature of alg 2 gives in Go, and those of a seal
/// of seal_type 2 in a context (spec §29.7, §29.10, §29.11).
const cmsSignatureVerdicts = {'F1', 'F2', 'F5', 'F6'};
const cmsSealVerdicts = {'S1', 'S2', 'S3', 'S4', 'S5'};
/// Whether [t] is Go's zero time, which Go writes as no time.
bool isGoZero(Instant t) => t.seconds == -62135596800 && t.nanos == 0;
/// The number of lines that the seal of Go's verdicts adds: none for S0, and
/// one for any other, S4 with its authority.
int sealLines(String seal) => seal == 'S0' ? 0 : 1;
/// [t] as the vectors of Go write a time: RFC 3339 with its fraction, and
/// null for none or for Go's zero time.
String? timeText(Instant? t) =>
t == null || isGoZero(t) ? null : formatRfc3339Nano(t);
/// The differences between [v] and [want], the verdicts of Go as the vectors
/// record them: signature, seal, lines, and with [keys] author_key,
/// author_label and sealed_at. A part that needs the reader of CMS, as [cms]
/// says, must be not evaluated, and its lines are those of Go without that
/// part; the other part must be Go's.
List<String> verdictDifferences(
Json want,
Verdicts v,
({bool signature, bool seal}) cms, {
bool keys = true,
}) {
/// author_label and sealed_at.
List<String> verdictDifferences(Json want, Verdicts v, {bool keys = true}) {
final out = <String>[];
void same(String what, Object? got, Object? expected) {
final g = canonical(got);
@ -57,46 +47,12 @@ List<String> verdictDifferences(
if (g != w) out.add('$what: got $g, want $w');
}
final signature = want['signature']! as String;
final seal = want['seal']! as String;
final lines = (want['lines']! as List).cast<String>();
if (v.error != null) out.add('the evaluator failed: ${v.error}');
if (cms.signature) {
same('signature of alg 2', v.signature?.code, null);
if (!cmsSignatureVerdicts.contains(signature)) {
out.add('Go gives $signature to a signature of alg 2');
}
} else {
same('signature', v.signature?.code, signature);
}
if (cms.seal) {
same('seal of seal_type 2', v.seal?.code, null);
if (!cmsSealVerdicts.contains(seal)) {
out.add('Go gives $seal to a seal of seal_type 2');
}
} else {
same('seal', v.seal?.code, seal);
}
if (!cms.signature && !cms.seal) {
same('lines', v.lines, lines);
final at = v.sealedAt;
if (keys) {
same(
'sealed_at',
at == null ? null : formatRfc3339Nano(at),
want['sealed_at'],
);
}
} else if (!cms.signature) {
// The seal of seal_type 2 is not evaluated: its line is not there.
same('lines', v.lines, lines.sublist(0, lines.length - sealLines(seal)));
} else if (!cms.seal) {
// The signature of alg 2 is not evaluated: only the line of the seal.
same('lines', v.lines, lines.sublist(lines.length - sealLines(seal)));
} else {
same('lines', v.lines, <String>[]);
}
same('signature', v.signature?.code, want['signature']);
same('seal', v.seal?.code, want['seal']);
same('lines', v.lines, want['lines']);
if (keys) {
same('sealed_at', timeText(v.sealedAt), want['sealed_at']);
final key = v.authorKey;
same(
'author_key',
@ -108,6 +64,31 @@ List<String> verdictDifferences(
return out;
}
/// A signer as the lines section of security_vectors.json and the cases of
/// securitycms_vectors.json write it, as signerJSON of
/// tool/security_go_vectors.go.
Json signerJson(SignerLine s) => {
'holder': s.holder,
'issuer': s.issuer,
'result': s.result.code,
'seal_holder': s.sealHolder,
'before': s.before,
if (timeText(s.sealTime) != null) 'seal_time': timeText(s.sealTime),
};
/// The detail of [v] as tool/security_go_vectors.go writes Go's, null
/// without one.
Json? detailJson(Verdicts v) {
final d = v.detail;
if (d == null) return null;
return {
'signers': [for (final s in d.signers) signerJson(s)],
'foreign': [for (final s in d.foreign) signerJson(s)],
'seal_holder': d.sealHolder,
if (timeText(d.sealTime) != null) 'seal_time': timeText(d.sealTime),
};
}
/// The bytes of [hex] or null.
Uint8List? hexOrNull(Object? hex) =>
hex == null ? null : fromHex(hex as String);
@ -167,7 +148,8 @@ Uint8List securityOf(Json c, List<Uint8List> bases) {
}
/// The differences between the evaluation of the case [c] and Go's: its
/// verdicts and lines, the parts of CMS, and alg and seal_type as read.
/// verdicts, lines and detail, the parts of CMS, and alg and seal_type as
/// read.
List<String> evaluateDifferences(
Json c,
Uint8List security,
@ -179,13 +161,13 @@ List<String> evaluateDifferences(
final context = at == null ? null : contexts[at];
final v = evaluateSecurity(security, context: context);
final goCms = ((c['cms'] as List?) ?? const []).cast<String>();
// The parts that the reader of CMS evaluates, as this library reads the
// area: those of Go, in a context.
final mine = cmsParts(security);
final cms = (
signature: goCms.contains('signature'),
seal: goCms.contains('seal'),
);
// The parts that only the reader of CMS evaluates, as this library reads
// the area: those of Go, in a context.
final mine = cmsParts(security);
if (context != null && mine != cms || context == null && goCms.isNotEmpty) {
out.add('cms: $mine, Go $goCms');
}
@ -205,7 +187,12 @@ List<String> evaluateDifferences(
...c,
'lines': [for (final i in (c['lines']! as List).cast<int>()) texts[i]],
};
out.addAll(verdictDifferences(want, v, cms));
out.addAll(verdictDifferences(want, v));
// Go's detail, the signers of alg 2 and the authority of a valid seal,
// which the generator writes whenever Go has one.
if (canonical(detailJson(v)) != canonical(c['detail'])) {
out.add('detail ${canonical(detailJson(v))}');
}
return out;
}
@ -372,3 +359,87 @@ List<String> encodeDifferences(List<Json> cases) {
}
return out;
}
// ---------------------------------------------------------------------------
// testdata/vectors/security_cms.json
/// The results of the signers [lines] as security_cms.json and the records
/// of the fixtures write them, as vectorSignerResults of Go's
/// cmsvectors_test.go: t with its fraction, only for a seal that verifies.
List<Json> signerResults(List<SignerLine> lines) => [
for (final l in lines)
{
'holder': l.holder,
'issuer': l.issuer,
'result': l.result.code,
if (timeText(l.sealTime) != null) 'seal_time': timeText(l.sealTime),
'before_round_time': l.before,
},
];
/// What a case of security_cms.json records of the verdicts [v], as
/// TestCMSVectors of Go reads them: the verdicts, the results of the
/// required signers and of the foreign ones, each list only when it is not
/// empty, the authority and t of a valid seal of key 3, and the lines.
Json cmsVectorOf(Verdicts v) {
final d = v.detail;
final when = d == null ? null : timeText(d.sealTime);
return {
'signature': v.signature?.code,
'seal': v.seal?.code,
if (d != null && d.signers.isNotEmpty) 'signers': signerResults(d.signers),
if (d != null && d.foreign.isNotEmpty)
'foreign_signers': signerResults(d.foreign),
if (when != null) 'seal_holder': d!.sealHolder,
'seal_time': ?when,
'lines': v.lines,
};
}
/// The context of a case of security_cms.json.
SecurityContext cmsVectorContext(Json c) {
final ctx = c['context']! as Json;
return SecurityContext(
controlCommit: fromHex(str(ctx, 'control_commit')),
headDigest: fromHex(str(ctx, 'head_digest')),
roundTime: parseRfc3339(str(ctx, 'round_time')),
);
}
/// The differences between the case [c] of security_cms.json and its
/// evaluation in its context, as TestCMSVectors of Go checks them: the
/// verdicts, the results, the seal and the lines, byte for byte, and no line
/// with a control or a bidirectional character.
List<String> cmsVectorDifferences(Json c) {
final v = evaluateSecurity(
fromHex(str(c, 'security_cbor')),
context: cmsVectorContext(c),
);
final out = <String>[];
final want = {
for (final k in const [
'signature',
'seal',
'signers',
'foreign_signers',
'seal_holder',
'seal_time',
'lines',
])
if (c.containsKey(k)) k: c[k],
};
final got = cmsVectorOf(v);
if (canonical(got) != canonical(want)) out.add('got ${canonical(got)}');
for (final line in v.lines) {
for (final r in line.runes) {
if (r < 0x20 ||
r >= 0x7f && r <= 0x9f ||
r >= 0x202a && r <= 0x202e ||
r >= 0x2066 && r <= 0x2069 ||
r == 0xfeff) {
out.add('a line with U+${r.toRadixString(16)}: $line');
}
}
}
return out;
}

@ -128,10 +128,27 @@ void main() {
);
final tsaTime = parseRfc3339('2026-09-29T10:00:00Z');
test('is cmsReader by default, and the opening evaluates with it', () {
// SIGNERS empty is F1, and a token that is not DER, S2.
final v = evaluateSecurity(
encodeSecurityWith(signature: alg2, seal: seal2),
context: context,
);
expect(
[v.signature, v.seal, v.evaluated],
[Verdict.signatureUnchecked, Verdict.sealUnreadable, true],
);
expect(v.lines, [
Verdict.signatureUnchecked.text,
Verdict.sealUnreadable.text,
]);
});
test('without one, alg 2 and seal_type 2 are not evaluated', () {
final v = evaluateSecurity(
encodeSecurityWith(signature: alg2, seal: seal2),
context: context,
cms: null,
);
expect(
[v.signature, v.seal, v.evaluated, v.lines],
@ -140,6 +157,7 @@ void main() {
final s = evaluateSecurity(
encodeSecurityWith(signature: alg2, seal: encodeSeal(1, [1])),
context: context,
cms: null,
);
expect(
[s.signature, s.seal, s.lines],
@ -155,6 +173,7 @@ void main() {
seal: seal2,
),
context: context,
cms: null,
);
expect(
[f.signature, f.seal, f.lines],

@ -94,7 +94,7 @@ void main() {
});
test('security.json: the verdicts and the lines of each area in its '
'context; those of alg 2 and seal_type 2 are of stage 5c', () {
'context', () {
final f = readJson('testdata/vectors/security.json');
expect(f['spec'], specVersion);
final ctx = f['context']! as Json;
@ -113,10 +113,11 @@ void main() {
final v = evaluateSecurity(security, context: context);
// The key of F4 is in its line.
expect(
verdictDifferences(c, v, parts, keys: false),
verdictDifferences(c, v, keys: false),
isEmpty,
reason: str(c, 'name'),
);
expect(v.evaluated, isTrue, reason: str(c, 'name'));
}
// A seal of seal_type 2 whose token is not DER: S2, from the reader of
// CMS.
@ -187,20 +188,43 @@ void main() {
);
}
// The verdicts of the record, in the context of the capsule.
// The verdicts of the record, in the context of the capsule, with
// the results of the signers of alg 2 and the authority and t of a
// seal of seal_type 2, which are also the earliest seal.
final context = SecurityContext(
controlCommit: cc,
headDigest: hd,
roundTime: parseRfc3339(str(r, 'unlock_at')),
);
final v = evaluateSecurity(security, context: context);
final want = r['verdicts']! as Json;
expect(verdictDifferences(want, v, keys: false), isEmpty);
final key = v.authorKey;
expect(
key == null ? null : bech32Encode('dkauthor', key),
want['author_key'],
);
final d = v.detail;
final results = sig?['signer_results'] as List?;
expect(
verdictDifferences(
r['verdicts']! as Json,
evaluateSecurity(security, context: context),
cmsParts(security),
canonical(
d == null || d.signers.isEmpty ? null : signerResults(d.signers),
),
isEmpty,
canonical(results),
);
expect(d?.foreign ?? const <SignerLine>[], isEmpty);
final sealedAt = <Instant>[
if (seal != null) parseRfc3339(str(seal, 'time')),
for (final s in (results ?? const []).cast<Json>())
if (s['result'] == 'valid') parseRfc3339(str(s, 'seal_time')),
]..sort(compareInstants);
if (seal != null) {
expect(
[d?.sealHolder, timeText(d?.sealTime)],
[seal['holder'], seal['time']],
);
}
expect(v.sealedAt, sealedAt.firstOrNull);
});
}
});

@ -0,0 +1,81 @@
// Helpers of the tests of the signatures of alg 2 and the seals of
// seal_type 2 against the vectors of Go: the areas of
// test/vectors/securitycms_vectors.json, which tool/security_go_vectors.go
// makes and evaluates with package capsule of the reference, put together
// again from their pieces, or from a base and its edit. They read no file,
// so that the tests that run on Node.js can use them.
library;
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:datekeys/src/sha256.dart' show sha256;
import 'open_vectors_support.dart';
import 'tlock_support.dart' show applyEdits;
/// The bytes of [pieces]: the hexadecimal of some bytes, or the index of a
/// chunk of [chunks].
Uint8List joinPieces(List<Object?> pieces, List<Uint8List> chunks) =>
concatBytes([
for (final p in pieces) p is int ? chunks[p] : fromHex(p! as String),
]);
/// The chunks of a vector file, each made of the ones before it.
List<Uint8List> chunksOf(Json f) {
final out = <Uint8List>[];
for (final c in (f['chunks']! as List).cast<Json>()) {
out.add(joinPieces(c['pieces']! as List<Object?>, out));
}
return out;
}
/// The bases of the file, from their pieces, or their hexadecimal in the
/// part of the file that the tests compiled to JavaScript read.
List<Uint8List> basesOf(Json f, List<Uint8List> chunks) => [
for (final b in f['bases']! as List)
b is String
? fromHex(b)
: joinPieces((b as Json)['pieces']! as List<Object?>, chunks),
];
/// The area of the case [c]: its hexadecimal, its pieces, or its base with
/// the edit of its target, the SignedData of key 2 (value), its SIGNERS
/// (signers) or the token of key 3 (token), written again with the
/// encoders of this library as Go writes it with those of capsule. The
/// first 8 bytes of its SHA-256 must be those of Go's.
Uint8List cmsAreaOf(Json c, List<Uint8List> chunks, List<Uint8List> bases) {
final Uint8List area;
final hex = c['hex'] as String?;
final pieces = c['pieces'] as List<Object?>?;
if (hex != null) {
area = fromHex(hex);
} else if (pieces != null) {
area = joinPieces(pieces, chunks);
} else {
final w = decodeSecurity(bases[c['base']! as int])!;
final edits = (c['edits']! as List).cast<Object?>();
var signature = w.signature;
var seal = w.seal;
switch (c['target']) {
case 'value' || 'signers':
final a = decodeAuthorSignature(signature!)!;
final value = c['target'] == 'value';
signature = encodeAuthorSignature(
algCms,
value ? a.key : applyEdits(a.key, edits),
value ? applyEdits(a.value, edits) : a.value,
);
case 'token':
final s = decodeSeal(seal!)!;
seal = encodeSeal(sealTypeRfc3161, applyEdits(s.token, edits));
default:
throw StateError('a target ${c['target']}');
}
area = encodeSecurityWith(signature: signature, seal: seal);
}
if (toHex(sha256(area).sublist(0, 8)) != c['sha256']) {
throw StateError('the area of ${canonical(c)} is not the one of Go');
}
return area;
}

@ -0,0 +1,264 @@
// The verdicts of a signature of alg 2 and of a seal of seal_type 2 on the
// VM and compiled to JavaScript: the part of the vectors of Go that
// test/vectors/securitycms_vectors.g.dart holds, which
// tool/security_go_vectors.go makes and evaluates with package capsule of
// the reference; the fixtures format3_signed_cms and format3_sealed opened
// to the verdicts and lines of their records; and what the API does:
// SIGNERS, the reader of CMS as the default of evaluateSecurity and of the
// opening, and Go's zero time as no time.
library;
import 'dart:convert';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart';
import 'package:datekeys/src/bech32.dart' show bech32Encode;
import 'package:test/test.dart';
import 'open_support.dart';
import 'securitycms_support.dart';
import 'vectors/securitycms_vectors.g.dart';
final Json _part = jsonDecode(securityCmsVectorsJson) as Json;
List<Json> get _cases => (_part['cases']! as List).cast<Json>();
/// The context of the vectors named [name].
SecurityContext _context(String name) {
final all = (_part['contexts']! as List).cast<Json>();
return contextsOf(_part)[all.indexWhere((c) => c['name'] == name)];
}
/// A sink of files that keeps them.
final class _Files implements FileSink {
final files = <BytesBuilder>[];
bool committed = false;
@override
void begin(Head head) =>
files.addAll([for (final _ in head.files) BytesBuilder()]);
@override
ByteSink create(int i) => _File(files[i]);
@override
void commit() => committed = true;
@override
void abort(Object reason) {}
}
final class _File implements ByteSink {
_File(this.b);
final BytesBuilder b;
@override
void add(Uint8List bytes) => b.add(bytes);
@override
void close() {}
@override
void abort(Object reason) {}
}
void main() {
test('the part of the vectors is of this spec, from its generator', () {
expect(_part['spec'], specVersion);
expect(_part['generator'], 'tool/security_go_vectors.go');
expect(_cases, hasLength(greaterThan(40)));
});
test('each case of the part, with the verdicts, the lines, the detail '
'and the earliest seal of Go', () {
final bases = basesOf(_part, const []);
final contexts = contextsOf(_part);
final texts = (_part['texts']! as List).cast<String>();
for (final c in _cases) {
expect(
evaluateDifferences(c, cmsAreaOf(c, const [], bases), contexts, texts),
isEmpty,
reason: c['name'] as String? ?? canonical(c),
);
}
});
group('the fixtures', () {
final fixtures =
((jsonDecode(securityCmsFixturesJson) as Json)['fixtures']! as List)
.cast<Json>();
for (final x in fixtures) {
test(
'${x['name']} opens to the verdicts and lines of its record',
() async {
final rel = x['release']! as Json;
final files = _Files();
final o = await openCapsule(
fromHex(str(x, 'dkc')),
OpenOptions(
source: suppliedRelease(
Release(rel['round']! as int, fromHex(str(rel, 'signature'))),
),
now: () => parseRfc3339(str(x, 'unlock_at')),
sink: files,
),
);
expect([o.ok, files.committed], [true, true], reason: '${o.error}');
final v = o.verdicts!;
expect(
verdictDifferences(x['verdicts']! as Json, v, keys: false),
isEmpty,
);
final key = v.authorKey;
expect(
key == null ? null : bech32Encode('dkauthor', key),
(x['verdicts']! as Json)['author_key'],
);
final d = v.detail!;
expect(
canonical(d.signers.isEmpty ? null : signerResults(d.signers)),
canonical(x['signer_results']),
);
final seal = x['seal'] as Json?;
expect(
canonical(
seal == null
? null
: {'holder': d.sealHolder, 'time': timeText(d.sealTime)},
),
canonical(seal),
);
},
);
}
});
group('SIGNERS', () {
final a = Uint8List(32)..[0] = 0x61;
final b = Uint8List(32)..[0] = 0x62;
test('encodeSigners sorts the hashes, as EncodeSigners of Go', () {
final x = encodeSigners([a, b]);
expect(encodeSigners([b, a]), x);
expect(x, hasLength(1 + 2 * 34));
expect(x.sublist(0, 3), [0x82, 0x58, 0x20]);
expect(maxSigners, 16);
expect(
encodeSigners(List.generate(16, (i) => Uint8List(32)..[31] = i)),
hasLength(1 + 16 * 34),
);
});
test('encodeSigners refuses what Go refuses, with its texts', () {
for (final (hashes, text) in [
(<List<int>>[], 'capsule: SIGNERS holds from 1 to 16 certificates'),
([a, a], 'capsule: SIGNERS names a certificate twice'),
(
List.generate(17, (i) => Uint8List(32)..[31] = i),
'capsule: SIGNERS holds from 1 to 16 certificates',
),
([Uint8List(31)], 'capsule: a SHA-256 of 31 bytes, want 32'),
]) {
expect(
() => encodeSigners(hashes),
throwsA(
isA<ArgumentError>().having((e) => e.message, 'message', text),
),
);
}
});
});
group('the reader of CMS', () {
test('is the default of evaluateSecurity and of the opening', () {
for (final c in _cases.take(12)) {
final area = cmsAreaOf(c, const [], basesOf(_part, const []));
final context = contextsOf(_part)[c['context']! as int];
final byDefault = evaluateSecurity(area, context: context);
final given = evaluateSecurity(area, context: context, cms: cmsReader);
expect(byDefault.lines, given.lines);
expect(
[byDefault.signature, byDefault.seal],
[given.signature, given.seal],
);
}
final options = OpenOptions(
source: suppliedRelease(Release(1, Uint8List(48))),
now: () => Instant(0),
);
expect(options.evaluator, same(evaluateSecurityInput));
});
test('reads a round time at Go\'s zero time as no round time, as '
'IsZero', () {
// A seal of key 3 before the round time: S4; without a round time, or
// at 0001-01-01T00:00:00Z, S5.
final c = _cases.firstWhere(
(c) =>
c['seal'] == 'S4' &&
((c['cms'] as List?) ?? const []).contains('seal'),
);
final area = cmsAreaOf(c, const [], basesOf(_part, const []));
final context = contextsOf(_part)[c['context']! as int];
SecurityContext at(Instant? round) => SecurityContext(
controlCommit: context.controlCommit,
headDigest: context.headDigest,
roundTime: round,
);
expect(evaluateSecurity(area, context: context).seal, Verdict.sealed);
for (final round in [null, Instant(-62135596800)]) {
final v = evaluateSecurity(area, context: at(round));
expect(v.seal, Verdict.sealedLate, reason: '$round');
expect(v.detail!.sealTime, isNotNull);
}
// A nanosecond after Go's zero time is a time: the seal of 2026 is not
// before it either.
expect(
evaluateSecurity(area, context: at(Instant(-62135596800, 1))).seal,
Verdict.sealedLate,
);
expect(_context('the same, without a round time').roundTime, isNull);
});
});
test('the earliest seal ignores Go\'s zero time, as SealedAt of Go', () {
final zero = Instant(-62135596800);
final later = parseRfc3339('2026-09-30T12:00:00Z');
SignerLine signer(Instant t) => SignerLine(
holder: 'Ana',
issuer: 'CA',
result: SignerResult.valid,
sealHolder: 'TSA',
sealTime: t,
);
expect(
Verdicts(
signature: Verdict.noSignature,
seal: Verdict.sealed,
detail: Detail(sealHolder: 'TSA', sealTime: zero),
).sealedAt,
isNull,
);
expect(
Verdicts(
signature: Verdict.signedComplete,
seal: Verdict.sealed,
detail: Detail(
signers: [signer(zero), signer(later)],
sealHolder: 'TSA',
sealTime: zero,
),
).sealedAt,
later,
);
// The line shows it all the same, as Go writes it.
expect(
Verdicts(
signature: Verdict.noSignature,
seal: Verdict.sealed,
detail: Detail(sealHolder: 'TSA', sealTime: zero),
).lines.last,
contains('existía el 0001-01-01T00:00:00Z'),
);
});
}

@ -0,0 +1,196 @@
// The verdicts of a signature of alg 2 and of a seal of seal_type 2 against
// Go, on the VM:
//
// - every case of testdata/vectors/security_cms.json, frozen by the Go
// reference at the draft v0.12, read in its context as TestCMSVectors of
// Go reads it, with its verdicts, the result of each signer, the authority
// and t of a valid seal, and its lines, byte for byte;
// - every case of test/vectors/securitycms_vectors.json, which
// tool/security_go_vectors.go makes and evaluates with package capsule of
// the reference: signers of every result, required and foreign, the
// validity of a certificate at the time of its seal, t plus the accuracy
// against the round time at the nanosecond, a seal of each kind beside a
// signature of each kind, and mutations;
// - and the part of both that securitycms_vectors.g.dart holds for the
// tests compiled to JavaScript, which must be that of the files.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'package:datekeys/datekeys.dart';
import 'package:test/test.dart';
import 'open_vectors_support.dart';
import 'security_support.dart';
import 'securitycms_support.dart';
import 'vectors/securitycms_vectors.g.dart';
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
void main() {
group('security_cms.json', () {
final f = readJson('testdata/vectors/security_cms.json');
final cases = (f['cases']! as List).cast<Json>();
test('has the 135 cases of its README, which reach every verdict but X '
'and F3', () {
expect(f['spec'], specVersion);
expect(f['description'], contains('v0.12'));
expect(cases, hasLength(135));
expect({for (final c in cases) c['name']}, hasLength(135));
final reached = {
for (final c in cases) ...[c['signature'], c['seal']],
};
expect(reached, {
'F0', 'F1', 'F2', 'F4', 'F5', 'F6', //
'S0', 'S1', 'S2', 'S3', 'S4', 'S5',
});
// Every result of a signer, and a foreign one.
final results = {
for (final c in cases)
for (final s in (c['signers'] as List? ?? const []).cast<Json>())
s['result'],
};
expect(results, {for (final r in SignerResult.values) r.code});
expect(cases.where((c) => c['foreign_signers'] != null), isNotEmpty);
});
for (final c in cases) {
test(str(c, 'name'), () {
expect(cmsVectorDifferences(c), isEmpty);
});
}
});
group('securitycms_vectors.json', () {
final f = readJson('test/vectors/securitycms_vectors.json');
final cases = (f['cases']! as List).cast<Json>();
final chunks = chunksOf(f);
final bases = basesOf(f, chunks);
final contexts = contextsOf(f);
final texts = (f['texts']! as List).cast<String>();
test('is of this spec, from its generator, with every verdict and every '
'result of a signer, required and foreign', () {
expect(f['spec'], specVersion);
expect(f['generator'], 'tool/security_go_vectors.go');
expect(cases, hasLength(greaterThan(700)));
final verdicts = {
for (final c in cases) ...[c['signature'], c['seal']],
};
expect(verdicts, containsAll(<String>['F0', 'F1', 'F2', 'F4', 'F5']));
expect(verdicts, containsAll(<String>['F6', 'S0', 'S1', 'S2', 'S3']));
expect(verdicts, containsAll(<String>['S4', 'S5']));
Set<Object?> resultsOf(String list) => {
for (final c in cases)
if (c['detail'] case final Json d)
for (final s in (d[list]! as List).cast<Json>()) s['result'],
};
final all = {for (final r in SignerResult.values) r.code};
expect(resultsOf('signers'), all);
expect(resultsOf('foreign'), all.difference({'absent'}));
// A seal before the round time and one that is not, for a signer
// and for key 3, and Go's zero time, which gives no earliest seal.
final befores = {
for (final c in cases)
if (c['detail'] case final Json d)
for (final s in (d['signers']! as List).cast<Json>()) s['before'],
};
expect(befores, {true, false});
expect(
cases.where(
(c) =>
c['seal'] == 'S4' &&
c['sealed_at'] == null &&
c['detail'] != null,
),
isNotEmpty,
);
expect(contexts.where((c) => c.roundTime == null), isNotEmpty);
});
test('every case, with the verdicts, the lines, the detail and the '
'earliest seal of Go', () {
for (final c in cases) {
final area = cmsAreaOf(c, chunks, bases);
expect(
evaluateDifferences(c, area, contexts, texts),
isEmpty,
reason: c['name'] as String? ?? canonical(c),
);
}
});
test('securitycms_vectors.g.dart holds a part of it', () {
final part = jsonDecode(securityCmsVectorsJson) as Json;
for (final k in ['spec', 'generator', 'contexts', 'texts']) {
expect(canonical(part[k]), canonical(f[k]), reason: k);
}
expect(
[for (final b in basesOf(part, const [])) toHex(b)],
[for (final b in bases) toHex(b)],
);
// Each case of the part is one of the file, in the same order, with
// the same area.
String key(Json c) => canonical({
for (final e in c.entries)
if (e.key != 'pieces' && e.key != 'hex') e.key: e.value,
});
final some = (part['cases']! as List).cast<Json>();
expect(some, hasLength(greaterThan(40)));
var at = 0;
for (final c in some) {
while (at < cases.length && key(cases[at]) != key(c)) {
at++;
}
expect(at, lessThan(cases.length), reason: key(c));
expect(
cmsAreaOf(c, const [], bases),
cmsAreaOf(cases[at], chunks, bases),
reason: key(c),
);
}
});
});
test('the fixtures of securitycms_vectors.g.dart are those of testdata/, '
'with what their records say', () {
final fixtures =
((jsonDecode(securityCmsFixturesJson) as Json)['fixtures']! as List)
.cast<Json>();
expect(
[for (final x in fixtures) x['name']],
['format3_signed_cms', 'format3_sealed'],
);
for (final x in fixtures) {
final name = str(x, 'name');
final r = readJson('testdata/fixtures/$name.json');
expect(
x['dkc'],
toHex(File('testdata/fixtures/${r['file']}').readAsBytesSync()),
reason: name,
);
for (final k in ['release', 'unlock_at', 'verdicts']) {
expect(canonical(x[k]), canonical(r[k]), reason: '$name: $k');
}
final sig = r['signature'] as Json?;
expect(
canonical(x['signer_results']),
canonical(sig?['signer_results']),
reason: name,
);
final seal = r['seal'] as Json?;
expect(
canonical(x['seal']),
canonical(
seal == null
? null
: {'holder': seal['holder'], 'time': seal['time']},
),
reason: name,
);
}
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.