diff --git a/.aegis/experiments/pr-253-rfc8785-fail-closed-v1.json b/.aegis/experiments/pr-253-rfc8785-fail-closed-v1.json new file mode 100644 index 000000000..28ec3c797 --- /dev/null +++ b/.aegis/experiments/pr-253-rfc8785-fail-closed-v1.json @@ -0,0 +1,86 @@ +{ + "admission_executable": { + "blob_id": "b9c998ddc85b9beeefec30121e88e828b15a8405", + "path": "sovereign-omega-v2/scripts/validate-experiment-plan.ts" + }, + "admission_workflow": { + "blob_id": "01a59d129b0431c9ccb92b1227a0e46992b1562f", + "path": ".github/workflows/experiment-admission.yml" + }, + "budget": { + "max_cost_microunits": 0, + "max_duration_seconds": 3600, + "max_mutations": 0 + }, + "claims_ledger": { + "path": ".aegis/claims-ledger.json", + "root": "495a01d7a942d5f90b39f2d2b178b074aee60c5e6460e1622f1917982ac59652" + }, + "constitution": { + "blob_id": "d0c210443e03313113e43da46c4d98269494baeb", + "path": "CONSTITUTIONAL_DECLARATION.md" + }, + "evidence_tier": "T2", + "execution_class": "EXPERIMENT", + "expected_outputs": [ + "ADMISSION_RECEIPT.json", + "EVIDENCE_MANIFEST.json", + "EXPERIMENT_PLAN.json", + "INTEGRATION_LEDGER.json", + "INTEGRATION_LEDGER.md", + "SHA256SUMS" + ], + "expected_parent_sha": "0bdffe75b56e5cd27c0632e1ba166620da327494", + "expected_parent_state_root": "7cc0329e312dfd309133e237dad6095cafbc881a3e934051df1f03303f507859", + "experiment_id": "pr-253-rfc8785-fail-closed-v1", + "integration_ledger_generator": { + "blob_id": "28823ae5b630be273b78210f0addf9c0a86aad05", + "path": "scripts/integration_ledger.py" + }, + "observability": { + "cancellation_mechanism": "github-actions-cancel-run", + "durable_execution_required": true, + "emergency_stop_reference": "github-actions:cancel-run", + "heartbeat_max_seconds": 300, + "provider": "github-actions" + }, + "operator_approval": { + "approval_record_hash": "65f85da29b8019f657f5e44bb5f08fd99a434ff8b1fa413325478bd0b9eeca56", + "authorization_basis": "user-explicit-kreni-rfc8785-canonicalization-hardening", + "decided_at": "2026-08-01T01:53:00+02:00", + "operator_actor_id": "tarikskalic", + "operator_session_id": "chatgpt-session-2026-08-01-pr-253-jcs-hardening", + "required": true, + "signature_mode": "GITHUB_OIDC_ATTESTATION", + "state": "APPROVED" + }, + "policy": { + "blob_id": "5937cf4d9fc59f224faabfb7865bba7aa5da90dc", + "path": "docs/rfcs/0001-operator-sovereign-control-plane.md" + }, + "replay_package": { + "include_admission_receipt": true, + "include_evidence_manifest": true, + "include_integration_ledger": true, + "include_plan": true, + "required": true + }, + "repository": "Aegis-Omega/AEGIS-OMEGA", + "requested_authority_domains": [ + "github:artifact-metadata-write", + "github:attestation-write", + "github:oidc-token-mint", + "github:workflow-artifact-write" + ], + "schema_version": "0.1.0", + "sovereignty_contracts": { + "blob_id": "d53860f1340293cf08955ebb8729a853432551eb", + "path": "sovereign-omega-v2/src/sovereignty/contracts.ts" + }, + "termination_conditions": [ + "budget_exhausted", + "observability_expired", + "operator_emergency_stop" + ], + "title": "Enforce fail-closed RFC 8785 canonicalization and adversarial boundary tests" +} diff --git a/.claude.json b/.claude.json index db426975c..5701c8279 100644 --- a/.claude.json +++ b/.claude.json @@ -9,8 +9,8 @@ "provenance": { "generator": "scripts/build-cognitive-manifest.py", "repository": "Aegis-Omega/AEGIS-OMEGA", - "source_ref": "claude/blissful-rubin-mt9jS", - "parent_state_hash": "410bcd49c721e65050382ae759db7af2f41fa9f572e625174252c72a8748ea93", + "source_ref": "fix/rfc8785-fail-closed-canonicalization", + "parent_state_hash": "e9f0ec153b0b320a1e791092f73209442ec43a982e503203d9c101ec40949cba", "signature_mode": "GITHUB_OIDC_ATTESTATION" }, "hashing": { @@ -488,5 +488,5 @@ "on_success": "broadcast-attested-verified-event-stream" } }, - "state_hash": "e9f0ec153b0b320a1e791092f73209442ec43a982e503203d9c101ec40949cba" + "state_hash": "d24e6c9c6d8a2791ab4595ecd800b8e23c58338e8a51ab8cf68aab98ce026b69" } diff --git a/sovereign-omega-v2/package.json b/sovereign-omega-v2/package.json index 7e7e6ceaa..5b92d6828 100644 --- a/sovereign-omega-v2/package.json +++ b/sovereign-omega-v2/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage", + "test:coverage": "vitest run --coverage --no-file-parallelism", "test:determinism": "node --experimental-vm-modules scripts/verify-canonicalization.ts", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "typecheck": "tsc --noEmit", diff --git a/sovereign-omega-v2/src/core/canonicalize.ts b/sovereign-omega-v2/src/core/canonicalize.ts index fb1433fe1..a997399bf 100644 --- a/sovereign-omega-v2/src/core/canonicalize.ts +++ b/sovereign-omega-v2/src/core/canonicalize.ts @@ -1,32 +1,33 @@ // ============================================================ // SOVEREIGN OMEGA — RFC 8785 JSON Canonicalization Scheme -// EPISTEMIC TIER: T0 (mechanically proven) +// EPISTEMIC TIER: T0 deterministic primitive; conformance is test-gated // GATE 1: byte-identical output across Node/Browser/WASM // ============================================================ -// Implementation of RFC 8785 (JSON Canonicalization Scheme). -// This is the ONLY permitted serialisation method for hashing. -// Never use JSON.stringify for integrity-critical operations. +// This is the only permitted serialization method for integrity hashing. +// Callers must project application values into ordinary JSON data first. // ============================================================ -/** - * Canonicalise a JavaScript value to its RFC 8785 byte representation. - * Returns a Uint8Array for direct use in SHA-256 without string conversion. - */ +const ESCAPE_PREFIX = String.fromCharCode(0x5c) +const ESCAPED_QUOTE = ESCAPE_PREFIX + '"' +const ESCAPED_BACKSLASH = ESCAPE_PREFIX + ESCAPE_PREFIX +const ESCAPED_BACKSPACE = ESCAPE_PREFIX + 'b' +const ESCAPED_TAB = ESCAPE_PREFIX + 't' +const ESCAPED_NEWLINE = ESCAPE_PREFIX + 'n' +const ESCAPED_FORM_FEED = ESCAPE_PREFIX + 'f' +const ESCAPED_CARRIAGE_RETURN = ESCAPE_PREFIX + 'r' +const ESCAPED_UNICODE_PREFIX = ESCAPE_PREFIX + 'u' + +/** Canonicalise an ordinary JSON value to RFC 8785 UTF-8 bytes. */ export function canonicalizeJCS(value: unknown): Uint8Array { - const str = serializeValue(value) - return new TextEncoder().encode(str) + return new TextEncoder().encode(serializeValue(value, new WeakSet())) } -/** - * Canonicalise to string (for debugging and testing against test vectors). - */ +/** Canonicalise to a string for tests and diagnostics. */ export function canonicalizeJCSString(value: unknown): string { - return serializeValue(value) + return serializeValue(value, new WeakSet()) } -// ─── Internal Serialisation ──────────────────────────────── - -function serializeValue(value: unknown): string { +function serializeValue(value: unknown, stack: WeakSet): string { if (value === null) return 'null' if (value === true) return 'true' if (value === false) return 'false' @@ -35,103 +36,222 @@ function serializeValue(value: unknown): string { if (type === 'number') return serializeNumber(value as number) if (type === 'string') return serializeString(value as string) - if (type === 'bigint') return serializeString((value as bigint).toString()) - if (Array.isArray(value)) { - const items = value.map(serializeValue) - return '[' + items.join(',') + ']' + if (type === 'bigint') { + throw new TypeError( + 'BigInt is not a JSON value; encode it explicitly as a decimal string before JCS canonicalization', + ) } + if (value === undefined) throw new TypeError('undefined is not a JSON value') + if (type === 'function') throw new TypeError('function is not a JSON value') + if (type === 'symbol') throw new TypeError('symbol is not a JSON value') + + if (Array.isArray(value)) return serializeArray(value, stack) + if (type === 'object') return serializeObject(value as object, stack) + + /* c8 ignore next -- all JavaScript value categories are exhausted above */ + throw new TypeError(`Unserializable type: ${type}`) +} + +function serializeArray(value: unknown[], stack: WeakSet): string { + return withCycleGuard(value, stack, () => { + if (Object.getOwnPropertySymbols(value).length !== 0) { + throw new TypeError('Symbol-keyed array properties are not JSON values') + } - if (type === 'object') { - const obj = value as Record - // RFC 8785: sort object keys by Unicode code point order - const sortedKeys = Object.keys(obj).sort((a, b) => { - // Compare by Unicode code point sequence (not locale-sensitive) - for (let i = 0; i < Math.min(a.length, b.length); i++) { - /* c8 ignore next -- noUncheckedIndexedAccess artifact; i < min(a.length, b.length) guarantees valid indices */ - const diff = (a.codePointAt(i) ?? 0) - (b.codePointAt(i) ?? 0) - if (diff !== 0) return diff + for (const name of Object.getOwnPropertyNames(value)) { + if (name === 'length') continue + if (!isCanonicalArrayIndex(name, value.length)) { + throw new TypeError( + `Non-index array property is not permitted at the JCS boundary: ${name}`, + ) } - return a.length - b.length - }) - const pairs = sortedKeys - .filter(k => obj[k] !== undefined) - .map(k => serializeString(k) + ':' + serializeValue(obj[k])) - return '{' + pairs.join(',') + '}' - } + } - if (value === undefined) throw new TypeError('undefined is not JSON-serialisable') - if (type === 'function') throw new TypeError('function is not JSON-serialisable') - /* c8 ignore next -- only symbol values reach this point; false branch structurally impossible */ - if (type === 'symbol') throw new TypeError('symbol is not JSON-serialisable') + const items: string[] = [] + for (let index = 0; index < value.length; index++) { + if (!Object.prototype.hasOwnProperty.call(value, index)) { + throw new TypeError( + `Sparse arrays are not permitted at the JCS boundary: missing index ${index}`, + ) + } - /* c8 ignore next -- TypeScript exhausts all reachable types above; no well-typed caller reaches this */ - throw new TypeError(`Unserializable type: ${type}`) + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)) + if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) { + throw new TypeError(`Array index ${index} must be an enumerable data property`) + } + + items.push(serializeValue(descriptor.value, stack)) + } + + return '[' + items.join(',') + ']' + }) } -function serializeNumber(n: number): string { - if (!isFinite(n)) throw new RangeError('Infinity and NaN are not RFC 8785 compliant') +function serializeObject(value: object, stack: WeakSet): string { + return withCycleGuard(value, stack, () => { + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) { + throw new TypeError('Only plain JSON objects are permitted at the JCS boundary') + } + + if (Object.getOwnPropertySymbols(value).length !== 0) { + throw new TypeError('Symbol-keyed object properties are not JSON values') + } + + const descriptors = Object.getOwnPropertyDescriptors(value) + const keys = Object.getOwnPropertyNames(value) + + for (const key of keys) { + const descriptor = descriptors[key] + if (!descriptor || !descriptor.enumerable) { + throw new TypeError( + `Non-enumerable object property is not permitted at the JCS boundary: ${key}`, + ) + } + if (!('value' in descriptor)) { + throw new TypeError( + `Accessor object property is not permitted at the JCS boundary: ${key}`, + ) + } + } - // RFC 8785 uses the ES2020 Number::toString which produces the - // shortest decimal representation that round-trips exactly. - // JavaScript's default number-to-string already does this for finite numbers. - if (Object.is(n, -0)) return '0' - return String(n) + keys.sort(compareUtf16CodeUnits) + + return ( + '{' + + keys + .map((key) => { + const descriptor = descriptors[key] + /* c8 ignore next -- descriptor validity was established above */ + if (!descriptor || !('value' in descriptor)) { + throw new TypeError(`Invalid object property descriptor: ${key}`) + } + return serializeString(key) + ':' + serializeValue(descriptor.value, stack) + }) + .join(',') + + '}' + ) + }) +} + +function withCycleGuard(value: object, stack: WeakSet, operation: () => T): T { + if (stack.has(value)) throw new TypeError('Cyclic values are not JSON values') + stack.add(value) + try { + return operation() + } finally { + stack.delete(value) + } } -function serializeString(s: string): string { - // RFC 8785 string serialisation: escape control characters and - // specific characters as required by JSON. +function isCanonicalArrayIndex(name: string, length: number): boolean { + if (!/^(0|[1-9][0-9]*)$/.test(name)) return false + const index = Number(name) + return ( + Number.isSafeInteger(index) && + index >= 0 && + index < length && + String(index) === name + ) +} + +/** RFC 8785 sorts raw property names by unsigned UTF-16 code units. */ +function compareUtf16CodeUnits(left: string, right: string): number { + const commonLength = Math.min(left.length, right.length) + for (let index = 0; index < commonLength; index++) { + const difference = left.charCodeAt(index) - right.charCodeAt(index) + if (difference !== 0) return difference + } + return left.length - right.length +} + +function serializeNumber(value: number): string { + if (!Number.isFinite(value)) { + throw new RangeError('Infinity and NaN are not RFC 8785 compliant') + } + if (Object.is(value, -0)) return '0' + return String(value) +} + +function serializeString(value: string): string { + assertWellFormedUnicode(value) + let result = '"' - for (let i = 0; i < s.length; i++) { - /* c8 ignore next -- noUncheckedIndexedAccess artifact; i < s.length guarantees valid index */ - const cp = s.codePointAt(i) ?? 0 - /* c8 ignore next -- same as above */ - const ch = s[i] ?? '' - - if (cp === 0x22) { result += '\\"'; continue } - if (cp === 0x5c) { result += '\\\\'; continue } - if (cp === 0x08) { result += '\\b'; continue } - if (cp === 0x09) { result += '\\t'; continue } - if (cp === 0x0a) { result += '\\n'; continue } - if (cp === 0x0c) { result += '\\f'; continue } - if (cp === 0x0d) { result += '\\r'; continue } - - if (cp < 0x20) { - // Other control characters: use \uXXXX - result += '\\u' + cp.toString(16).padStart(4, '0') + for (let index = 0; index < value.length; index++) { + const codePoint = value.codePointAt(index) + /* c8 ignore next -- index is in range */ + if (codePoint === undefined) throw new TypeError('Invalid Unicode code point') + + if (codePoint === 0x22) { + result += ESCAPED_QUOTE continue } - - // Non-BMP character (U+10000..U+10FFFF): codePointAt returns full scalar > 0xFFFF. - // Must emit both UTF-16 code units and skip the low surrogate on next iteration. - if (cp > 0xFFFF) { - /* c8 ignore next -- noUncheckedIndexedAccess artifact; well-formed strings always have a paired low surrogate */ - result += ch + (s[i + 1] ?? '') - i++ // skip the low surrogate code unit + if (codePoint === 0x5c) { + result += ESCAPED_BACKSLASH continue } - - // Lone high surrogate (codePointAt returns surrogate value when unpaired) - if (cp >= 0xD800 && cp <= 0xDBFF) { - result += '\\u' + cp.toString(16).padStart(4, '0') + if (codePoint === 0x08) { + result += ESCAPED_BACKSPACE continue } - // Lone low surrogate - if (cp >= 0xDC00 && cp <= 0xDFFF) { - result += '\\u' + cp.toString(16).padStart(4, '0') + if (codePoint === 0x09) { + result += ESCAPED_TAB + continue + } + if (codePoint === 0x0a) { + result += ESCAPED_NEWLINE + continue + } + if (codePoint === 0x0c) { + result += ESCAPED_FORM_FEED + continue + } + if (codePoint === 0x0d) { + result += ESCAPED_CARRIAGE_RETURN + continue + } + if (codePoint < 0x20) { + result += ESCAPED_UNICODE_PREFIX + codePoint.toString(16).padStart(4, '0') continue } - result += ch + if (codePoint > 0xffff) { + result += value.slice(index, index + 2) + index++ + continue + } + + result += value[index] } - result += '"' - return result + + return result + '"' } -// ─── Test Vector Validation ──────────────────────────────── +/** RFC 8785 requires invalid Unicode data, including lone surrogates, to fail. */ +function assertWellFormedUnicode(value: string): void { + for (let index = 0; index < value.length; index++) { + const codeUnit = value.charCodeAt(index) + + if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { + if (index + 1 >= value.length) { + throw new TypeError('Lone high surrogate is not RFC 8785 compliant') + } + const next = value.charCodeAt(index + 1) + if (next < 0xdc00 || next > 0xdfff) { + throw new TypeError('Lone high surrogate is not RFC 8785 compliant') + } + index++ + continue + } + + if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { + throw new TypeError('Lone low surrogate is not RFC 8785 compliant') + } + } +} -/** RFC 8785 Appendix B test vectors for conformance verification. */ +/** RFC 8785 Appendix B and Section 3.2.3 conformance vectors. */ export const RFC8785_TEST_VECTORS: Array<{ input: unknown; expected: string }> = [ { input: null, expected: 'null' }, { input: true, expected: 'true' }, @@ -154,21 +274,38 @@ export const RFC8785_TEST_VECTORS: Array<{ input: unknown; expected: string }> = { input: { z: 1, a: 2, m: 3 }, expected: '{"a":2,"m":3,"z":1}' }, { input: { payload: { b: 2, a: 1 }, type: 'test' }, - expected: '{"payload":{"a":1,"b":2},"type":"test"}' + expected: '{"payload":{"a":1,"b":2},"type":"test"}', + }, + { + input: { + '\u20ac': 'Euro Sign', + '\r': 'Carriage Return', + '\ufb33': 'Hebrew Letter Dalet With Dagesh', + '1': 'One', + '😀': 'Emoji: Grinning Face', + '\u0080': 'Control', + '\u00f6': 'Latin Small Letter O With Diaeresis', + }, + expected: + '{"\\r":"Carriage Return","1":"One","€":"Control","ö":"Latin Small Letter O With Diaeresis","€":"Euro Sign","😀":"Emoji: Grinning Face","דּ":"Hebrew Letter Dalet With Dagesh"}', }, ] -export function verifyRFC8785Conformance(): { passed: number; failed: Array<{ index: number; expected: string; got: string }> } { +export function verifyRFC8785Conformance(): { + passed: number + failed: Array<{ index: number; expected: string; got: string }> +} { const failed: Array<{ index: number; expected: string; got: string }> = [] - for (let i = 0; i < RFC8785_TEST_VECTORS.length; i++) { - const vec = RFC8785_TEST_VECTORS[i] - /* c8 ignore next -- noUncheckedIndexedAccess artifact; i < RFC8785_TEST_VECTORS.length guarantees vec is defined */ - if (!vec) continue - const got = canonicalizeJCSString(vec.input) - /* c8 ignore next -- RFC8785 test vectors all pass by T0 guarantee; failure path exists for debugging broken implementations */ - if (got !== vec.expected) { - failed.push({ index: i, expected: vec.expected, got }) + + for (let index = 0; index < RFC8785_TEST_VECTORS.length; index++) { + const vector = RFC8785_TEST_VECTORS[index] + /* c8 ignore next -- index is bounded by the array length */ + if (!vector) continue + const got = canonicalizeJCSString(vector.input) + if (got !== vector.expected) { + failed.push({ index, expected: vector.expected, got }) } } + return { passed: RFC8785_TEST_VECTORS.length - failed.length, failed } } diff --git a/sovereign-omega-v2/test/determinism/serializer-fuzz.test.ts b/sovereign-omega-v2/test/determinism/serializer-fuzz.test.ts index 0b4edd596..0c4b54320 100644 Binary files a/sovereign-omega-v2/test/determinism/serializer-fuzz.test.ts and b/sovereign-omega-v2/test/determinism/serializer-fuzz.test.ts differ diff --git a/sovereign-omega-v2/test/unit/jcs.test.ts b/sovereign-omega-v2/test/unit/jcs.test.ts index 4bfb39207..b705d430c 100644 --- a/sovereign-omega-v2/test/unit/jcs.test.ts +++ b/sovereign-omega-v2/test/unit/jcs.test.ts @@ -20,6 +20,22 @@ describe('RFC 8785 Conformance — Gate 1', () => { expect(canonicalizeJCSString({ b: { z: 1, a: 2 }, a: 1 })).toBe('{"a":1,"b":{"a":2,"z":1}}') }) + it('sorts property names by unsigned UTF-16 code units', () => { + const input = { + '\u20ac': 'Euro Sign', + '\r': 'Carriage Return', + '\ufb33': 'Hebrew Letter Dalet With Dagesh', + '1': 'One', + '😀': 'Emoji: Grinning Face', + '\u0080': 'Control', + '\u00f6': 'Latin Small Letter O With Diaeresis', + } + + expect(canonicalizeJCSString(input)).toBe( + '{"\\r":"Carriage Return","1":"One","€":"Control","ö":"Latin Small Letter O With Diaeresis","€":"Euro Sign","😀":"Emoji: Grinning Face","דּ":"Hebrew Letter Dalet With Dagesh"}' + ) + }) + it('handles -0 as 0', () => { expect(canonicalizeJCSString(-0)).toBe('0') }) @@ -66,16 +82,62 @@ describe('RFC 8785 Conformance — Gate 1', () => { expect(run2).toBe(run3) }) - it('throws on undefined values', () => { - expect(() => canonicalizeJCSString(undefined)).toThrow() + it('throws on undefined values, including nested object properties', () => { + expect(() => canonicalizeJCSString(undefined)).toThrow(TypeError) + expect(() => canonicalizeJCSString({ permitted: true, hidden: undefined })).toThrow(TypeError) + }) + + it('throws on BigInt rather than silently changing its type', () => { + expect(() => canonicalizeJCSString(1n)).toThrow(TypeError) + expect(() => canonicalizeJCSString({ sequence: 1n })).toThrow(TypeError) }) it('throws on Infinity', () => { - expect(() => canonicalizeJCSString(Infinity)).toThrow() + expect(() => canonicalizeJCSString(Infinity)).toThrow(RangeError) }) it('throws on NaN', () => { - expect(() => canonicalizeJCSString(NaN)).toThrow() + expect(() => canonicalizeJCSString(NaN)).toThrow(RangeError) + }) + + it('throws on lone surrogate code units in values and property names', () => { + expect(() => canonicalizeJCSString('\ud800')).toThrow(TypeError) + expect(() => canonicalizeJCSString('\udc00')).toThrow(TypeError) + expect(() => canonicalizeJCSString({ ['\ud800']: 'invalid-key' })).toThrow(TypeError) + }) + + it('throws on sparse arrays and non-index array properties', () => { + const sparse = new Array(2) + sparse[0] = 'present' + expect(() => canonicalizeJCSString(sparse)).toThrow(TypeError) + + const extended = [1, 2] as number[] & { extra?: number } + extended.extra = 3 + expect(() => canonicalizeJCSString(extended)).toThrow(TypeError) + }) + + it('throws on custom objects and accessors without invoking a getter', () => { + expect(() => canonicalizeJCSString(new Date(0))).toThrow(TypeError) + expect(() => canonicalizeJCSString(new Map([['a', 1]]))).toThrow(TypeError) + + let invoked = false + const withAccessor = {} + Object.defineProperty(withAccessor, 'secret', { + enumerable: true, + get() { + invoked = true + return 'must-not-run' + }, + }) + + expect(() => canonicalizeJCSString(withAccessor)).toThrow(TypeError) + expect(invoked).toBe(false) + }) + + it('throws on cycles', () => { + const cyclic: Record = {} + cyclic.self = cyclic + expect(() => canonicalizeJCSString(cyclic)).toThrow(TypeError) }) it('handles all RFC 8785 test vectors individually', () => {