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
parent
e2c70f50c1
commit
b4316e1e19
@ -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…
Reference in new issue