You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/test/der_test.dart

656 lines
19 KiB

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

Powered by TurnKey Linux.