Stage 1: strict DER, times included, as internal/der of Go at v0.12

lib/src/der.dart is the port of internal/der/der.go at 601e6d2, the
draft of v0.12, whose DER already differs from spec-v0.11: check, split,
content, setOfSorted and parseTime, with the texts of the reference. It
is internal, as in Go, and keeps the names of the Go package for an
import with a prefix.

parseTime reads UTCTime and GeneralizedTime in their forms of X.690
and refuses a date or a time that does not exist. It returns a DerTime,
exact to the nanosecond as Go's time.Time and unlike Dart's DateTime:
its fields and its seconds since the epoch, below 2^53.

The tests port der_test.go, with the texts that Go prints. The fuzz
target is a property over seeded mutations of its seeds and of the
signatures and tokens of security_cms.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 2 days ago
parent e2c70f50c1
commit b4316e1e19

@ -2,9 +2,10 @@
/// DateKeys Access Key (`.dkk`), as `datekeys-go` and `datekeys-ts` implement
/// them, checked against the same test data.
///
/// Stage 0 of docs/PLAN_dart.md, the package and its test data, and from
/// stage 1 the normative errors of spec §69, byte helpers and the CBOR
/// profile of spec §58. The protocol arrives stage by stage.
/// Stages 0 and 1 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. DER, internal in the Go reference, is internal here too. The protocol
/// arrives stage by stage.
library;
export 'src/bytes.dart'

@ -0,0 +1,422 @@
/// A check that bytes are one element in the Distinguished Encoding Rules of
/// X.690, as package internal/der of datekeys-go at v0.12, which spec v0.11
/// §29.10 asks of a CMS signature before it looks inside it: definite and
/// minimal lengths, no high tag numbers, no constructed form of a type that
/// DER only has primitive, canonical BOOLEAN, INTEGER, NULL, OBJECT
/// IDENTIFIER, BIT STRING, UTCTime and GeneralizedTime, and no bytes after
/// the element. A signature that two implementations read the same must not
/// depend on what a lenient reader accepts.
///
/// The order of the elements of a SET OF cannot be checked without a schema:
/// [setOfSorted] does it for the callers that know theirs.
///
/// Internal, as in Go: lib/datekeys.dart does not export it. The functions
/// keep the names of the Go package, for an import with a prefix, as
/// `import 'der.dart' as der;` and `der.check(b)`.
library;
import 'dart:typed_data';
import 'bytes.dart';
/// Bytes that are not the DER element asked for. The message says what is
/// wrong, in the words of the reference, and starts with `der: `.
final class DerException implements Exception {
/// An exception whose message is [message].
const DerException(this.message);
/// The text of the error, as Go's err.Error() of the reference.
final String message;
@override
String toString() => message;
}
const _maxDepth = 32;
/// Throws a [DerException] unless [b] is exactly one DER element.
void check(Uint8List b) {
final n = _check(b, 0, b.length, 0);
if (n != b.length) {
throw DerException('der: ${b.length - n} bytes after the element');
}
}
/// The identifier octet of the DER element [b], which must be constructed,
/// and the encodings of its children, views of [b]. It assumes [check]
/// passed.
({int id, List<Uint8List> children}) split(Uint8List b) {
if (b.isEmpty || b[0] & 0x20 == 0) {
throw const DerException('der: not a constructed element');
}
final (hl, cl) = _header(b, 0, b.length);
final end = hl + cl;
final children = <Uint8List>[];
for (var p = hl; p < end;) {
final (h, c) = _header(b, p, end);
children.add(Uint8List.sublistView(b, p, p + h + c));
p += h + c;
}
return (id: b[0], children: children);
}
/// The content octets of the DER element [b], a view of it.
Uint8List content(Uint8List b) {
final (hl, cl) = _header(b, 0, b.length);
return Uint8List.sublistView(b, hl, hl + cl);
}
/// Whether the encodings are in ascending order of their bytes, as DER
/// requires of the elements of a SET OF (X.690 11.6). Equal elements may
/// repeat, side by side: X.690 does not forbid it, and a time-stamping
/// authority may send its certificate twice. Whoever counts the elements of a
/// SET OF decides what a repetition means.
bool setOfSorted(List<List<int>> elems) {
for (var i = 1; i < elems.length; i++) {
if (compareBytes(elems[i - 1], elems[i]) > 0) return false;
}
return true;
}
/// Reads a UTCTime or a GeneralizedTime element as DER writes them (X.690
/// 11.7 and 11.8): YYMMDDHHMMSSZ, with the years 50 to 99 in the 20th century
/// (RFC 5280 4.1.2.5.1), or YYYYMMDDHHMMSS, an optional fraction of seconds
/// without a trailing zero, and Z. A date or a time that does not exist, a
/// second 60 included, is refused. [fraction] reports whether a
/// GeneralizedTime has one; digits of it beyond the nanosecond are dropped,
/// as Go drops them.
({DerTime time, bool fraction}) parseTime(Uint8List b) {
if (b.length < 2 || (b[0] != 0x17 && b[0] != 0x18)) {
throw const DerException('der: not a UTCTime or a GeneralizedTime');
}
final c = content(b);
return _parseTime(b[0], c, 0, c.length);
}
/// A time of DER, in UTC and exact to the nanosecond, as Go's time.Time of
/// the reference: Dart's DateTime holds microseconds at most.
final class DerTime implements Comparable<DerTime> {
const DerTime._(
this.year,
this.month,
this.day,
this.hour,
this.minute,
this.second,
this.nanosecond,
);
/// The year, 0 to 9999.
final int year;
/// The month, 1 to 12.
final int month;
/// The day of the month, from 1.
final int day;
/// The hour, 0 to 23.
final int hour;
/// The minute, 0 to 59.
final int minute;
/// The second, 0 to 59.
final int second;
/// The nanosecond within the second, 0 to 999 999 999.
final int nanosecond;
/// The seconds since 1970-01-01T00:00:00Z, negative before, as Go's
/// Time.Unix. Every value is below 2^53, so it is exact on every platform.
int get unixSeconds =>
_daysFromCivil(year, month, day) * 86400 +
hour * 3600 +
minute * 60 +
second;
/// Whether this time is before [other].
bool isBefore(DerTime other) => compareTo(other) < 0;
/// Whether this time is after [other].
bool isAfter(DerTime other) => compareTo(other) > 0;
@override
int compareTo(DerTime other) {
final s = unixSeconds.compareTo(other.unixSeconds);
return s != 0 ? s : nanosecond.compareTo(other.nanosecond);
}
@override
bool operator ==(Object other) =>
other is DerTime &&
other.unixSeconds == unixSeconds &&
other.nanosecond == nanosecond;
@override
int get hashCode => Object.hash(unixSeconds, nanosecond);
/// The time in RFC 3339, as Go's time.RFC3339Nano writes a time in UTC,
/// such as `2024-02-29T12:00:00.125Z`.
@override
String toString() {
String two(int v) => v.toString().padLeft(2, '0');
var fraction = '';
if (nanosecond != 0) {
var digits = nanosecond.toString().padLeft(9, '0');
while (digits.endsWith('0')) {
digits = digits.substring(0, digits.length - 1);
}
fraction = '.$digits';
}
return '${year.toString().padLeft(4, '0')}-${two(month)}-${two(day)}'
'T${two(hour)}:${two(minute)}:${two(second)}${fraction}Z';
}
}
// The days from 1970-01-01 to the date, in the proleptic Gregorian calendar
// of Go's time package (days_from_civil of Howard Hinnant).
int _daysFromCivil(int year, int month, int day) {
final y = month <= 2 ? year - 1 : year;
final era = (y >= 0 ? y : y - 399) ~/ 400;
final yearOfEra = y - era * 400;
final dayOfYear =
(153 * (month > 2 ? month - 3 : month + 9) + 2) ~/ 5 + day - 1;
final dayOfEra =
yearOfEra * 365 + yearOfEra ~/ 4 - yearOfEra ~/ 100 + dayOfYear;
return era * 146097 + dayOfEra - 719468;
}
int _daysInMonth(int year, int month) {
if (month == 2) {
final leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
return leap ? 29 : 28;
}
return const [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
}
const _bad = DerException('der: a time that is not in the form of DER');
// parseTime of Go on the content s[start:end] of an element of tag 0x17 or
// 0x18, read as bytes.
({DerTime time, bool fraction}) _parseTime(
int tag,
Uint8List s,
int start,
int end,
) {
if (end - start < 13 || s[end - 1] != 0x5a) throw _bad; // Z
// The body is s[start:bodyEnd], without the Z, and the fraction of seconds
// s[fracStart:end - 1], empty when there is none.
var bodyEnd = end - 1;
var fracStart = end - 1;
final int year;
if (tag == 0x17) {
if (bodyEnd - start != 12 || !_digits(s, start, bodyEnd)) throw _bad;
final yy = _atoi(s, start, start + 2) + 1900;
year = yy < 1950 ? yy + 100 : yy;
} else {
final dot = _indexOf(s, start, bodyEnd, 0x2e); // .
if (dot >= 0) {
fracStart = dot + 1;
if (fracStart == end - 1 ||
s[end - 2] == 0x30 ||
!_digits(s, fracStart, end - 1)) {
throw _bad;
}
bodyEnd = dot;
}
if (bodyEnd - start != 14 || !_digits(s, start, bodyEnd)) throw _bad;
year = _atoi(s, start, start + 4);
}
// MMDDHHMMSS, the last ten digits of the body.
final p = bodyEnd - 10;
final month = _atoi(s, p, p + 2);
final day = _atoi(s, p + 2, p + 4);
final hour = _atoi(s, p + 4, p + 6);
final minute = _atoi(s, p + 6, p + 8);
final second = _atoi(s, p + 8, p + 10);
if (month < 1 ||
month > 12 ||
day < 1 ||
hour > 23 ||
minute > 59 ||
second > 59 ||
day > _daysInMonth(year, month)) {
throw const DerException('der: a date or a time that does not exist');
}
final fracLength = end - 1 - fracStart;
var nanos = 0;
for (var i = 0; i < 9; i++) {
nanos *= 10;
if (i < fracLength) nanos += s[fracStart + i] - 0x30;
}
return (
time: DerTime._(year, month, day, hour, minute, second, nanos),
fraction: fracLength > 0,
);
}
// Whether s[start:end] is not empty and only ASCII digits.
bool _digits(Uint8List s, int start, int end) {
for (var i = start; i < end; i++) {
if (s[i] < 0x30 || s[i] > 0x39) return false;
}
return end > start;
}
int _atoi(Uint8List s, int start, int end) {
var n = 0;
for (var i = start; i < end; i++) {
n = n * 10 + s[i] - 0x30;
}
return n;
}
int _indexOf(Uint8List s, int start, int end, int byte) {
for (var i = start; i < end; i++) {
if (s[i] == byte) return i;
}
return -1;
}
// The length of the identifier and length octets of the element at
// b[start:end], and the length of its content, which must fit in it.
(int, int) _header(Uint8List b, int start, int end) {
if (end - start < 2) throw const DerException('der: truncated element');
if (b[start] & 0x1f == 0x1f) {
throw const DerException('der: a tag number of 31 or more');
}
final l = b[start + 1];
final int headerLen;
final int contentLen;
if (l < 0x80) {
headerLen = 2;
contentLen = l;
} else if (l == 0x80) {
throw const DerException('der: an indefinite length');
} else if (l == 0xff) {
throw const DerException('der: a length of 0xff');
} else {
final n = l & 0x7f;
if (n > 4 || end - start < 2 + n) {
throw const DerException('der: a length that does not fit');
}
if (b[start + 2] == 0) {
throw const DerException('der: a length with a leading zero');
}
// At most four bytes, below 2^32: exact without a shift.
var v = 0;
for (var i = 0; i < n; i++) {
v = v * 256 + b[start + 2 + i];
}
if (v < 0x80) {
throw const DerException('der: a long form for a length under 128');
}
headerLen = 2 + n;
contentLen = v;
}
if (contentLen > end - start - headerLen) {
throw const DerException('der: an element longer than its container');
}
return (headerLen, contentLen);
}
// Validates the element at the start of b[start:end] and returns its length.
int _check(Uint8List b, int start, int end, int depth) {
if (depth > _maxDepth) throw const DerException('der: nested too deep');
final (hl, cl) = _header(b, start, end);
final id = b[start];
final cls = id >> 6;
final constructed = id & 0x20 != 0;
final tag = id & 0x1f;
final contentStart = start + hl;
final contentEnd = contentStart + cl;
if (constructed) {
if (cls == 0 && tag != 16 && tag != 17) {
throw DerException('der: constructed form of the universal type $tag');
}
for (var p = contentStart; p < contentEnd;) {
p += _check(b, p, contentEnd, depth + 1);
}
return hl + cl;
}
if (cls == 0) _checkPrimitive(tag, b, contentStart, contentEnd);
return hl + cl;
}
// Checks the content c[start:end] of a primitive element of the universal
// class.
void _checkPrimitive(int tag, Uint8List c, int start, int end) {
final length = end - start;
switch (tag) {
case 1: // BOOLEAN
if (length != 1 || (c[start] != 0 && c[start] != 0xff)) {
throw const DerException('der: a BOOLEAN that is not 00 or FF');
}
case 2 || 10: // INTEGER, ENUMERATED
if (length == 0) throw const DerException('der: an empty INTEGER');
if (length > 1 &&
((c[start] == 0 && c[start + 1] & 0x80 == 0) ||
(c[start] == 0xff && c[start + 1] & 0x80 != 0))) {
throw const DerException('der: an INTEGER that is not minimal');
}
case 3: // BIT STRING
if (length == 0 || c[start] > 7 || (length == 1 && c[start] != 0)) {
throw const DerException('der: a malformed BIT STRING');
}
if (length > 1 &&
c[start] != 0 &&
c[end - 1] & ((1 << c[start]) - 1) != 0) {
throw const DerException(
'der: a BIT STRING with unused bits that are not zero',
);
}
case 5: // NULL
if (length != 0) throw const DerException('der: a NULL with content');
case 6: // OBJECT IDENTIFIER
if (length == 0 || c[end - 1] & 0x80 != 0) {
throw const DerException('der: a malformed OBJECT IDENTIFIER');
}
var first = true;
for (var i = start; i < end; i++) {
if (first && c[i] == 0x80) {
throw const DerException(
'der: an OBJECT IDENTIFIER with a leading 0x80 in a subidentifier',
);
}
first = c[i] & 0x80 == 0;
}
case 23 || 24: // UTCTime, GeneralizedTime
_parseTime(tag, c, start, end);
case 4 ||
7 ||
12 ||
18 ||
19 ||
20 ||
21 ||
22 ||
25 ||
26 ||
27 ||
28 ||
30:
// OCTET STRING, ObjectDescriptor and the restricted character string
// types: DER writes them primitive (X.690 10.2), with their content as
// it is. A name may hold any of them, a NumericString among them, as
// the certificates of some countries do: what is not text decides at
// the text of the name (spec §29.10), not here.
break;
case 16 || 17:
throw DerException('der: the universal type $tag in primitive form');
default:
// 0 is the end of contents of BER, and the rest are types that no
// certificate, signature or token of the profile has: REAL,
// RELATIVE-OID, TIME and the reserved tags, whose DER has rules of
// their own that this module does not check.
throw DerException(
'der: the universal type $tag, which the profile does not use',
);
}
}

@ -0,0 +1,59 @@
// Helpers of der_test.dart and der_vectors_test.dart: the walk of
// FuzzDERCheck of datekeys-go, and the mutations of a fuzzer.
import 'dart:math';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart' show concatBytes, toHex;
import 'package:datekeys/src/der.dart' as der;
import 'package:test/test.dart';
/// Checks that split, content and parseTime read the element [b], which
/// check accepted, element by element: the children of a constructed element
/// are its content exactly, a primitive one is not split, and a time of the
/// universal class is read by parseTime.
void walkAccepted(Uint8List b) {
final c = der.content(b);
if (b[0] & 0x20 == 0) {
expect(() => der.split(b), throwsA(isA<der.DerException>()));
if (b[0] == 0x17 || b[0] == 0x18) der.parseTime(b);
return;
}
final s = der.split(b);
expect(s.id, b[0]);
expect(toHex(concatBytes(s.children)), toHex(c), reason: toHex(b));
for (final child in s.children) {
walkAccepted(child);
}
}
// Bytes that matter to DER: identifiers, lengths, and the characters of a
// time.
const _boundaries = [
0x00, 0x01, 0x02, 0x04, 0x05, 0x06, 0x17, 0x18, 0x1f, 0x30, 0x31, 0x2e, //
0x5a, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0xa0, 0xff,
];
/// [b] with one to three random edits, as the mutations of a fuzzer.
Uint8List mutateDer(Random r, List<int> b) {
final out = List<int>.of(b);
for (var n = 1 + r.nextInt(3); n > 0; n--) {
final k = r.nextInt(7);
if (k == 0 && out.isNotEmpty) {
out[r.nextInt(out.length)] ^= 1 << r.nextInt(8);
} else if (k == 1 && out.isNotEmpty) {
out[r.nextInt(out.length)] = _boundaries[r.nextInt(_boundaries.length)];
} else if (k == 2 && out.isNotEmpty) {
out.length = r.nextInt(out.length);
} else if (k == 3) {
out.insert(r.nextInt(out.length + 1), r.nextInt(256));
} else if (k == 4 && out.length > 1) {
out.removeAt(r.nextInt(out.length));
} else if (k == 5 && out.isNotEmpty) {
out[r.nextInt(out.length)] = 0x30 + r.nextInt(10);
} else {
out.add(r.nextInt(256));
}
}
return Uint8List.fromList(out);
}

@ -0,0 +1,655 @@
// The strict DER of X.690 that spec §29.10 asks of a CMS signature, as
// der_test.go of datekeys-go at 601e6d2 (the draft of v0.12). Every error text
// is the one that the Go reference prints for the same input; the fuzz target
// is a property over seeded mutations of its seeds.
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart' show fromHex, toHex;
import 'package:datekeys/src/der.dart' as der;
import 'package:test/test.dart';
import 'der_support.dart';
Uint8List h(String hex) => fromHex(hex);
String zeros(int n) => '00' * n;
String hexOf(String s) => toHex(utf8.encode(s));
/// The message of the DerException that [f] throws.
String derError(void Function() f) {
try {
f();
} on der.DerException catch (e) {
return e.message;
}
fail('no DerException');
}
void main() {
test('check accepts exactly the DER of the profile', () {
// The cases of TestCheck: null for DER, and the text of the reference
// for the rest.
final cases = <(String, String, String?)>[
(
'sequence of an integer and a null',
'3005020101'
'0500',
null,
),
(
'a long length',
'04'
'8180${zeros(128)}',
null,
),
(
'a long form under 128',
'0481'
'01'
'00',
'der: a long form for a length under 128',
),
(
'a length with a leading zero',
'04820001'
'00',
'der: a length with a leading zero',
),
(
'a length of 128 with a leading zero',
'04820080${zeros(128)}',
'der: a length with a leading zero',
),
('an indefinite length', '30800000', 'der: an indefinite length'),
('a length of 0xff', '04ff${zeros(127)}', 'der: a length of 0xff'),
(
'a length of five bytes',
'0485'
'0100000000'
'00',
'der: a length that does not fit',
),
(
'a length of nine bytes that wraps around to 128',
'0489'
'010000000000000080${zeros(128)}',
'der: a length that does not fit',
),
(
'a length whose bytes are missing',
'0482'
'01',
'der: a length that does not fit',
),
('a lone identifier octet', '04', 'der: truncated element'),
('nothing', '', 'der: truncated element'),
('a high tag number', '1f0100', 'der: a tag number of 31 or more'),
('truncated', '0402aa', 'der: an element longer than its container'),
(
'trailing bytes',
'0500'
'00',
'der: 1 bytes after the element',
),
('BOOLEAN 01', '010101', 'der: a BOOLEAN that is not 00 or FF'),
('BOOLEAN 00', '010100', null),
('BOOLEAN FF', '0101ff', null),
(
'BOOLEAN of two bytes',
'0102ffff',
'der: a BOOLEAN that is not 00 or FF',
),
(
'INTEGER with a leading zero',
'02020001',
'der: an INTEGER that is not minimal',
),
('INTEGER 0x80 with its zero', '02020080', null),
(
'INTEGER with a leading FF',
'0202ff80',
'der: an INTEGER that is not minimal',
),
(
'INTEGER -1 in two bytes',
'0202ffff',
'der: an INTEGER that is not minimal',
),
('INTEGER -129', '0202ff7f', null),
('empty INTEGER', '0200', 'der: an empty INTEGER'),
(
'ENUMERATED with a leading zero',
'0a020001',
'der: an INTEGER that is not minimal',
),
('NULL with content', '050100', 'der: a NULL with content'),
(
'constructed OCTET STRING',
'2404'
'0402aabb',
'der: constructed form of the universal type 4',
),
(
'constructed INTEGER',
'2203'
'020101',
'der: constructed form of the universal type 2',
),
(
'BIT STRING with unused bits set',
'03020701',
'der: a BIT STRING with unused bits that are not zero',
),
('BIT STRING with unused bits clear', '03020780', null),
('BIT STRING of 4 bits', '030204f0', null),
('an empty BIT STRING', '030100', null),
(
'a BIT STRING without its first octet',
'0300',
'der: a malformed BIT STRING',
),
(
'a BIT STRING of eight unused bits',
'03020800',
'der: a malformed BIT STRING',
),
(
'a BIT STRING of no bits with unused bits',
'030101',
'der: a malformed BIT STRING',
),
('OID', '06032a0304', null),
(
'OID with 0x80 inside a subidentifier',
'0604'
'2a818001',
null,
),
(
'OID with a leading 0x80',
'06038001'
'02',
'der: an OBJECT IDENTIFIER with a leading 0x80 in a subidentifier',
),
(
'OID that does not end',
'06022a83',
'der: a malformed OBJECT IDENTIFIER',
),
('an empty OID', '0600', 'der: a malformed OBJECT IDENTIFIER'),
('context tag, constructed', 'a003020101', null),
('context tag, primitive, any content', '8003000000', null),
(
'SET in primitive form',
'1100',
'der: the universal type 17 in primitive form',
),
(
'SEQUENCE in primitive form',
'1000',
'der: the universal type 16 in primitive form',
),
(
'the end of contents',
'0000',
'der: the universal type 0, which the profile does not use',
),
(
'a reserved universal tag',
'0e0141',
'der: the universal type 14, which the profile does not use',
),
(
'a SEQUENCE of the end of contents',
'30020000',
'der: the universal type 0, which the profile does not use',
),
(
'a SEQUENCE with a bad child',
'3003'
'020000',
'der: an empty INTEGER',
),
(
'a SEQUENCE whose child does not fit',
'3003'
'040500',
'der: an element longer than its container',
),
('UTF8String', '0c026162', null),
('UTF8String that is not UTF-8', '0c01ff', null),
('PrintableString with an underscore', '13015f', null),
('TeletexString', '1401e9', null),
('IA5String', '160161', null),
('VisibleString', '1a0161', null),
('UniversalString', '1c0400000061', null),
('BMPString', '1e020061', null),
// The other string types are DER too, whatever their content: a name
// may hold a NumericString, as the INN of a Russian certificate.
('NumericString', '1204${hexOf('1234')}', null),
('NumericString with a letter', '1201${hexOf('A')}', null),
('VideotexString', '150141', null),
('GraphicString', '190141', null),
('GeneralString', '1b0141', null),
('ObjectDescriptor', '070141', null),
(
'NumericString in constructed form',
'3203'
'120131',
'der: constructed form of the universal type 18',
),
(
'REAL',
'0900',
'der: the universal type 9, which the profile does not use',
),
(
'RELATIVE-OID',
'0d0101',
'der: the universal type 13, which the profile does not use',
),
// Times in the forms of DER (X.690 11.7, 11.8).
('UTCTime', '170d${hexOf('250101120000Z')}', null),
(
'an empty UTCTime',
'1700',
'der: a time that is not in the form of DER',
),
(
'UTCTime without seconds',
'170b${hexOf('2501011200Z')}',
'der: a time that is not in the form of DER',
),
(
'UTCTime with a digit more',
'170e${hexOf('2501011200001Z')}',
'der: a time that is not in the form of DER',
),
(
'UTCTime with an offset',
'1711${hexOf('250101120000+0100')}',
'der: a time that is not in the form of DER',
),
(
'UTCTime of 30 February',
'170d${hexOf('250230120000Z')}',
'der: a date or a time that does not exist',
),
(
'UTCTime with second 60',
'170d${hexOf('250101235960Z')}',
'der: a date or a time that does not exist',
),
(
'a UTCTime that is not a time',
'170a${hexOf('not a time')}',
'der: a time that is not in the form of DER',
),
(
'UTCTime with a slash in its seconds',
'170d${hexOf('2501011200/0Z')}',
'der: a time that is not in the form of DER',
),
(
'UTCTime with a colon in its day',
'170d${hexOf('25010:120000Z')}',
'der: a time that is not in the form of DER',
),
('GeneralizedTime', '180f${hexOf('20250101120000Z')}', null),
(
'GeneralizedTime with a fraction',
'1812${hexOf('20250101120000.25Z')}',
null,
),
(
'GeneralizedTime with a trailing zero',
'1813${hexOf('20250101120000.250Z')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with an empty fraction',
'1810${hexOf('20250101120000.Z')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with a letter in its fraction',
'1812${hexOf('20250101120000.2aZ')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime without Z',
'180e${hexOf('20250101120000')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with a comma',
'1812${hexOf('20250101120000,25Z')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime without seconds',
'180d${hexOf('202501011200Z')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with a slash in its seconds',
'180f${hexOf('202501011200/0Z')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime of month 0',
'180f${hexOf('20250001120000Z')}',
'der: a date or a time that does not exist',
),
(
'GeneralizedTime of month 13',
'180f${hexOf('20251301120000Z')}',
'der: a date or a time that does not exist',
),
(
'GeneralizedTime of day 0',
'180f${hexOf('20250100120000Z')}',
'der: a date or a time that does not exist',
),
(
'GeneralizedTime of 31 April',
'180f${hexOf('20250431120000Z')}',
'der: a date or a time that does not exist',
),
(
'GeneralizedTime of hour 24',
'180f${hexOf('20250101240000Z')}',
'der: a date or a time that does not exist',
),
(
'GeneralizedTime of minute 60',
'180f${hexOf('20250101126000Z')}',
'der: a date or a time that does not exist',
),
(
'GeneralizedTime of 29 February 2024',
'180f${hexOf('20240229235959Z')}',
null,
),
// What a check that is missing would let through: the last byte read
// as Z, a colon read as the digit 10, a slash read as a year, a tag of
// a high number read as one byte, and an indefinite length read as a
// long form.
(
'UTCTime that ends in another letter',
'170d${hexOf('250101120000X')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with a digit in the place of Z',
'180f${hexOf('202501011200000')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with a colon in its day',
'180f${hexOf('2025010:120000Z')}',
'der: a time that is not in the form of DER',
),
(
'GeneralizedTime with a slash in its year',
'180f${hexOf('/0250101120000Z')}',
'der: a time that is not in the form of DER',
),
(
'a context tag of a high number',
'9f0100',
'der: a tag number of 31 or more',
),
(
'an indefinite length and nothing after it',
'3080',
'der: an indefinite length',
),
];
for (final (name, hex, text) in cases) {
final b = h(hex);
if (text == null) {
expect(() => der.check(b), returnsNormally, reason: name);
} else {
expect(derError(() => der.check(b)), text, reason: name);
}
}
});
test(
'the elements of a SET OF go in ascending order, equal ones may repeat',
() {
final a = [0x02, 0x01, 0x01];
final b = [0x02, 0x01, 0x02];
expect(der.setOfSorted([a, b]), isTrue);
expect(der.setOfSorted([b, a]), isFalse);
expect(der.setOfSorted([a, a, b]), isTrue);
expect(der.setOfSorted([a, b, a]), isFalse);
expect(der.setOfSorted([]), isTrue);
},
);
test('elements nested 32 levels below the outer one are DER, 33 are not', () {
Uint8List nested(int n) {
var b = [0x05, 0x00];
for (var i = 0; i < n; i++) {
b = [0x30, b.length, ...b];
}
return Uint8List.fromList(b);
}
der.check(nested(32));
expect(derError(() => der.check(nested(33))), 'der: nested too deep');
});
test('parseTime reads the times of DER exactly to the nanosecond', () {
// Seconds since the epoch, nanoseconds and the form of RFC 3339, as Go's
// Time.Unix, Time.Nanosecond and RFC3339Nano print them.
final cases = <(String, int, int, bool, String)>[
(
'\x17\x0d'
'491231235959Z',
2524607999,
0,
false,
'2049-12-31T23:59:59Z',
),
(
'\x17\x0d'
'500101000000Z',
-631152000,
0,
false,
'1950-01-01T00:00:00Z',
),
(
'\x18\x13'
'20240229120000.125Z',
1709208000,
125000000,
true,
'2024-02-29T12:00:00.125Z',
),
(
'\x18\x19'
'20240229120000.123456789Z',
1709208000,
123456789,
true,
'2024-02-29T12:00:00.123456789Z',
),
(
'\x18\x0f'
'19490101000000Z',
-662688000,
0,
false,
'1949-01-01T00:00:00Z',
),
// Digits beyond the nanosecond are dropped, as Go drops them.
(
'\x18\x1b'
'20240229120000.12345678912Z',
1709208000,
123456789,
true,
'2024-02-29T12:00:00.123456789Z',
),
// The proleptic Gregorian calendar of Go: the year 0 is a leap year.
(
'\x18\x0f'
'00000229120000Z',
-62162078400,
0,
false,
'0000-02-29T12:00:00Z',
),
(
'\x18\x0f'
'99991231235959Z',
253402300799,
0,
false,
'9999-12-31T23:59:59Z',
),
];
for (final (el, seconds, nanos, fraction, text) in cases) {
final t = der.parseTime(Uint8List.fromList(el.codeUnits));
expect(t.time.unixSeconds, seconds, reason: text);
expect(t.time.nanosecond, nanos, reason: text);
expect(t.fraction, fraction, reason: text);
expect('${t.time}', text);
}
final bad = <(String, String)>[
(
'\x18\x0f'
'20230229120000Z',
'der: a date or a time that does not exist',
),
(
'\x04\x0d'
'491231235959Z',
'der: not a UTCTime or a GeneralizedTime',
),
// A GeneralizedTime in an OCTET STRING, a UTCTime in a UTF8String.
(
'\x04\x0f'
'20230228120000Z',
'der: not a UTCTime or a GeneralizedTime',
),
(
'\x0c\x0d'
'491231235959Z',
'der: not a UTCTime or a GeneralizedTime',
),
('\x17', 'der: not a UTCTime or a GeneralizedTime'),
// Its content does not fit.
(
'\x17\x0d'
'4912',
'der: an element longer than its container',
),
('', 'der: not a UTCTime or a GeneralizedTime'),
];
for (final (el, text) in bad) {
expect(
derError(() => der.parseTime(Uint8List.fromList(el.codeUnits))),
text,
);
}
});
test('times compare in order, to the nanosecond', () {
der.DerTime t(String s) => der
.parseTime(Uint8List.fromList([0x18, s.length, ...s.codeUnits]))
.time;
final a = t('20240229120000.1Z');
final b = t('20240229120000.100000001Z');
final c = t('20240229120001Z');
expect(a.isBefore(b), isTrue);
expect(b.isBefore(c), isTrue);
expect(c.isAfter(a), isTrue);
expect(a.compareTo(a), 0);
expect(a, t('20240229120000.1Z'));
expect(a, isNot(b));
// A UTCTime and a GeneralizedTime of the same instant are the same time.
final utc = der.parseTime(h('170d${hexOf('491231235959Z')}')).time;
expect(utc, t('20491231235959Z'));
expect(utc.hashCode, t('20491231235959Z').hashCode);
});
test('split and content read the children and the content', () {
final b = h('30050201010500');
der.check(b);
final s = der.split(b);
expect(s.id, 0x30);
expect(s.children.map(toHex), ['020101', '0500']);
expect(toHex(der.content(b)), '0201010500');
// Nothing, a primitive element, and constructed ones whose content, or a
// child, does not fit.
final bad = <(String, String)>[
('', 'der: not a constructed element'),
('0400', 'der: not a constructed element'),
('3005', 'der: an element longer than its container'),
('a005', 'der: an element longer than its container'),
('300304050000', 'der: an element longer than its container'),
];
for (final (hex, text) in bad) {
expect(derError(() => der.split(h(hex))), text, reason: hex);
}
expect(
derError(() => der.content(h('040500'))),
'der: an element longer than its container',
);
expect(derError(() => der.content(h('04'))), 'der: truncated element');
});
test('a child that does not fit is an error of split, which returns', () {
// The loop over the children advances by each header: a child that does
// not fit must end it, or it would never end.
expect(
derError(() => der.split(h('3003040500'))),
'der: an element longer than its container',
);
});
test('what check accepts, split, content and parseTime read', () {
// FuzzDERCheck over seeded mutations of its seeds: the children of a
// constructed element are its content exactly, a primitive one is not
// split, and a time of the universal class is read by parseTime.
final seeds = [
for (final s in [
'30050201010500',
'a003020101',
'0603'
'2a0304',
'170d${hexOf('250101120000Z')}',
'1812${hexOf('20250101120000.25Z')}',
'30800000',
'0489'
'010000000000000080',
'3010'
'170d${hexOf('491231235959Z')}'
'0101ff',
])
h(s),
];
final r = Random(7);
var accepted = 0;
for (var i = 0; i < 20000; i++) {
final b = mutateDer(r, seeds[r.nextInt(seeds.length)]);
try {
der.check(b);
} on der.DerException {
continue;
}
accepted++;
walkAccepted(b);
}
expect(accepted, greaterThan(100));
});
}

@ -0,0 +1,74 @@
// The DER elements of the security areas of security_cms.json, the
// signatures and the tokens, as the seeds of FuzzDERCheck of datekeys-go:
// check accepts each, split, content and parseTime read it element by
// element, and so for every mutation of them that check accepts.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:datekeys/datekeys.dart' show fromHex;
import 'package:datekeys/src/der.dart' as der;
import 'package:test/test.dart';
import 'der_support.dart';
/// The outermost runs of bytes of [b] that are one DER SEQUENCE of more than
/// 127 bytes each, as derElements of der_test.go.
List<Uint8List> derElements(Uint8List b) {
final out = <Uint8List>[];
for (var i = 0; i + 2 < b.length; i++) {
if (b[i] != 0x30 || b[i + 1] < 0x81 || b[i + 1] > 0x83) continue;
final rest = Uint8List.sublistView(b, i);
final Uint8List element;
try {
final content = der.content(rest);
final headerLength = content.offsetInBytes - rest.offsetInBytes;
element = Uint8List.sublistView(b, i, i + headerLength + content.length);
der.check(element);
} on der.DerException {
continue;
}
out.add(element);
i += element.length - 1;
}
return out;
}
void main() {
final file = jsonDecode(
File('testdata/vectors/security_cms.json').readAsStringSync(),
) as Map<String, Object?>;
final areas = [
for (final c in (file['cases']! as List).cast<Map<String, Object?>>())
fromHex(c['security_cbor']! as String),
];
final seeds = [for (final a in areas) ...derElements(a)];
test('the signatures and tokens of security_cms.json are DER', () {
expect(areas, isNotEmpty);
// Some cases hold no DER on purpose: a signature that is not a CMS, a
// seal that is not DER, SIGNERS out of order or empty.
expect(seeds, isNotEmpty);
seeds.forEach(walkAccepted);
});
test('what check accepts of their mutations, the readers read', () {
final r = Random(8);
var accepted = 0;
for (var i = 0; i < 3000; i++) {
final b = mutateDer(r, seeds[r.nextInt(seeds.length)]);
try {
der.check(b);
} on der.DerException {
continue;
}
accepted++;
walkAccepted(b);
}
expect(accepted, greaterThan(0));
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.