mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-09-10 19:26:00 +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
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue