Stage 4b: PUBLIC_HEADER, CONTROL_CBOR of the three formats and the .dkk

lib/src/header.dart, control.dart and accesskey.dart port DecodeHeader,
EncodeHeader, DecodeControl and EncodeControl of package capsule and
package accesskey of datekeys-go at c531e93, in the layers of spec §69.1
and with the texts of Go: the limit of the frame, the type tag and the
schema version, the CBOR profile with the re-encoding and the CDDL, keys 6
and 7 of CONTROL_CBOR versions 2 and 3, and only then the DateKey of the
header and access_type and access_material of the .dkk. The .dkk is
written with the rule of spec §72 for the extensions of the specification
and read back before it is returned, as MarshalBody. I_PAYLOAD and
access_material are copied once and wiped on every path; their objects
print without them. The access policy is the enum AccessPolicy.

The tests read every fixture of testdata/: the PRELUDE, the sections and
header_binding of the 24 capsules, their header and control decoded and
written back, the round time of their DateKey, P, and in format 3 the frame
of BODY, the area, the head and the files; the six .dkk with their
capsule_digest. And the shared vectors dk1.json, quicknet_rounds.json,
profile_quicknet.json, padding.json and the 172 schemas of cbor.json,
which stage 1 left aside, each decoded by its schema and written back.

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

@ -0,0 +1,342 @@
/// The DateKeys Access Key, the portable .dkk credential (spec §38, §40 to
/// §44), as package accesskey of datekeys-go and accesskey.ts of
/// datekeys-ts: the same layers of spec §69.1 in the same order, with the
/// same codes and texts.
///
/// A .dkk is a sensitive capability (spec §7.4): its X25519 identity is kept
/// as 32 raw bytes in [AccessKey.material]. Nothing here prints it, every
/// copy that decoding makes is wiped, and [AccessKey.wipe] wipes the one it
/// returns.
library;
import 'dart:typed_data';
import 'bytes.dart';
import 'cbor.dart';
import 'errors.dart';
import 'extension.dart';
import 'framing.dart';
import 'schema.dart';
/// The type tag of BODY_CBOR (spec §41).
const accessKeyTypeTag = 'datekeys-access-key';
/// The schema version of BODY_CBOR (spec §41).
const accessKeyVersion = 1;
/// The only access_type of V1 (spec §41).
const accessTypeX25519 = 'x25519';
const _idSize = 16;
const _digestSize = 32;
const _x25519Size = 32;
/// verification_metadata (spec §43): a fast failure for a wrong file and
/// help for the interface, never a security property.
final class Verification {
/// The metadata of [capsuleDigest].
Verification(this.capsuleDigest);
/// Key 0, capsule_digest: the SHA-256 of the exact bytes of the .dkc.
final Uint8List capsuleDigest;
@override
bool operator ==(Object other) =>
other is Verification && equalBytes(other.capsuleDigest, capsuleDigest);
@override
int get hashCode => Object.hashAll(capsuleDigest);
}
/// A decoded .dkk (spec §41).
final class AccessKey {
/// A .dkk of the fields of keys 2 to 8.
AccessKey({
required this.credentialId,
required this.capsuleId,
required this.type,
required this.material,
this.verification,
this.critical = const [],
this.noncritical = const [],
});
/// Key 2, credential_id: 16 random and opaque bytes (spec §42).
final Uint8List credentialId;
/// Key 3, the capsule_id of the only capsule this credential is for
/// (spec §38).
final Uint8List capsuleId;
/// Key 4, access_type.
final String type;
/// Key 5, access_material: the 32 raw bytes of an X25519 identity. A
/// secret.
final Uint8List material;
/// Key 6, verification_metadata, null when absent (spec §43, §58.1).
final Verification? verification;
/// Key 7, critical_extensions.
final List<Extension> critical;
/// Key 8, noncritical_extensions.
final List<Extension> noncritical;
/// Overwrites the material with zeros.
void wipe() => material.fillRange(0, material.length, 0);
/// The key without its material, as String of Go.
@override
String toString() =>
'AccessKey{credential_id=${toHex(credentialId)} '
'capsule_id=${toHex(capsuleId)} type=$type material=REDACTED}';
}
/// Checks access_type and access_material of [k], as Identity of Go does
/// before it builds the X25519 identity: ERR_ACCESS_INVALID unless they are
/// `x25519` and 32 bytes. [decodeAccessKey] has checked those of the keys it
/// returns.
void checkAccessKeyMaterial(AccessKey k) =>
_validateMaterial(k.type, k.material);
void _validateMaterial(String type, Uint8List material) {
if (type != accessTypeX25519) {
throw DateKeysException(
ErrorCode.accessInvalid,
'accesskey: access_type ${goQuote(utf8Bytes(type))} is not supported '
'by V1',
);
}
if (material.length != _x25519Size) {
throw DateKeysException(
ErrorCode.accessInvalid,
'accesskey: x25519 access_material is ${material.length} bytes, want '
'$_x25519Size',
);
}
}
// BODY_CBOR as it is encoded: keys 2 to 8, keys 0 and 1 being the constants
// accessKeyTypeTag and accessKeyVersion.
final class _Wire {
Uint8List credentialId = Uint8List(0);
Uint8List capsuleId = Uint8List(0);
String accessType = '';
// A secret.
Uint8List material = Uint8List(0);
// capsule_digest, the only key of verification_metadata (key 6); null
// when key 6 is omitted.
Uint8List? digest;
List<Extension> critical = const [];
List<Extension> noncritical = const [];
}
DateKeysException _nonCanonical(String context) =>
DateKeysException(ErrorCode.nonCanonicalCbor, context);
// Reads BODY_CBOR with every rule of the CDDL whose violation is
// ERR_NON_CANONICAL_CBOR (layer 3 of spec §69.1); access_type and
// access_material, which have a code of their own (spec §57), are checked
// afterwards. The caller wipes w.material, whatever the result.
void _decodeWire(CborDecoder d, _Wire w) {
final pairs = d.map(9);
final seen = <int>{};
for (var i = 0; i < pairs; i++) {
final k = d.key();
switch (k) {
case 0:
inKey(k, () => d.text(accessKeyTypeTag.length));
case 1:
inKey(k, () => d.uint(accessKeyVersion));
case 2:
w.credentialId = inKey(k, () => d.bstr(_idSize, _idSize));
case 3:
w.capsuleId = inKey(k, () => d.bstr(_idSize, _idSize));
case 4:
w.accessType = inKey(k, () => d.text(maxDkkBodyLen));
case 5:
w.material = inKey(k, () => d.bstr(0, maxDkkBodyLen));
case 6:
w.digest = inKey(k, () => _decodeVerification(d));
case 7:
w.critical = inKey(k, () => decodeExtensionArray(d));
case 8:
w.noncritical = inKey(k, () => decodeExtensionArray(d));
default:
throw keyNotDefined(k);
}
seen.add(k as int);
}
requireKeys(seen, 6);
d.endMap();
}
// Reads verification_metadata, {0: capsule_digest}. It is present only when
// it holds a digest: an empty map does not stand for absence (spec §43,
// §58.1).
Uint8List _decodeVerification(CborDecoder d) {
final pairs = d.map(1);
if (pairs == 0) {
throw _nonCanonical(
'empty verification_metadata; an absent one omits key 6',
);
}
final k = d.key();
if (k != 0) {
throw _nonCanonical('verification_metadata key $k is not defined');
}
final digest = withContext(
'capsule_digest',
() => d.bstr(_digestSize, _digestSize),
);
d.endMap();
return digest;
}
void _encodeWire(CborEncoder e, _Wire w) {
final digest = w.digest;
e
..map(
6 +
(digest == null ? 0 : 1) +
presence(w.critical) +
presence(w.noncritical),
)
..uint(0)
..text(accessKeyTypeTag)
..uint(1)
..uint(accessKeyVersion)
..uint(2)
..bstr(w.credentialId)
..uint(3)
..bstr(w.capsuleId)
..uint(4)
..text(w.accessType)
..uint(5)
..bstr(w.material);
if (digest != null) {
e
..uint(6)
..map(1)
..uint(0)
..bstr(digest);
}
encodeExtensionArrays(e, 7, w.critical, w.noncritical);
}
DateKeysException _tooLong(int n) => DateKeysException(
ErrorCode.integrity,
'accesskey: BODY_CBOR of $n bytes exceeds $maxDkkBodyLen',
);
/// Validates and decodes BODY_CBOR, which the limit of the body of a .dkk
/// of spec §57 bounds whatever it was read from, in the layers of spec
/// §69.1, as DecodeBody of Go: the limit (ERR_INTEGRITY); the type tag and
/// the schema version; the CBOR profile, the re-encoding and the CDDL,
/// verification_metadata and the extension arrays included, and no
/// extension_id in both arrays (ERR_NON_CANONICAL_CBOR); and only then
/// access_type and access_material (ERR_ACCESS_INVALID). Whether its
/// critical extensions are known is decided by the consumer (spec §63 step
/// 9.a). The caller wipes the key it gets.
AccessKey decodeAccessKeyBody(List<int> body) {
if (body.length > maxDkkBodyLen) throw _tooLong(body.length);
final w = _Wire();
try {
withContext('accesskey', () {
checkSchema(body, accessKeyTypeTag, accessKeyVersion);
unmarshalCbor(body, (d) => _decodeWire(d, w), (e) => _encodeWire(e, w));
checkDisjoint(w.critical, w.noncritical);
});
_validateMaterial(w.accessType, w.material);
final digest = w.digest;
return AccessKey(
credentialId: w.credentialId,
capsuleId: w.capsuleId,
type: w.accessType,
material: Uint8List.fromList(w.material),
verification: digest == null ? null : Verification(digest),
critical: List.unmodifiable(w.critical),
noncritical: List.unmodifiable(w.noncritical),
);
} finally {
w.material.fillRange(0, w.material.length, 0);
}
}
/// Decodes exactly one .dkk, as Decode of Go: its frame ([splitAccessKey]),
/// its canonical body and its fields ([decodeAccessKeyBody]). Whether its
/// critical extensions are known is decided by the consumer.
AccessKey decodeAccessKey(List<int> dkk) =>
decodeAccessKeyBody(splitAccessKey(dkk));
/// BODY_CBOR of [k] (spec §41), after checking that [decodeAccessKeyBody]
/// accepts it (spec §72), as MarshalBody of Go. The extensions of [k] are
/// written in canonical order once they pass the rules of spec §54, and the
/// extensions of the specification only where spec §72 registers them
/// ([checkWrite] with [StandardExtensions]): datekeys.capsule in the
/// noncritical array, with data, which is the locator's and is not checked
/// here, and never datekeys.note. The caller wipes the result.
Uint8List marshalAccessKeyBody(AccessKey k) {
_validateMaterial(k.type, k.material);
if (k.credentialId.length != _idSize || k.capsuleId.length != _idSize) {
throw _nonCanonical(
'accesskey: credential_id and capsule_id must be $_idSize bytes',
);
}
final verification = k.verification;
if (verification != null &&
verification.capsuleDigest.length != _digestSize) {
// An empty map does not stand for absence (spec §43).
throw _nonCanonical('accesskey: capsule_digest must be $_digestSize bytes');
}
final w = _Wire()
..credentialId = k.credentialId
..capsuleId = k.capsuleId
..accessType = k.type
..material = k.material
..digest = verification?.capsuleDigest
..critical = canonicalExtensions(k.critical)
..noncritical = canonicalExtensions(k.noncritical);
checkDisjoint(w.critical, w.noncritical);
// Spec §72: datekeys.capsule goes only in the noncritical array of a .dkk,
// and datekeys.note never in a .dkk.
for (final (arr, exts) in [
(ExtensionArray.critical, w.critical),
(ExtensionArray.noncritical, w.noncritical),
]) {
checkWrite(
const StandardExtensions(),
ExtensionObject.accessKey,
arr,
exts,
context: 'accesskey',
);
}
final e = CborEncoder();
_encodeWire(e, w);
final b = e.out();
if (b.length > maxDkkBodyLen) {
b.fillRange(0, b.length, 0);
throw _tooLong(b.length);
}
// The self-check of spec §72: the reader accepts what is written.
try {
decodeAccessKeyBody(b).wipe();
} on DateKeysException catch (err) {
b.fillRange(0, b.length, 0);
throw err.wrap('accesskey: self-check: the reader rejects this body');
}
return b;
}
/// A whole .dkk: the prelude of 12 bytes and BODY_CBOR (spec §40), as
/// Encode of Go. The body it encodes is wiped once copied; the caller wipes
/// the result.
Uint8List encodeAccessKey(AccessKey k) {
final body = marshalAccessKeyBody(k);
final out = concatBytes([dkkPreludeBytes(body.length), body]);
body.fillRange(0, body.length, 0);
return out;
}

@ -0,0 +1,269 @@
/// CONTROL_CBOR (spec §31, §63 step 14), as DecodeControl and EncodeControl
/// of package capsule of datekeys-go and control.ts of datekeys-ts, schema
/// versions 1 to 3: the same layers of spec §69.1 in the same order, with the
/// same codes and texts. Its schema version is the format of its capsule,
/// which is not part of [Control]: [decodeControl] and [encodeControl]
/// receive it.
///
/// payload_identity is I_PAYLOAD, a secret: every copy that decoding makes
/// is wiped, and [Control.wipe] wipes the one it returns.
library;
import 'dart:typed_data';
import 'big_endian.dart';
import 'bytes.dart';
import 'cbor.dart';
import 'errors.dart';
import 'extension.dart';
import 'framing.dart';
import 'padding.dart';
import 'schema.dart';
/// The type tag of CONTROL_CBOR (spec §31).
const controlTypeTag = 'datekeys-control';
// payload_length has a fixed size, so that the length of CONTROL_CBOR never
// depends on L (spec §31, §55.2).
const _payloadLengthSize = 8;
/// CONTROL_CBOR (spec §31).
final class Control {
/// A control of the fields of keys 2 to 7. [payloadLength] and [padding]
/// exist in formats 2 and 3 only, and are null in format 1.
Control({
required this.headerBinding,
required this.payloadIdentity,
this.critical = const [],
this.noncritical = const [],
this.payloadLength,
this.padding,
});
/// Key 2, header_binding: SHA-256(PRELUDE || PUBLIC_HEADER), 32 bytes.
final Uint8List headerBinding;
/// Key 3, payload_identity: the 32 raw bytes of the X25519 identity
/// I_PAYLOAD. A secret.
final Uint8List payloadIdentity;
/// Key 4, critical_extensions.
final List<Extension> critical;
/// Key 5, noncritical_extensions.
final List<Extension> noncritical;
/// Key 6, L: the length of the content, at most L_MAX; in format 3, the
/// length of BODY. Formats 2 and 3 only (spec §29.1, §29.2, §31).
final int? payloadLength;
/// Key 7, the padding rule of PAYLOAD_AGE. Formats 2 and 3 only.
final PaddingRule? padding;
/// Overwrites I_PAYLOAD with zeros.
void wipe() => payloadIdentity.fillRange(0, payloadIdentity.length, 0);
/// The control without I_PAYLOAD, as String of Go.
@override
String toString() =>
'Control{header_binding=${toHex(headerBinding)} '
'payload_identity=REDACTED payload_length=${payloadLength ?? 0} '
'padding=${padding?.code ?? 0}}';
}
// CONTROL_CBOR as it is encoded: keys 2 to 7, keys 0 and 1 being the
// constant controlTypeTag and the format.
final class _Wire {
_Wire(this.format);
// Key 1, the schema version.
final CapsuleFormat format;
Uint8List headerBinding = Uint8List(0);
// A secret.
Uint8List payloadIdentity = Uint8List(0);
List<Extension> critical = const [];
List<Extension> noncritical = const [];
// Key 6, formats 2 and 3 only: 8 bytes, big-endian.
Uint8List? payloadLength;
// Key 7, formats 2 and 3 only.
int? padding;
}
DateKeysException _nonCanonical(String context) =>
DateKeysException(ErrorCode.nonCanonicalCbor, context);
// L as read, at most L_MAX, and reported exactly when it is not, whatever
// its 64 bits.
Uint8List _checkPayloadLength(Uint8List b) {
final l = readUint64BE(b, 0);
if (l is! int || l > maxPayloadLength) {
throw _nonCanonical('payload_length $l exceeds L_MAX = $maxPayloadLength');
}
return b;
}
// Reads CONTROL_CBOR with every rule of the CDDL of the schema version of
// w.format: keys 6 and 7 are required in versions 2 and 3 and not defined in
// version 1. The caller wipes w.payloadIdentity, whatever the result.
void _decodeWire(CborDecoder d, _Wire w) {
final padded = w.format.isPadded;
final pairs = d.map(padded ? 8 : 6);
final seen = <int>{};
for (var i = 0; i < pairs; i++) {
final k = d.key();
if ((k == 6 || k == 7) && !padded) throw keyNotDefined(k);
switch (k) {
case 0:
inKey(k, () => d.text(controlTypeTag.length));
case 1:
inKey(k, () => d.uint(w.format.version));
case 2:
w.headerBinding = inKey(k, () => d.bstr(32, 32));
case 3:
w.payloadIdentity = inKey(k, () => d.bstr(32, 32));
case 4:
w.critical = inKey(k, () => decodeExtensionArray(d));
case 5:
w.noncritical = inKey(k, () => decodeExtensionArray(d));
case 6:
w.payloadLength = inKey(
k,
() => _checkPayloadLength(
d.bstr(_payloadLengthSize, _payloadLengthSize),
),
);
case 7:
// Compared as read, so that 257 and the like never pass as a
// defined code.
w.padding = inKey(k, () {
final v = d.uint();
if (PaddingRule.fromCode(v) == null) {
throw _nonCanonical('padding code $v is not defined');
}
return v;
});
default:
throw keyNotDefined(k);
}
seen.add(k as int);
}
requireKeys(seen, 4);
if (padded) {
for (final k in const [6, 7]) {
if (!seen.contains(k)) throw _nonCanonical('key $k is missing');
}
}
d.endMap();
}
void _encodeWire(CborEncoder e, _Wire w) {
final padded = w.format.isPadded;
e
..map(4 + presence(w.critical) + presence(w.noncritical) + (padded ? 2 : 0))
..uint(0)
..text(controlTypeTag)
..uint(1)
..uint(w.format.version)
..uint(2)
..bstr(w.headerBinding)
..uint(3)
..bstr(w.payloadIdentity);
encodeExtensionArrays(e, 4, w.critical, w.noncritical);
if (padded) {
e
..uint(6)
..bstr(w.payloadLength ?? Uint8List(0))
..uint(7)
..uint(w.padding ?? 0);
}
}
/// Validates and decodes the CONTROL_CBOR of a capsule of [format] (spec
/// §31, §63 step 14), in the layers of spec §69.1, as DecodeControl of Go:
/// the type tag and the schema version, which must be the format (another
/// is ERR_UNSUPPORTED_VERSION, whatever follows); then the CBOR profile, the
/// re-encoding and the CDDL of that version; then no extension_id in both
/// extension arrays. In versions 2 and 3, payload_length is 8 bytes of at
/// most L_MAX and padding is 1 or 2. A non-canonical encoding is rejected,
/// although CONTROL_CBOR is not hashed. Its critical extensions (layer 4)
/// are checked by the caller. The caller wipes the control it gets.
Control decodeControl(List<int> b, CapsuleFormat format) =>
withContext('capsule: CONTROL_CBOR', () {
checkSchema(b, controlTypeTag, format.version);
final w = _Wire(format);
// I_PAYLOAD is wiped on every path: the copy that decoding reads and
// the re-encoding, which unmarshalCbor wipes.
try {
unmarshalCbor(b, (d) => _decodeWire(d, w), (e) => _encodeWire(e, w));
checkDisjoint(w.critical, w.noncritical);
final l = w.payloadLength;
return Control(
headerBinding: w.headerBinding,
payloadIdentity: Uint8List.fromList(w.payloadIdentity),
critical: List.unmodifiable(w.critical),
noncritical: List.unmodifiable(w.noncritical),
// _decodeWire bounds both, so the conversions are exact.
payloadLength: l == null ? null : readUint64BE(l, 0) as int,
padding: w.padding == null ? null : PaddingRule.fromCode(w.padding!),
);
} finally {
w.payloadIdentity.fillRange(0, w.payloadIdentity.length, 0);
}
});
/// The Deterministic CBOR of [c] as the CONTROL_CBOR of a capsule of
/// [format], as EncodeControl of Go: schema version 1 without keys 6 and 7,
/// or schema version 2 or 3 with them (spec §31). Its value must be one that
/// [decodeControl] returns: a field that breaks a rule of the schema is
/// ERR_NON_CANONICAL_CBOR, and keys 6 and 7 present in format 1, or absent
/// in formats 2 and 3, or an L above L_MAX, an error of the caller without
/// a normative code, as in Go. The caller wipes the result: it holds
/// I_PAYLOAD.
Uint8List encodeControl(Control c, CapsuleFormat format) {
if (c.headerBinding.length != 32 || c.payloadIdentity.length != 32) {
throw _nonCanonical(
'capsule: header_binding and payload_identity must be 32 bytes',
);
}
final w = _Wire(format)
..headerBinding = c.headerBinding
..payloadIdentity = c.payloadIdentity;
if (!format.isPadded) {
if (c.payloadLength != null || c.padding != null) {
throw ArgumentError(
'capsule: CONTROL_CBOR of format 1 has no payload_length and no '
'padding',
);
}
} else {
// Go's Control holds a padding code and an L that are 0 when unset.
final padding = c.padding;
if (padding == null) {
throw ArgumentError('capsule: padding code 0 is not defined');
}
final l = c.payloadLength;
if (l == null) {
throw ArgumentError(
'capsule: CONTROL_CBOR of format ${format.version} needs '
'payload_length',
);
}
if (l < 0 || l > maxPayloadLength) {
throw ArgumentError(
'capsule: payload_length $l exceeds L_MAX = $maxPayloadLength',
);
}
final length = Uint8List(_payloadLengthSize);
writeUint64BE(length, 0, l);
w
..payloadLength = length
..padding = padding.code;
}
w
..critical = canonicalExtensions(c.critical)
..noncritical = canonicalExtensions(c.noncritical);
checkDisjoint(w.critical, w.noncritical);
final e = CborEncoder(capacity: 103);
_encodeWire(e, w);
return e.out();
}

@ -0,0 +1,232 @@
/// PUBLIC_HEADER (spec §24, §27), as DecodeHeader and EncodeHeader of
/// package capsule of datekeys-go and header.ts of datekeys-ts: the same
/// layers of spec §69.1 in the same order, with the same codes and texts.
library;
import 'dart:typed_data';
import 'bytes.dart';
import 'cbor.dart';
import 'datekey.dart';
import 'errors.dart';
import 'extension.dart';
import 'framing.dart';
import 'schema.dart';
/// The type tag of PUBLIC_HEADER (spec §24).
const headerTypeTag = 'datekeycap';
/// The schema version of PUBLIC_HEADER, the same in the three formats.
const headerVersion = 1;
/// The size of capsule_id (spec §21).
const capsuleIdSize = 16;
/// The declared access policy of a capsule (spec §25).
enum AccessPolicy {
/// 0, time_only: the date alone opens the capsule.
timeOnly(0, 'time_only'),
/// 1, time_and_key: the date and an access credential.
timeAndKey(1, 'time_and_key');
const AccessPolicy(this.code, this.label);
/// The value of access_policy, key 4 of PUBLIC_HEADER.
final int code;
/// The name of Go's Policy.String: time_only or time_and_key.
final String label;
/// The policy of [code], or null when V1 defines none.
static AccessPolicy? fromCode(int code) => switch (code) {
0 => timeOnly,
1 => timeAndKey,
_ => null,
};
@override
String toString() => label;
}
/// The policy named [s], `time_only` or `time_and_key`, as ParsePolicy of
/// Go, which has no normative code for another name.
AccessPolicy parsePolicy(String s) => switch (s) {
'time_only' => AccessPolicy.timeOnly,
'time_and_key' => AccessPolicy.timeAndKey,
_ => throw ArgumentError(
'capsule: unknown access policy ${goQuote(utf8Bytes(s))}',
),
};
/// PUBLIC_HEADER (spec §24). It has no profile_id of its own: the profile is
/// the one of the DateKey, the single source of truth.
final class Header {
/// A header of the fields of keys 2 to 6.
Header({
required this.capsuleId,
required this.dateKey,
required this.policy,
this.critical = const [],
this.noncritical = const [],
});
/// Key 2, capsule_id, 16 bytes.
final Uint8List capsuleId;
/// Key 3, the canonical dk1_ DateKey.
final DateKey dateKey;
/// Key 4, access_policy.
final AccessPolicy policy;
/// Key 5, critical_extensions.
final List<Extension> critical;
/// Key 6, noncritical_extensions.
final List<Extension> noncritical;
/// capsule_id in hexadecimal.
String get capsuleIdHex => toHex(capsuleId);
}
// PUBLIC_HEADER as it is encoded: keys 2 to 6, keys 0 and 1 being the
// constants headerTypeTag and headerVersion.
final class _Wire {
Uint8List capsuleId = Uint8List(0);
String dateKey = '';
int policy = 0;
List<Extension> critical = const [];
List<Extension> noncritical = const [];
}
// Reads PUBLIC_HEADER with every rule of the CDDL whose violation is
// ERR_NON_CANONICAL_CBOR, the extension arrays included (layer 3 of spec
// §69.1); the DateKey, which has codes of its own (spec §57), is parsed
// afterwards.
_Wire _decodeWire(CborDecoder d) {
final w = _Wire();
final pairs = d.map(7);
final seen = <int>{};
for (var i = 0; i < pairs; i++) {
final k = d.key();
switch (k) {
case 0:
inKey(k, () => d.text(headerTypeTag.length));
case 1:
inKey(k, () => d.uint(headerVersion));
case 2:
w.capsuleId = inKey(k, () => d.bstr(capsuleIdSize, capsuleIdSize));
case 3:
w.dateKey = inKey(k, () => d.text(maxPublicHeaderLen));
case 4:
// Compared as read, before any narrowing, which would let 256, 257
// or 2^32 pass as a policy of V1.
w.policy = inKey(k, () {
final p = d.uint();
if (p > AccessPolicy.timeAndKey.code) {
throw DateKeysException(
ErrorCode.nonCanonicalCbor,
'access_policy $p is not defined in V1',
);
}
return p;
});
case 5:
w.critical = inKey(k, () => decodeExtensionArray(d));
case 6:
w.noncritical = inKey(k, () => decodeExtensionArray(d));
default:
throw keyNotDefined(k);
}
seen.add(k as int);
}
requireKeys(seen, 5);
d.endMap();
return w;
}
void _encodeWire(CborEncoder e, _Wire w) {
e
..map(5 + presence(w.critical) + presence(w.noncritical))
..uint(0)
..text(headerTypeTag)
..uint(1)
..uint(headerVersion)
..uint(2)
..bstr(w.capsuleId)
..uint(3)
..text(w.dateKey)
..uint(4)
..uint(w.policy);
encodeExtensionArrays(e, 5, w.critical, w.noncritical);
}
DateKeysException _tooLong(int n) => DateKeysException(
ErrorCode.integrity,
'capsule: PUBLIC_HEADER of $n bytes exceeds $maxPublicHeaderLen',
);
/// Validates and decodes the bytes of PUBLIC_HEADER (spec §24, §27, §63
/// step 4), in the layers of spec §69.1, as DecodeHeader of Go: the limit of
/// spec §57 (layer 1, ERR_INTEGRITY); the type tag and the schema version
/// (layer 2); the CBOR profile, the re-encoding and the CDDL, with a
/// capsule_id of 16 bytes, an access_policy of V1 and well-formed extension
/// arrays, and no extension_id in both arrays (layer 3,
/// ERR_NON_CANONICAL_CBOR); and only then a canonical DateKey (layer 4).
/// Whether the profile is pinned and the critical extensions are known is
/// decided by the caller, still in layer 4.
Header decodeHeader(List<int> b) {
if (b.length > maxPublicHeaderLen) throw _tooLong(b.length);
return withContext('capsule: PUBLIC_HEADER', () {
checkSchema(b, headerTypeTag, headerVersion);
late _Wire w;
unmarshalCbor(b, (d) => w = _decodeWire(d), (e) => _encodeWire(e, w));
checkDisjoint(w.critical, w.noncritical);
final dateKey = parseDateKey(w.dateKey);
return Header(
capsuleId: w.capsuleId,
dateKey: dateKey,
// _decodeWire bounds the policy to 0 or 1.
policy: AccessPolicy.fromCode(w.policy)!,
critical: List.unmodifiable(w.critical),
noncritical: List.unmodifiable(w.noncritical),
);
});
}
/// The Deterministic CBOR of [h], at most [maxPublicHeaderLen] bytes (spec
/// §57), as EncodeHeader of Go: a DateKey that is not valid is
/// ERR_DATEKEY_INVALID, the extensions are written in canonical order, and
/// a header above the limit is ERR_INTEGRITY. The rule of spec §72 for the
/// extensions of the specification is the writer's ([checkWrite]).
Uint8List encodeHeader(Header h) {
final compact = compactDateKey(h.dateKey);
if (compact.isEmpty) {
throw DateKeysException(
ErrorCode.dateKeyInvalid,
'capsule: invalid DateKey',
);
}
if (h.capsuleId.length != capsuleIdSize) {
throw DateKeysException(
ErrorCode.nonCanonicalCbor,
'capsule: capsule_id is ${h.capsuleId.length} bytes, want '
'$capsuleIdSize',
);
}
final critical = canonicalExtensions(h.critical);
final noncritical = canonicalExtensions(h.noncritical);
checkDisjoint(critical, noncritical);
final w = _Wire()
..capsuleId = h.capsuleId
..dateKey = compact
..policy = h.policy.code
..critical = critical
..noncritical = noncritical;
final e = CborEncoder();
_encodeWire(e, w);
final b = Uint8List.fromList(e.out());
if (b.length > maxPublicHeaderLen) throw _tooLong(b.length);
return b;
}

@ -0,0 +1,235 @@
// Every official fixture of testdata/ through the formats: the PRELUDE, the
// sections and header_binding of each .dkc, its PUBLIC_HEADER and its
// CONTROL_CBOR, decoded and written back to the same bytes, the profile and
// the round time of its DateKey, P = rule(L), and in format 3 the frame of
// BODY, the security area and the place of the head and of the files; and
// each .dkk, whose capsule_digest is the SHA-256 of its .dkc. The values are
// those of the records <name>.json and <name>.dkk.json, which Go wrote.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:datekeys/src/accesskey.dart';
import 'package:datekeys/src/body.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/control.dart';
import 'package:datekeys/src/datekey.dart';
import 'package:datekeys/src/digest.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/extension.dart';
import 'package:datekeys/src/framing.dart';
import 'package:datekeys/src/header.dart';
import 'package:datekeys/src/padding.dart';
import 'package:datekeys/src/profile.dart';
import 'package:test/test.dart';
typedef Json = Map<String, Object?>;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
Uint8List readBytes(String path) => File(path).readAsBytesSync();
/// The extensions of a record, as `{critical, id, version, data}`.
List<Object?> recordExtensions(Object? v) => [
for (final e in ((v as List<Object?>?) ?? const []).cast<Json>())
[e['critical'], e['id'], e['version'], e['data']],
];
List<Object?> extensionsOf(List<Extension> critical, List<Extension> non) => [
for (final (c, list) in [(true, critical), (false, non)])
for (final e in list)
[c, e.id, e.version, e.data == null ? null : toHex(e.data!)],
];
String codeOf(void Function() body) {
try {
body();
return 'ok';
} on DateKeysException catch (e) {
return e.code.code;
}
}
void main() {
final names =
Directory('testdata/fixtures')
.listSync()
.map((f) => f.uri.pathSegments.last)
.where((n) => n.endsWith('.dkc'))
.map((n) => n.substring(0, n.length - 4))
.toList()
..sort();
final keys =
Directory('testdata/fixtures')
.listSync()
.map((f) => f.uri.pathSegments.last)
.where((n) => n.endsWith('.dkk'))
.toList()
..sort();
test('there are the 24 capsules and the 6 access keys of spec §67, §68', () {
expect(names, hasLength(24));
expect(keys, hasLength(6));
});
for (final name in names) {
test(name, () {
final r = readJson('testdata/fixtures/$name.json');
final dkc = readBytes('testdata/fixtures/$name.dkc');
expect(toHex(capsuleDigest(dkc)), r['sha256']);
// The frame.
final format = CapsuleFormat.fromVersion(r['format']! as int)!;
final prelude = parsePrelude(dkc.sublist(0, dkcPreludeSize));
expect(prelude.format, format);
expect(toHex(preludeBytes(prelude)), r['prelude']);
final s = splitCapsule(dkc);
expect(s.prelude, prelude);
expect(toHex(s.preludeBytes), r['prelude']);
expect(toHex(s.publicHeader), r['public_header']);
expect(s.sealedControl, hasLength(prelude.sealedControlLen));
expect(s.payload.length, dkc.length - payloadOffset(prelude));
expect(
toHex(headerBinding(s.preludeBytes, s.publicHeader)),
r['header_binding'],
);
// PUBLIC_HEADER, its profile and its round.
final h = decodeHeader(s.publicHeader);
expect(
[h.capsuleIdHex, compactDateKey(h.dateKey), h.policy.label],
[r['capsule_id'], r['datekey'], r['access_policy']],
);
expect(
extensionsOf(h.critical, h.noncritical),
recordExtensions(r['header_extensions']),
);
expect(toHex(encodeHeader(h)), r['public_header']);
final p = defaultRegistry().lookup(h.dateKey.profileId)!;
validateDateKey(h.dateKey, p);
expect(formatRfc3339(unlockAt(h.dateKey, p)!), r['unlock_at']);
// CONTROL_CBOR, in its format and in no other.
final control = decodeControl(
fromHex(r['control_cbor']! as String),
format,
);
expect(
[toHex(control.headerBinding), toHex(control.payloadIdentity)],
[r['header_binding'], r['payload_identity']],
);
expect(
extensionsOf(control.critical, control.noncritical),
recordExtensions(r['control_extensions']),
);
expect(toHex(encodeControl(control, format)), r['control_cbor']);
for (final other in CapsuleFormat.values.where((f) => f != format)) {
expect(
codeOf(
() => decodeControl(fromHex(r['control_cbor']! as String), other),
),
'ERR_UNSUPPORTED_VERSION',
);
}
final plaintext = readBytes('testdata/fixtures/${r['plaintext_file']}');
if (!format.isPadded) {
expect([control.payloadLength, control.padding], [null, null]);
expect(r.containsKey('padding'), isFalse);
expect(plaintext.length, r['payload_length']);
return;
}
final l = control.payloadLength!;
final rule = control.padding!;
expect([l, rule.code], [r['payload_length'], r['padding']]);
final padded = paddedLength(l, rule);
expect(padded, r['padded_length']);
expect(plaintext.length, l);
// The plaintext of PAYLOAD_AGE is the content and zeros up to P.
final check = PaddingCheck(l, padded);
final content = BytesBuilder()
..add(check.add(plaintext))
..add(check.add(Uint8List(padded - l)));
check.close();
expect(content.takeBytes(), plaintext);
if (format != CapsuleFormat.format3) return;
// BODY: its frame, the security area, the head and the files.
final frame = parseBodyFrame(plaintext.sublist(0, bodyFrameSize), l);
expect(frame.areaLen, r['area_len']);
final area = plaintext.sublist(
bodyFrameSize,
bodyFrameSize + frame.areaLen,
);
checkArea(area, frame.securityLen);
expect(toHex(area.sublist(0, frame.securityLen)), r['security_cbor']);
final headStart = bodyFrameSize + frame.areaLen;
expect(
toHex(plaintext.sublist(headStart, headStart + frame.headLen)),
r['head_cbor'],
);
expect(headStart + frame.headLen, r['content_offset']);
final files = (r['files'] as List<Object?>?) ?? const [];
expect(
contentLength(frame, l),
[
0,
for (final f in files) (f! as Json)['size']! as int,
].reduce((a, b) => a + b),
);
});
}
for (final key in keys) {
test(key, () {
final r = readJson('testdata/fixtures/$key.json');
final raw = readBytes('testdata/fixtures/$key');
expect(toHex(capsuleDigest(raw)), r['sha256']);
final k = decodeAccessKey(raw);
expect(
[
toHex(k.credentialId),
toHex(k.capsuleId),
k.type,
toHex(k.material),
k.verification == null ? null : toHex(k.verification!.capsuleDigest),
],
[
r['credential_id'],
r['capsule_id'],
r['access_type'],
r['access_material'],
r['capsule_digest'],
],
);
expect(
extensionsOf(k.critical, k.noncritical),
recordExtensions(r['extensions']),
);
checkAccessKeyMaterial(k);
// capsule_digest is the SHA-256 of the exact bytes of its .dkc, and its
// capsule_id that of the header of that capsule.
final dkc = readBytes('testdata/fixtures/${r['capsule']}');
checkCapsuleDigest(capsuleDigest(dkc), k.verification!.capsuleDigest);
expect(
decodeHeader(splitCapsule(dkc).publicHeader).capsuleId,
k.capsuleId,
);
expect(
codeOf(
() => checkCapsuleDigest(
capsuleDigest(raw),
k.verification!.capsuleDigest,
),
),
'ERR_ACCESS_INVALID',
);
expect(toHex(encodeAccessKey(k)), toHex(raw));
k.wipe();
expect(k.material.every((b) => b == 0), isTrue);
});
}
}

@ -0,0 +1,297 @@
// PUBLIC_HEADER, CONTROL_CBOR, the .dkk and the Provider Profile
// (lib/src/header.dart, control.dart, accesskey.dart and profile.dart), as
// header.test.ts, control.test.ts, accesskey.test.ts and profile.test.ts of
// datekeys-ts: values of a fixed seed that encode and decode back to
// themselves, the copies of the secrets and their wiping, the texts that
// hide them, and the pinned registry. The values and texts of Go are in the
// differential (formats_header.json, formats_control.json,
// formats_framing.json, formats_profile.json and formats_encode.json). It
// reads no file: it runs on the VM and compiled to JavaScript.
library;
import 'dart:math';
import 'dart:typed_data';
import 'package:datekeys/src/accesskey.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/control.dart';
import 'package:datekeys/src/datekey.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/extension.dart';
import 'package:datekeys/src/framing.dart';
import 'package:datekeys/src/header.dart';
import 'package:datekeys/src/padding.dart';
import 'package:datekeys/src/profile.dart';
import 'package:test/test.dart';
String codeOf(void Function() body) {
try {
body();
return 'ok';
} on DateKeysException catch (e) {
return e.code.code;
}
}
Uint8List bytesOf(Random r, int n) =>
Uint8List.fromList([for (var i = 0; i < n; i++) r.nextInt(256)]);
// 0 to 3 extensions with distinct identifiers, unsorted.
List<Extension> extensionsOf(Random r, String prefix) => [
for (var i = 0; i < r.nextInt(4); i++)
Extension(
'$prefix${r.nextInt(1000)}x$i',
r.nextInt(3),
r.nextBool() ? bytesOf(r, 1 + r.nextInt(5)) : null,
),
];
void main() {
test('headers encode and decode back to themselves', () {
final r = Random(17);
for (var i = 0; i < 200; i++) {
final h = Header(
capsuleId: bytesOf(r, 16),
dateKey: DateKey('datekeys:quicknet:v1', 1 + r.nextInt(1 << 30)),
policy: AccessPolicy.values[r.nextInt(2)],
critical: extensionsOf(r, 'c'),
noncritical: extensionsOf(r, 'n'),
);
final b = encodeHeader(h);
final back = decodeHeader(b);
expect(
[back.capsuleId, back.dateKey, back.policy],
[h.capsuleId, h.dateKey, h.policy],
);
expect(back.critical, canonicalExtensions(h.critical));
expect(back.noncritical, canonicalExtensions(h.noncritical));
expect(encodeHeader(back), b);
expect(back.capsuleIdHex, toHex(h.capsuleId));
}
});
test('access policies by code and by name', () {
expect(
[
for (final p in AccessPolicy.values) [p.code, p.label, '$p'],
],
[
[0, 'time_only', 'time_only'],
[1, 'time_and_key', 'time_and_key'],
],
);
expect([0, 1, 2].map(AccessPolicy.fromCode).toList(), [
AccessPolicy.timeOnly,
AccessPolicy.timeAndKey,
null,
]);
expect(parsePolicy('time_and_key'), AccessPolicy.timeAndKey);
expect(() => parsePolicy('x'), throwsArgumentError);
expect(
codeOf(
() => encodeHeader(
Header(
capsuleId: Uint8List(15),
dateKey: const DateKey('a', 1),
policy: AccessPolicy.timeOnly,
),
),
),
'ERR_NON_CANONICAL_CBOR',
);
});
test('controls encode and decode back, of a length that does not depend '
'on L', () {
final r = Random(19);
for (var i = 0; i < 200; i++) {
final format = CapsuleFormat.values[i % 3];
final c = Control(
headerBinding: bytesOf(r, 32),
payloadIdentity: bytesOf(r, 32),
critical: extensionsOf(r, 'c'),
noncritical: extensionsOf(r, 'n'),
payloadLength: format.isPadded
? [0, 1, 4294967297, maxPayloadLength, r.nextInt(1 << 30)][i % 5]
: null,
padding: format.isPadded ? PaddingRule.values[r.nextInt(2)] : null,
);
final b = encodeControl(c, format);
final back = decodeControl(b, format);
expect(
[back.headerBinding, back.payloadIdentity, back.payloadLength],
[c.headerBinding, c.payloadIdentity, c.payloadLength],
);
expect(back.padding, c.padding);
expect(encodeControl(back, format), b);
if (c.critical.isEmpty && c.noncritical.isEmpty) {
expect(b.length, format.isPadded ? 103 : 91);
expect(b[20], format.version);
}
for (final other in CapsuleFormat.values.where((f) => f != format)) {
expect(
codeOf(() => decodeControl(b, other)),
'ERR_UNSUPPORTED_VERSION',
);
}
}
});
test('a control keeps its own copy of I_PAYLOAD, wipes it, and never '
'prints it', () {
final c = Control(
headerBinding: Uint8List(32),
payloadIdentity: Uint8List(32)..fillRange(0, 32, 0xab),
payloadLength: 1,
padding: PaddingRule.reforzado,
);
final b = encodeControl(c, CapsuleFormat.format2);
final back = decodeControl(b, CapsuleFormat.format2);
b.fillRange(0, b.length, 0);
expect(back.payloadIdentity, Uint8List(32)..fillRange(0, 32, 0xab));
expect('$back', isNot(contains('abab')));
expect('$back', contains('REDACTED'));
back.wipe();
expect(back.payloadIdentity, Uint8List(32));
expect(() => encodeControl(c, CapsuleFormat.format1), throwsArgumentError);
expect(
() => encodeControl(
Control(
headerBinding: Uint8List(32),
payloadIdentity: Uint8List(32),
padding: PaddingRule.bloque256,
),
CapsuleFormat.format3,
),
throwsArgumentError,
);
});
test('access keys encode and decode back, and never print their '
'material', () {
final r = Random(23);
for (var i = 0; i < 200; i++) {
final k = AccessKey(
credentialId: bytesOf(r, 16),
capsuleId: bytesOf(r, 16),
type: accessTypeX25519,
material: bytesOf(r, 32),
verification: r.nextBool() ? Verification(bytesOf(r, 32)) : null,
critical: extensionsOf(r, 'c'),
noncritical: extensionsOf(r, 'n'),
);
final dkk = encodeAccessKey(k);
final back = decodeAccessKey(dkk);
expect(
[back.credentialId, back.capsuleId, back.type, back.material],
[k.credentialId, k.capsuleId, k.type, k.material],
);
expect(back.verification, k.verification);
expect(back.critical, canonicalExtensions(k.critical));
expect(encodeAccessKey(back), dkk);
expect('$back', isNot(contains(toHex(k.material))));
// The key keeps its own copy of the material.
dkk.fillRange(0, dkk.length, 0);
expect(back.material, k.material);
back.wipe();
expect(back.material, Uint8List(32));
expect(codeOf(() => checkAccessKeyMaterial(back)), 'ok');
}
expect(
codeOf(
() => checkAccessKeyMaterial(
AccessKey(
credentialId: Uint8List(16),
capsuleId: Uint8List(16),
type: 'X25519',
material: Uint8List(32),
),
),
),
'ERR_ACCESS_INVALID',
);
expect(
codeOf(
() => marshalAccessKeyBody(
AccessKey(
credentialId: Uint8List(15),
capsuleId: Uint8List(16),
type: accessTypeX25519,
material: Uint8List(32),
),
),
),
'ERR_NON_CANONICAL_CBOR',
);
});
group('Provider Profiles', () {
final q = quicknet();
test('Quicknet is its pinned CBOR, and its range ends in 9999', () {
expect(toHex(canonicalCbor(q)), quicknetCanonicalCbor);
expect(toHex(profileHash(q)), quicknetProfileHash);
expect(decodeProfile(fromHex(quicknetCanonicalCbor)), q);
validateProfile(q);
expect(q.chainHashHex, quicknetChainHash);
expect(
formatRfc3339(roundTime(q, q.maxRound)),
startsWith('9999-12-31T23:59:5'),
);
expect(q.copyWith(genesisTime: maxUnixTime).maxRound, 1);
expect(q.copyWith(genesisTime: maxUnixTime + 1).maxRound, 0);
expect(q.copyWith(period: 0).maxRound, 0);
});
test('the default registry is built once and hands out copies', () {
final reg = defaultRegistry();
expect(defaultRegistry(), same(reg));
final p = reg.lookup(quicknetId)!;
expect(p, q);
p.chainHash.fillRange(0, 32, 0);
expect(reg.lookup(quicknetId), q);
expect(reg.lookup('datekeys:evmnet:v1'), isNull);
final copy = q.copy();
copy.publicKey[0] ^= 1;
expect(quicknet(), isNot(copy));
});
test('a network "default", or none, is left out of the chain hash', () {
final d = q.copyWith(network: 'default');
expect(chainInfoHash(d), chainInfoHash(q.copyWith(network: '')));
expect(
chainInfoHash(d),
isNot(chainInfoHash(q.copyWith(network: 'defaults'))),
);
validateProfile(d.copyWith(chainHash: chainInfoHash(d)));
expect(
codeOf(() => chainInfoHash(q.copyWith(period: maxChainHashPeriod + 1))),
'ERR_UNKNOWN_PROFILE',
);
expect(
chainInfoHash(q.copyWith(period: maxChainHashPeriod)),
hasLength(32),
);
});
test('a profile_id is [a-z0-9:._-], from 1 to 128, the first of '
'[a-z0-9]', () {
for (final s in ['datekeys:quicknet:v1', '0${'a' * 127}', 'a', '0:._-']) {
expect(validId(s), isTrue, reason: s);
}
for (final s in [
'',
'A',
':a',
'.a',
'a b',
'a/b',
'é',
'0${'a' * 128}',
'a\n',
]) {
expect(validId(s), isFalse, reason: s);
}
});
});
}

@ -0,0 +1,269 @@
// The shared vectors of testdata/ that the formats read, as vectors.test.ts
// of datekeys-ts: dk1.json, quicknet_rounds.json, profile_quicknet.json,
// padding.json and the schemas block of cbor.json, each object through the
// decoder of its schema and written back to the same bytes.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'package:datekeys/src/accesskey.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/control.dart';
import 'package:datekeys/src/datekey.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/framing.dart';
import 'package:datekeys/src/header.dart';
import 'package:datekeys/src/padding.dart';
import 'package:datekeys/src/profile.dart';
import 'package:test/test.dart';
typedef Json = Map<String, Object?>;
Json readJson(String path) => jsonDecode(File(path).readAsStringSync()) as Json;
List<Json> section(Json file, String name) =>
(file[name]! as List).cast<Json>();
/// The code of the [DateKeysException] that [body] throws, or `ok`.
String resultOf(void Function() body) {
try {
body();
return 'ok';
} on DateKeysException catch (e) {
return e.code.code;
}
}
void main() {
group('testdata/vectors/dk1.json', () {
final file = readJson('testdata/vectors/dk1.json');
final vectors = section(file, 'vectors');
test('is of this specification and holds valid and invalid strings', () {
expect(file['spec'], '0.11');
expect(vectors.where((v) => v.containsKey('dk1')), isNotEmpty);
expect(vectors.where((v) => v.containsKey('input')), isNotEmpty);
});
for (final v in vectors) {
test(v['name'], () {
if (v.containsKey('input')) {
expect(
resultOf(() => parseDateKey(v['input']! as String)),
v['error'],
);
return;
}
final d = parseDateKey(v['dk1']! as String);
expect([d.profileId, d.round], [v['network'], v['round']]);
expect(canonicalJson(d), v['canonical_json']);
expect(compactDateKey(d), v['dk1']);
expect(compactDateKey(d), 'dk1_${v['base64url']}');
});
}
});
group('testdata/vectors/quicknet_rounds.json', () {
final file = readJson('testdata/vectors/quicknet_rounds.json');
final p = quicknet();
test('is for the pinned Quicknet profile', () {
expect(file['profile'], quicknetId);
expect(section(file, 'vectors'), isNotEmpty);
});
for (final v in section(file, 'vectors')) {
test(v['name'], () {
final at = parseRfc3339(v['requested']! as String);
if (v.containsKey('error')) {
expect(resultOf(() => resolveDateKey(p, at)), v['error']);
return;
}
final d = resolveDateKey(p, at);
expect(d.round, v['round']);
expect(formatRfc3339Nano(unlockAt(d, p)!), v['effective']);
expect(resolveRfc3339(p, v['requested']! as String), d);
});
}
});
test('testdata/vectors/profile_quicknet.json', () {
final v = readJson('testdata/vectors/profile_quicknet.json');
final p = quicknet();
expect(
[
p.id,
p.provider,
p.network,
toHex(p.chainHash),
toHex(p.publicKey),
p.period,
p.genesisTime,
toHex(p.genesisSeed),
p.scheme,
],
[
v['profile_id'],
v['provider'],
v['network'],
v['chain_hash'],
v['public_key'],
v['period_seconds'],
v['genesis_time'],
v['genesis_seed'],
v['scheme'],
],
);
expect(toHex(canonicalCbor(p)), v['canonical_cbor']);
expect(toHex(profileHash(p)), v['profile_hash']);
expect(decodeProfile(fromHex(v['canonical_cbor']! as String)), p);
expect(
[quicknetCanonicalCbor, quicknetProfileHash],
[v['canonical_cbor'], v['profile_hash']],
);
});
group('testdata/vectors/padding.json', () {
final file = readJson('testdata/vectors/padding.json');
final vectors = section(file, 'vectors');
test('names L_MAX and covers the boundaries of spec §29.1', () {
expect(file['l_max'], maxPayloadLength);
final ls = vectors.map((v) => v['l']).toList();
for (final l in [
0,
256,
257,
2113929216,
2113929217,
4227858432,
4227858433,
4294967295,
4294967297,
562949953421311,
maxPayloadLength,
]) {
expect(ls, contains(l));
}
expect(file['rejected'], contains(maxPayloadLength + 1));
});
for (final v in vectors) {
test('L = ${v['l']}', () {
final l = v['l']! as int;
final b = paddedLength(l, PaddingRule.bloque256);
final r = paddedLength(l, PaddingRule.reforzado);
expect(
[b, r, payloadAgeLength(b), payloadAgeLength(r)],
[
v['bloque256'],
v['reforzado'],
v['payload_age_bloque256'],
v['payload_age_reforzado'],
],
);
if (l > 256) {
final (:e, :s, :lastBits) = padmeParameters(l);
expect([e, s, lastBits], [v['e'], v['s'], v['last_bits']]);
} else {
expect(v.containsKey('e'), isFalse);
}
});
}
test('rejects the lengths above L_MAX', () {
for (final l in (file['rejected']! as List).cast<int>()) {
for (final rule in PaddingRule.values) {
expect(
() => paddedLength(l, rule),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
'capsule: content of $l bytes exceeds L_MAX = '
'$maxPayloadLength',
),
),
);
}
}
});
});
group('testdata/vectors/cbor.json: the schemas', () {
final file = readJson('testdata/vectors/cbor.json');
final schemas = section(file, 'schemas');
// The decoder of each schema, for the capsule format of the vector; it
// returns the writer of the object it decoded.
final decoders = <String, Object Function() Function(List<int>, int)>{
'provider_profile': (b, _) {
final p = decodeProfile(b);
return () => canonicalCbor(p);
},
'public_header': (b, _) {
final h = decodeHeader(b);
return () => encodeHeader(h);
},
'control_cbor': (b, format) {
final f = CapsuleFormat.fromVersion(format)!;
final c = decodeControl(b, f);
return () => encodeControl(c, f);
},
'dkk_body': (b, _) {
final k = decodeAccessKeyBody(b);
return () => marshalAccessKeyBody(k);
},
};
const blocks = {
'provider_profile': 'provider_profile',
'public_header': 'public_header',
'control_cbor': 'control_cbor',
'dkk_body': 'dkk_body',
'verification_metadata': 'dkk_body',
'extension': 'public_header',
};
test('has every block, with CONTROL_CBOR in the three formats', () {
for (final block in blocks.keys) {
expect(
schemas.where((v) => v['block'] == block).length,
greaterThanOrEqualTo(5),
reason: block,
);
}
for (final v in schemas) {
expect(blocks[v['block']], v['schema'], reason: '${v['name']}');
expect(
v.containsKey('format'),
v['schema'] == 'control_cbor' &&
(v['name']! as String).startsWith('format '),
reason: '${v['name']}',
);
}
final controls = schemas.where((v) => v['schema'] == 'control_cbor');
for (final f in [2, 3]) {
expect(
controls.where((v) => v['format'] == f).length,
greaterThanOrEqualTo(5),
);
}
});
for (final v in schemas) {
test('${v['block']} (${v['schema']}): ${v['name']}', () {
final input = fromHex(v['hex']! as String);
Object Function()? write;
final result = resultOf(() {
write = decoders[v['schema']]!(input, (v['format'] as int?) ?? 1);
});
expect(result, v['result']);
// An accepted object is written back to the same bytes.
if (write != null) {
expect(toHex(write!() as List<int>), v['hex']);
}
});
}
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.