mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 23:07:29 +00:00
feat(ans): implement @logicsrc/ans M1 — resolver + offline verifier
M1 of the ANS SDK (docs/ans-sdk.md): - name: parse/format ans://v<semver>.<agent>.<domain> - cbor: minimal RFC 8949 codec for the COSE_Sign1 subset - verify/merkle: RFC 6962 leaf/node hashing, tree build, inclusion-proof generation + verification - verify/es256 + cose: COSE_Sign1 build/parse, Sig_structure, ES256 (WebCrypto) - verify/rootkeys: kid -> verifier key (JWKS entries; sumdb-note is M2) - verify: verifyReceipt() + verifyResolution() (signature + inclusion proof + name binding), pure and offline - client: AnsClient resolve/rootKeys/register/status over injectable fetch, with a DnsApplier hook for verify-dns Tests (24) cover name parsing, CBOR round-trips/vectors, RFC 6962 proofs, a full ES256+Merkle receipt round-trip with positive/negative cases, and the client against mocked fetch. Wired into the root build script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0ae24719e3
commit
29975b2df7
20 changed files with 1009 additions and 28 deletions
114
packages/ans/src/verify/cose.ts
Normal file
114
packages/ans/src/verify/cose.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// COSE_Sign1 (RFC 8152) construction + parsing, plus the M1 ANS receipt
|
||||
// envelope model.
|
||||
//
|
||||
// A receipt is a COSE_Sign1 (CBOR tag 18) over the agent identity statement
|
||||
// (the payload). The transparency-log inclusion proof is carried in an
|
||||
// unprotected header under the label "ans-proof":
|
||||
//
|
||||
// { root: bstr, index: uint, size: uint, path: [bstr] }
|
||||
//
|
||||
// NOTE: this envelope shape is the LogicSRC M1 model. Upstream ANS may place the
|
||||
// proof in a different (registered SCITT) header; M2 pins this against fixtures
|
||||
// captured from the upstream Go server and adjusts `parseReceipt` accordingly.
|
||||
|
||||
import { CborTag, decodeCbor, encodeCbor, type CborValue } from '../cbor.js';
|
||||
import { toHex } from '../bytes.js';
|
||||
|
||||
export const COSE_SIGN1_TAG = 18;
|
||||
export const COSE_ALG_ES256 = -7;
|
||||
const HEADER_ALG = 1;
|
||||
const HEADER_KID = 4;
|
||||
const HEADER_ANS_PROOF = 'ans-proof';
|
||||
|
||||
export interface InclusionProof {
|
||||
root: Uint8Array;
|
||||
index: number;
|
||||
size: number;
|
||||
path: Uint8Array[];
|
||||
}
|
||||
|
||||
export interface ReceiptEnvelope {
|
||||
/** lowercase hex of the 4-byte kid header. */
|
||||
kidHex: string;
|
||||
alg: number;
|
||||
payload: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
proof: InclusionProof;
|
||||
/** Raw CBOR of the protected header map (signed). */
|
||||
protectedBytes: Uint8Array;
|
||||
}
|
||||
|
||||
/** Canonical COSE Sig_structure bytes the signature covers. */
|
||||
export function sigStructure(protectedBytes: Uint8Array, payload: Uint8Array): Uint8Array {
|
||||
return encodeCbor(['Signature1', protectedBytes, new Uint8Array(0), payload]);
|
||||
}
|
||||
|
||||
/** Build a COSE_Sign1 receipt. Used by tests/tools; the signer provides ES256(r||s). */
|
||||
export async function buildReceipt(opts: {
|
||||
kid: Uint8Array;
|
||||
payload: Uint8Array;
|
||||
proof: InclusionProof;
|
||||
sign: (message: Uint8Array) => Promise<Uint8Array>;
|
||||
}): Promise<Uint8Array> {
|
||||
const protectedBytes = encodeCbor(new Map<CborValue, CborValue>([[HEADER_ALG, COSE_ALG_ES256]]));
|
||||
const proofMap = new Map<CborValue, CborValue>([
|
||||
['root', opts.proof.root],
|
||||
['index', opts.proof.index],
|
||||
['size', opts.proof.size],
|
||||
['path', opts.proof.path as CborValue[]],
|
||||
]);
|
||||
const unprotected = new Map<CborValue, CborValue>([
|
||||
[HEADER_KID, opts.kid],
|
||||
[HEADER_ANS_PROOF, proofMap],
|
||||
]);
|
||||
const signature = await opts.sign(sigStructure(protectedBytes, opts.payload));
|
||||
const sign1 = new CborTag(COSE_SIGN1_TAG, [protectedBytes, unprotected, opts.payload, signature]);
|
||||
return encodeCbor(sign1);
|
||||
}
|
||||
|
||||
/** Parse COSE_Sign1 receipt bytes into a structured envelope. */
|
||||
export function parseReceipt(cbor: Uint8Array): ReceiptEnvelope {
|
||||
let decoded = decodeCbor(cbor);
|
||||
if (decoded instanceof CborTag) {
|
||||
if (decoded.tag !== COSE_SIGN1_TAG) throw new Error(`receipt: unexpected CBOR tag ${decoded.tag}`);
|
||||
decoded = decoded.value;
|
||||
}
|
||||
if (!Array.isArray(decoded) || decoded.length !== 4) throw new Error('receipt: not a COSE_Sign1 array');
|
||||
|
||||
const [protectedBytes, unprotected, payload, signature] = decoded;
|
||||
if (!(protectedBytes instanceof Uint8Array)) throw new Error('receipt: bad protected header');
|
||||
if (!(unprotected instanceof Map)) throw new Error('receipt: bad unprotected header');
|
||||
if (!(payload instanceof Uint8Array)) throw new Error('receipt: detached/empty payload unsupported in M1');
|
||||
if (!(signature instanceof Uint8Array)) throw new Error('receipt: bad signature');
|
||||
|
||||
const protectedMap = decodeCbor(protectedBytes);
|
||||
const alg = protectedMap instanceof Map ? protectedMap.get(HEADER_ALG) : undefined;
|
||||
|
||||
const kid = unprotected.get(HEADER_KID) ?? (protectedMap instanceof Map ? protectedMap.get(HEADER_KID) : undefined);
|
||||
if (!(kid instanceof Uint8Array)) throw new Error('receipt: missing kid header');
|
||||
|
||||
const proofRaw = unprotected.get(HEADER_ANS_PROOF);
|
||||
if (!(proofRaw instanceof Map)) throw new Error('receipt: missing ans-proof header');
|
||||
const proof = parseProof(proofRaw);
|
||||
|
||||
return {
|
||||
kidHex: toHex(kid),
|
||||
alg: typeof alg === 'number' ? alg : COSE_ALG_ES256,
|
||||
payload,
|
||||
signature,
|
||||
proof,
|
||||
protectedBytes,
|
||||
};
|
||||
}
|
||||
|
||||
function parseProof(map: Map<CborValue, CborValue>): InclusionProof {
|
||||
const root = map.get('root');
|
||||
const index = map.get('index');
|
||||
const size = map.get('size');
|
||||
const path = map.get('path');
|
||||
if (!(root instanceof Uint8Array)) throw new Error('receipt: proof.root missing');
|
||||
if (typeof index !== 'number') throw new Error('receipt: proof.index missing');
|
||||
if (typeof size !== 'number') throw new Error('receipt: proof.size missing');
|
||||
if (!Array.isArray(path) || path.some((p) => !(p instanceof Uint8Array))) throw new Error('receipt: proof.path malformed');
|
||||
return { root, index, size, path: path as Uint8Array[] };
|
||||
}
|
||||
15
packages/ans/src/verify/es256.ts
Normal file
15
packages/ans/src/verify/es256.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// ES256 (ECDSA P-256 / SHA-256) verification via WebCrypto. COSE signatures are
|
||||
// raw IEEE-P1363 (r || s, 64 bytes), which is exactly what WebCrypto expects.
|
||||
|
||||
export function importEs256VerifyKey(jwk: JsonWebKey): Promise<CryptoKey> {
|
||||
return crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
|
||||
}
|
||||
|
||||
export function verifyEs256(key: CryptoKey, signature: Uint8Array, message: Uint8Array): Promise<boolean> {
|
||||
return crypto.subtle.verify(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
key,
|
||||
signature as unknown as ArrayBuffer,
|
||||
message as unknown as ArrayBuffer,
|
||||
);
|
||||
}
|
||||
61
packages/ans/src/verify/index.ts
Normal file
61
packages/ans/src/verify/index.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Offline ANS verifier. No network: given root keys (fetched once or pinned),
|
||||
// it cryptographically verifies a receipt against the transparency log without
|
||||
// trusting the registry operator.
|
||||
|
||||
import { parseReceipt, sigStructure } from './cose.js';
|
||||
import { verifyEs256 } from './es256.js';
|
||||
import { leafHash, verifyInclusion } from './merkle.js';
|
||||
import { toHex, fromUtf8 } from '../bytes.js';
|
||||
import { toAnsName } from '../name.js';
|
||||
import type { AnsIdentity, Receipt, VerifyOptions, VerifyResult } from '../types.js';
|
||||
|
||||
/**
|
||||
* Verify a SCITT COSE_Sign1 inclusion receipt:
|
||||
* 1. map kid -> root verifier key
|
||||
* 2. ES256-verify the signature over the COSE Sig_structure
|
||||
* 3. recompute the leaf hash (RFC 6962) and walk the inclusion proof to the root
|
||||
*/
|
||||
export async function verifyReceipt(receipt: Receipt, opts: VerifyOptions): Promise<VerifyResult> {
|
||||
let env;
|
||||
try {
|
||||
env = parseReceipt(receipt.cbor);
|
||||
} catch (error) {
|
||||
return { ok: false, reason: `parse: ${(error as Error).message}` };
|
||||
}
|
||||
|
||||
const key = opts.rootKeys.keys.get(env.kidHex);
|
||||
if (!key) return { ok: false, reason: `unknown kid ${env.kidHex}` };
|
||||
|
||||
const message = sigStructure(env.protectedBytes, env.payload);
|
||||
const signatureOk = await verifyEs256(key, env.signature, message);
|
||||
if (!signatureOk) return { ok: false, reason: 'invalid signature' };
|
||||
|
||||
const leaf = await leafHash(env.payload);
|
||||
const inclusionOk = await verifyInclusion({
|
||||
index: env.proof.index,
|
||||
size: env.proof.size,
|
||||
leafHash: leaf,
|
||||
auditPath: env.proof.path,
|
||||
root: env.proof.root,
|
||||
});
|
||||
if (!inclusionOk) return { ok: false, reason: 'inclusion proof failed' };
|
||||
|
||||
return { ok: true, rootHashHex: toHex(env.proof.root) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a resolved identity: the receipt must verify AND its signed payload
|
||||
* must bind the resolved ans:// name.
|
||||
*/
|
||||
export async function verifyResolution(identity: AnsIdentity, opts: VerifyOptions): Promise<VerifyResult> {
|
||||
const result = await verifyReceipt(identity.receipt, opts);
|
||||
if (!result.ok) return result;
|
||||
|
||||
const env = parseReceipt(identity.receipt.cbor);
|
||||
const signedName = fromUtf8(env.payload).trim();
|
||||
const expected = toAnsName(identity.name).raw;
|
||||
if (signedName !== expected) {
|
||||
return { ok: false, reason: `name binding mismatch: receipt signs ${signedName}, resolved ${expected}` };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
38
packages/ans/src/verify/merkle.test.ts
Normal file
38
packages/ans/src/verify/merkle.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { inclusionProof, leafHash, merkleRoot, nodeHash, verifyInclusion } from './merkle.js';
|
||||
import { utf8, toHex } from '../bytes.js';
|
||||
|
||||
const leaves = ['a', 'b', 'c', 'd'].map(utf8);
|
||||
|
||||
describe('RFC 6962 merkle tree', () => {
|
||||
it('computes the root as node(node(la,lb), node(lc,ld)) for 4 leaves', async () => {
|
||||
const [la, lb, lc, ld] = await Promise.all(leaves.map(leafHash));
|
||||
const expected = await nodeHash(await nodeHash(la, lb), await nodeHash(lc, ld));
|
||||
expect(toHex(await merkleRoot(leaves))).toBe(toHex(expected));
|
||||
});
|
||||
|
||||
it('verifies inclusion proofs for every leaf', async () => {
|
||||
const root = await merkleRoot(leaves);
|
||||
for (let i = 0; i < leaves.length; i++) {
|
||||
const auditPath = await inclusionProof(leaves, i);
|
||||
const ok = await verifyInclusion({ index: i, size: leaves.length, leafHash: await leafHash(leaves[i]), auditPath, root });
|
||||
expect(ok).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('verifies inclusion for an odd-sized (5-leaf) tree', async () => {
|
||||
const five = ['a', 'b', 'c', 'd', 'e'].map(utf8);
|
||||
const root = await merkleRoot(five);
|
||||
const auditPath = await inclusionProof(five, 4);
|
||||
expect(await verifyInclusion({ index: 4, size: 5, leafHash: await leafHash(five[4]), auditPath, root })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a tampered proof, wrong index, and out-of-range index', async () => {
|
||||
const root = await merkleRoot(leaves);
|
||||
const auditPath = await inclusionProof(leaves, 1);
|
||||
const lb = await leafHash(leaves[1]);
|
||||
expect(await verifyInclusion({ index: 1, size: 4, leafHash: await leafHash(utf8('x')), auditPath, root })).toBe(false);
|
||||
expect(await verifyInclusion({ index: 2, size: 4, leafHash: lb, auditPath, root })).toBe(false);
|
||||
expect(await verifyInclusion({ index: 9, size: 4, leafHash: lb, auditPath, root })).toBe(false);
|
||||
});
|
||||
});
|
||||
85
packages/ans/src/verify/merkle.ts
Normal file
85
packages/ans/src/verify/merkle.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// RFC 6962 Merkle tree hashing + inclusion-proof verification.
|
||||
//
|
||||
// Leaf hash: SHA-256(0x00 || data)
|
||||
// Node hash: SHA-256(0x01 || left || right)
|
||||
//
|
||||
// The tree builder + proof generator are included so the verifier can be tested
|
||||
// against self-consistent vectors (and against hand-derived small trees) before
|
||||
// upstream fixtures are available.
|
||||
|
||||
import { sha256, concat, bytesEqual } from '../bytes.js';
|
||||
|
||||
const LEAF_PREFIX = new Uint8Array([0x00]);
|
||||
const NODE_PREFIX = new Uint8Array([0x01]);
|
||||
|
||||
export function leafHash(data: Uint8Array): Promise<Uint8Array> {
|
||||
return sha256(concat(LEAF_PREFIX, data));
|
||||
}
|
||||
|
||||
export function nodeHash(left: Uint8Array, right: Uint8Array): Promise<Uint8Array> {
|
||||
return sha256(concat(NODE_PREFIX, left, right));
|
||||
}
|
||||
|
||||
/** Largest power of two strictly less than n (n >= 2). */
|
||||
function splitPoint(n: number): number {
|
||||
let k = 1;
|
||||
while (k * 2 < n) k *= 2;
|
||||
return k;
|
||||
}
|
||||
|
||||
/** Merkle Tree Hash (MTH) over a list of leaf payloads. */
|
||||
export async function merkleRoot(leaves: Uint8Array[]): Promise<Uint8Array> {
|
||||
if (leaves.length === 0) return sha256(new Uint8Array(0));
|
||||
if (leaves.length === 1) return leafHash(leaves[0]);
|
||||
const k = splitPoint(leaves.length);
|
||||
const [left, right] = await Promise.all([merkleRoot(leaves.slice(0, k)), merkleRoot(leaves.slice(k))]);
|
||||
return nodeHash(left, right);
|
||||
}
|
||||
|
||||
/** Audit path for the leaf at `index`, leaf-to-root order. */
|
||||
export async function inclusionProof(leaves: Uint8Array[], index: number): Promise<Uint8Array[]> {
|
||||
if (index < 0 || index >= leaves.length) throw new Error('inclusionProof: index out of range');
|
||||
if (leaves.length <= 1) return [];
|
||||
const k = splitPoint(leaves.length);
|
||||
if (index < k) {
|
||||
const sub = await inclusionProof(leaves.slice(0, k), index);
|
||||
return [...sub, await merkleRoot(leaves.slice(k))];
|
||||
}
|
||||
const sub = await inclusionProof(leaves.slice(k), index - k);
|
||||
return [...sub, await merkleRoot(leaves.slice(0, k))];
|
||||
}
|
||||
|
||||
async function rootFromProof(index: number, size: number, leaf: Uint8Array, path: Uint8Array[]): Promise<Uint8Array> {
|
||||
if (size <= 1) {
|
||||
if (path.length !== 0) throw new Error('inclusion proof: path too long');
|
||||
return leaf;
|
||||
}
|
||||
if (path.length === 0) throw new Error('inclusion proof: path too short');
|
||||
const k = splitPoint(size);
|
||||
const sibling = path[path.length - 1];
|
||||
const rest = path.slice(0, -1);
|
||||
if (index < k) {
|
||||
const sub = await rootFromProof(index, k, leaf, rest);
|
||||
return nodeHash(sub, sibling);
|
||||
}
|
||||
const sub = await rootFromProof(index - k, size - k, leaf, rest);
|
||||
return nodeHash(sibling, sub);
|
||||
}
|
||||
|
||||
export interface InclusionInput {
|
||||
index: number;
|
||||
size: number;
|
||||
leafHash: Uint8Array;
|
||||
auditPath: Uint8Array[];
|
||||
root: Uint8Array;
|
||||
}
|
||||
|
||||
export async function verifyInclusion(input: InclusionInput): Promise<boolean> {
|
||||
if (input.index < 0 || input.index >= input.size) return false;
|
||||
try {
|
||||
const computed = await rootFromProof(input.index, input.size, input.leafHash, input.auditPath);
|
||||
return bytesEqual(computed, input.root);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
30
packages/ans/src/verify/rootkeys.ts
Normal file
30
packages/ans/src/verify/rootkeys.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Transparency-log verifier keys, indexed by 4-byte kid (lowercase hex).
|
||||
//
|
||||
// M1 accepts a JWKS-style list ({ kid, jwk }). The upstream sumdb-note
|
||||
// (`/root-keys`) parser is M2 (TODO) — captured against fixtures.
|
||||
|
||||
import { importEs256VerifyKey } from './es256.js';
|
||||
import { fromHex, toHex } from '../bytes.js';
|
||||
import type { RootKeys } from '../types.js';
|
||||
|
||||
export interface RootKeyEntry {
|
||||
/** 4-byte key id as hex (e.g. "01020304") or any string the receipts use. */
|
||||
kid: string;
|
||||
jwk: JsonWebKey;
|
||||
}
|
||||
|
||||
export async function rootKeysFromEntries(entries: RootKeyEntry[]): Promise<RootKeys> {
|
||||
const keys = new Map<string, CryptoKey>();
|
||||
for (const entry of entries) {
|
||||
const kidHex = normalizeKid(entry.kid);
|
||||
keys.set(kidHex, await importEs256VerifyKey(entry.jwk));
|
||||
}
|
||||
return { keys };
|
||||
}
|
||||
|
||||
/** Accept kid as hex ("01020304") or as already-normalized; lowercases hex. */
|
||||
function normalizeKid(kid: string): string {
|
||||
const lower = kid.toLowerCase();
|
||||
if (/^[0-9a-f]+$/.test(lower) && lower.length % 2 === 0) return toHex(fromHex(lower));
|
||||
return lower;
|
||||
}
|
||||
86
packages/ans/src/verify/verify.test.ts
Normal file
86
packages/ans/src/verify/verify.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { buildReceipt } from './cose.js';
|
||||
import { inclusionProof, leafHash, merkleRoot } from './merkle.js';
|
||||
import { rootKeysFromEntries } from './rootkeys.js';
|
||||
import { verifyReceipt, verifyResolution } from './index.js';
|
||||
import { utf8 } from '../bytes.js';
|
||||
import type { AnsIdentity, RootKeys } from '../types.js';
|
||||
|
||||
const KID = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
|
||||
const NAME = 'ans://v1.0.0.my-agent.example.com';
|
||||
const payload = utf8(NAME);
|
||||
const otherLeaves = [utf8('sibling-0'), utf8('sibling-2'), utf8('sibling-3')];
|
||||
const leaves = [otherLeaves[0], payload, otherLeaves[1], otherLeaves[2]];
|
||||
const INDEX = 1;
|
||||
|
||||
let keyPair: CryptoKeyPair;
|
||||
let rootKeys: RootKeys;
|
||||
let receiptCbor: Uint8Array;
|
||||
|
||||
async function sign(message: Uint8Array): Promise<Uint8Array> {
|
||||
const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, keyPair.privateKey, message as unknown as ArrayBuffer);
|
||||
return new Uint8Array(sig);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
keyPair = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])) as CryptoKeyPair;
|
||||
const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
rootKeys = await rootKeysFromEntries([{ kid: '01020304', jwk }]);
|
||||
|
||||
const root = await merkleRoot(leaves);
|
||||
const path = await inclusionProof(leaves, INDEX);
|
||||
receiptCbor = await buildReceipt({ kid: KID, payload, proof: { root, index: INDEX, size: leaves.length, path }, sign });
|
||||
});
|
||||
|
||||
describe('offline receipt verification', () => {
|
||||
it('verifies a well-formed receipt (signature + inclusion proof)', async () => {
|
||||
const result = await verifyReceipt({ cbor: receiptCbor }, { rootKeys });
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.rootHashHex).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it('verifies a resolution whose payload binds the resolved name', async () => {
|
||||
const identity: AnsIdentity = {
|
||||
name: NAME as never, // toAnsName normalizes the string form
|
||||
capabilities: [],
|
||||
events: [],
|
||||
receipt: { cbor: receiptCbor },
|
||||
} as unknown as AnsIdentity;
|
||||
const result = await verifyResolution({ ...identity, name: { raw: NAME, version: '1.0.0', agent: 'my-agent', domain: 'example.com' } }, { rootKeys });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an unknown kid', async () => {
|
||||
const empty = await rootKeysFromEntries([]);
|
||||
const result = await verifyReceipt({ cbor: receiptCbor }, { rootKeys: empty });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toContain('unknown kid');
|
||||
});
|
||||
|
||||
it('rejects a receipt signed by a different key', async () => {
|
||||
const other = (await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])) as CryptoKeyPair;
|
||||
const wrongJwk = await crypto.subtle.exportKey('jwk', other.publicKey);
|
||||
const wrongKeys = await rootKeysFromEntries([{ kid: '01020304', jwk: wrongJwk }]);
|
||||
const result = await verifyReceipt({ cbor: receiptCbor }, { rootKeys: wrongKeys });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('invalid signature');
|
||||
});
|
||||
|
||||
it('rejects a tampered inclusion proof (wrong root)', async () => {
|
||||
const badRoot = new Uint8Array(32).fill(0xff);
|
||||
const path = await inclusionProof(leaves, INDEX);
|
||||
const tampered = await buildReceipt({ kid: KID, payload, proof: { root: badRoot, index: INDEX, size: leaves.length, path }, sign });
|
||||
const result = await verifyReceipt({ cbor: tampered }, { rootKeys });
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('inclusion proof failed');
|
||||
});
|
||||
|
||||
it('rejects a resolution whose name does not match the signed payload', async () => {
|
||||
const result = await verifyResolution(
|
||||
{ name: { raw: 'ans://v1.0.0.other.example.com', version: '1.0.0', agent: 'other', domain: 'example.com' }, capabilities: [], events: [], receipt: { cbor: receiptCbor } },
|
||||
{ rootKeys },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toContain('name binding mismatch');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue