/** * IETF draft-ietf-cose-merkle-tree-proofs-18 alias tests. * * Verifies that the new IETF-aligned field names coexist with the legacy * names on the same InclusionProof object, and that COSE_MerkleInclusionProof * is a working type alias for InclusionProof. */ import { describe, it, expect, expectTypeOf } from 'vitest'; import { createHash } from 'node:crypto'; import { buildMerkleRoot, buildMerkleRootFromLeaves, signRemoteId, generateRemoteIdKey, verifyInclusionProof, leafHash, F3411IdType, F3411UaType, type InclusionProof, type COSE_MerkleInclusionProof, type TreeHead, type RemoteIdMessage, } from '../../src/remoteid/index.js'; const BASIC: RemoteIdMessage = { kind: 'basic_id', idType: F3411IdType.SERIAL_NUMBER, uaType: F3411UaType.ROTORCRAFT_HELI_OR_MULTIROTOR, uasId: 'IETF-ALIAS-TEST-0001', }; function makeBatch(count: number) { const key = generateRemoteIdKey(); const logs = Array.from({ length: count }, (_, i) => signRemoteId(BASIC, key, 1_700_000_000_000 + i), ); return { key, logs, batch: buildMerkleRoot(logs) }; } describe('IETF alias — type identity', () => { it('COSE_MerkleInclusionProof is assignable to InclusionProof and vice versa', () => { expectTypeOf().toEqualTypeOf(); }); }); describe('IETF alias — field presence on every inclusion_proof', () => { it('exposes both legacy and IETF names on the same proof object', () => { const { batch } = makeBatch(5); const proof = batch.inclusion_proof(2); // Legacy expect(proof.index).toBe(2); expect(proof.leafCount).toBe(5); expect(Array.isArray(proof.siblings)).toBe(true); expect(typeof proof.root).toBe('string'); // IETF draft-18 aliases expect(proof.leaf_index).toBe(2); expect(proof.tree_size).toBe(5); expect(Array.isArray(proof.audit_path)).toBe(true); expect(typeof proof.root_hash).toBe('string'); }); it('legacy and IETF fields are byte-equal for every value', () => { const { batch } = makeBatch(8); for (let i = 0; i < 8; i++) { const p = batch.inclusion_proof(i); expect(p.leaf_index).toBe(p.index); expect(p.tree_size).toBe(p.leafCount); expect(p.root_hash).toBe(p.root); expect(p.audit_path).toEqual(p.siblings); expect(p.audit_path.length).toBe(p.siblings.length); for (let s = 0; s < p.audit_path.length; s++) { expect(p.audit_path[s]!.hash).toBe(p.siblings[s]!.hash); expect(p.audit_path[s]!.side).toBe(p.siblings[s]!.side); } } }); }); describe('IETF alias — verification parity', () => { it('verifyInclusionProof accepts a proof accessed via IETF aliases', () => { const { logs, batch } = makeBatch(6); for (let i = 0; i < 6; i++) { const proof = batch.inclusion_proof(i); const leaf = leafHash(logs[i]!); // Walk using the IETF audit_path + root_hash view. let cur = leaf; for (const step of proof.audit_path) { const left = step.side === 'L' ? Buffer.from(step.hash, 'hex') : Buffer.from(cur, 'hex'); const right = step.side === 'L' ? Buffer.from(cur, 'hex') : Buffer.from(step.hash, 'hex'); cur = createHash('sha256').update(left).update(right).digest('hex'); } expect(cur).toBe(proof.root_hash); // And the original API still works. expect(verifyInclusionProof(leaf, proof, batch.root)).toBe(true); } }); }); describe('IETF alias — TreeHead shape', () => { it('TreeHead carries tree_size, root_hash (bytes), timestamp', () => { const { batch } = makeBatch(4); const head: TreeHead = { tree_size: batch.leafCount, root_hash: Buffer.from(batch.root, 'hex'), timestamp: Date.now(), }; expect(head.tree_size).toBe(4); expect(head.root_hash).toBeInstanceOf(Uint8Array); expect(head.root_hash.length).toBe(32); // SHA-256 expect(Number.isFinite(head.timestamp)).toBe(true); }); }); describe('IETF alias — buildMerkleRootFromLeaves also wires aliases', () => { it('aliases are present on proofs from the lower-level primitive', () => { const leaves = Array.from({ length: 4 }, (_, i) => 'deadbeef'.repeat(8).slice(0, 60) + i.toString(16).padStart(4, '0'), ); const batch = buildMerkleRootFromLeaves(leaves); const p = batch.inclusion_proof(1); expect(p.leaf_index).toBe(1); expect(p.tree_size).toBe(4); expect(p.root_hash).toBe(p.root); expect(p.audit_path).toEqual(p.siblings); }); });