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