@hashborn/verify 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/remote.js ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Reading a published record from the Hashborn API and verifying it here. The API is only a
3
+ * transport: the server's own `verified` flag is never read. The record text, signature, root and
4
+ * pubSeed it returns are checked locally with SHA-256, exactly as `verifyRecord` checks them.
5
+ *
6
+ * Source: `GET /api/v1/records/<id>`. When that answer carries no signature (the v1 record view
7
+ * leaves the 2,404 signature bytes out), the published bytes are read from
8
+ * `GET /api/index?q=record&id=<id>`, the same index route the site's pages read.
9
+ */
10
+ import { pqAddress } from './address.js';
11
+ import { DEFAULT_BASE_URL, PQ } from './constants.js';
12
+ import { HashbornError } from './errors.js';
13
+ import { decodeSignature } from './signature.js';
14
+ import { verifyRecord } from './verify.js';
15
+ export const RECORD_ID_RE = /^0x[0-9a-fA-F]{72}$/;
16
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
17
+ const str = (v, label) => {
18
+ if (typeof v !== 'string')
19
+ throw new HashbornError(`the API answer has no ${label}`);
20
+ return v;
21
+ };
22
+ async function getJson(fetcher, url) {
23
+ let res;
24
+ try {
25
+ res = await fetcher(url, { headers: { accept: 'application/json' } });
26
+ }
27
+ catch (err) {
28
+ throw new HashbornError(`could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`, 'network');
29
+ }
30
+ let body = null;
31
+ try {
32
+ body = await res.json();
33
+ }
34
+ catch {
35
+ if (res.ok)
36
+ throw new HashbornError(`${url} did not answer with JSON`, 'network');
37
+ }
38
+ return { status: res.status, body };
39
+ }
40
+ function parseFields(raw) {
41
+ try {
42
+ const value = JSON.parse(raw);
43
+ if (!isObj(value))
44
+ return null;
45
+ for (const v of Object.values(value))
46
+ if (v !== null && !['string', 'number', 'boolean'].includes(typeof v))
47
+ return null;
48
+ return value;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function fromV1(b) {
55
+ if (typeof b.signature !== 'string' || typeof b.record !== 'string')
56
+ return null;
57
+ const signer = isObj(b.signer) ? b.signer : {};
58
+ const anchored = isObj(b.anchored) ? b.anchored : {};
59
+ return {
60
+ domain: str(b.domain, 'domain'),
61
+ record: b.record,
62
+ signature: b.signature,
63
+ root: str(signer.root, 'signer.root'),
64
+ pubSeed: str(signer.pubSeed, 'signer.pubSeed'),
65
+ messageHash: str(b.messageHash, 'messageHash'),
66
+ leaf: Number(b.leaf),
67
+ wallet: str(signer.wallet, 'signer.wallet'),
68
+ txHash: str(anchored.txHash, 'anchored.txHash'),
69
+ blockNumber: Number(anchored.block),
70
+ anchoredAt: str(anchored.at, 'anchored.at'),
71
+ chainId: typeof anchored.chainId === 'number' ? anchored.chainId : null,
72
+ };
73
+ }
74
+ function fromIndex(r, chainId) {
75
+ const identity = isObj(r.identity) ? r.identity : {};
76
+ return {
77
+ domain: str(r.domain, 'domain'),
78
+ record: str(r.record, 'record'),
79
+ signature: str(r.signature, 'signature'),
80
+ root: str(r.root, 'root'),
81
+ pubSeed: str(identity.pubSeed, 'identity.pubSeed'),
82
+ messageHash: str(r.messageHash, 'messageHash'),
83
+ leaf: Number(r.leaf),
84
+ wallet: str(r.wallet, 'wallet'),
85
+ txHash: str(r.txHash, 'txHash'),
86
+ blockNumber: Number(r.blockNumber),
87
+ anchoredAt: new Date(Number(r.timestamp) * 1000).toISOString(),
88
+ chainId,
89
+ };
90
+ }
91
+ /**
92
+ * Reads one anchored record (id = anchoring transaction hash + log index, as on
93
+ * `/records/<id>`) and builds its attestation from the published bytes. Does not verify: pass the
94
+ * result to `verifyRecord(r.attestation, r.record)`, or call `verifyRemote`.
95
+ * Throws a HashbornError (`not-found`, `network`, `malformed`) when there is nothing to verify.
96
+ */
97
+ export async function fetchRecord(id, options = {}) {
98
+ if (typeof id !== 'string' || !RECORD_ID_RE.test(id))
99
+ throw new HashbornError('a record id is 0x followed by 72 hex characters');
100
+ const recordId = id.toLowerCase();
101
+ const fetcher = options.fetch ?? globalThis.fetch;
102
+ if (!fetcher)
103
+ throw new HashbornError('no fetch implementation; pass options.fetch', 'network');
104
+ const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
105
+ const v1 = await getJson(fetcher, `${base}/api/v1/records/${recordId}`);
106
+ if (v1.status === 404)
107
+ throw new HashbornError(`no record with id ${recordId}`, 'not-found');
108
+ if (v1.status !== 200 || !isObj(v1.body)) {
109
+ const msg = isObj(v1.body) && typeof v1.body.error === 'string' ? `: ${v1.body.error}` : '';
110
+ throw new HashbornError(`the API answered ${v1.status}${msg}`, 'network');
111
+ }
112
+ const chainId = isObj(v1.body.anchored) && typeof v1.body.anchored.chainId === 'number' ? v1.body.anchored.chainId : null;
113
+ let published = fromV1(v1.body);
114
+ let source = 'v1';
115
+ if (!published) {
116
+ const idx = await getJson(fetcher, `${base}/api/index?q=record&id=${recordId}`);
117
+ if (idx.status !== 200 || !isObj(idx.body))
118
+ throw new HashbornError(`the index answered ${idx.status}`, 'network');
119
+ if (idx.body.data === null)
120
+ throw new HashbornError(`no record with id ${recordId}`, 'not-found');
121
+ if (!isObj(idx.body.data))
122
+ throw new HashbornError('the index answer has no record');
123
+ published = fromIndex(idx.body.data, chainId);
124
+ source = 'index';
125
+ }
126
+ if (published.signature.length !== 2 + PQ.signatureBytes * 2) {
127
+ throw new HashbornError(`the published signature is not ${PQ.signatureBytes} bytes`);
128
+ }
129
+ const signature = decodeSignature(published.signature);
130
+ const attestation = {
131
+ version: 1,
132
+ scheme: 'wots-merkle',
133
+ domain: published.domain,
134
+ pqAddress: pqAddress(published),
135
+ root: published.root,
136
+ pubSeed: published.pubSeed,
137
+ height: PQ.height,
138
+ messageHash: published.messageHash,
139
+ signature,
140
+ txHash: published.txHash,
141
+ anchoredAt: published.anchoredAt,
142
+ };
143
+ return {
144
+ id: recordId,
145
+ attestation,
146
+ record: published.record,
147
+ fields: parseFields(published.record),
148
+ leaf: published.leaf,
149
+ wallet: published.wallet,
150
+ txHash: published.txHash,
151
+ blockNumber: published.blockNumber,
152
+ anchoredAt: published.anchoredAt,
153
+ chainId: published.chainId,
154
+ source,
155
+ };
156
+ }
157
+ /**
158
+ * `fetchRecord` then `verifyRecord` here, on the published bytes: the record text must hash to
159
+ * `messageHash`, the signature must climb to the identity's root, and the leaf the anchor burned
160
+ * must be the leaf that signed. The server's own verdict is never used.
161
+ */
162
+ export async function verifyRemote(id, options = {}) {
163
+ const remote = await fetchRecord(id, options);
164
+ const result = verifyRecord(remote.attestation, remote.record);
165
+ if (result.valid && remote.leaf !== remote.attestation.signature.leaf) {
166
+ return {
167
+ ...result,
168
+ valid: false,
169
+ code: 'leaf',
170
+ reason: `The anchor burned leaf ${remote.leaf} but the signature names leaf ${remote.attestation.signature.leaf}.`,
171
+ id: remote.id,
172
+ remote,
173
+ };
174
+ }
175
+ return { ...result, id: remote.id, remote };
176
+ }
@@ -0,0 +1,6 @@
1
+ /** The 2,404-byte wire form of a signature, as PQRegistry stores it: leaf u32 || sigma || auth. */
2
+ import { type Hex, type MerkleSignature } from './constants.js';
3
+ /** `leaf u32 || sigma_0..66 || auth_0..7`, the wire form of PROTOCOL-V1 section 5. */
4
+ export declare function encodeSignature(signature: MerkleSignature): Hex;
5
+ /** The wire form back into a signature: the inverse of `encodeSignature`. */
6
+ export declare function decodeSignature(wire: Hex): MerkleSignature;
@@ -0,0 +1,26 @@
1
+ /** The 2,404-byte wire form of a signature, as PQRegistry stores it: leaf u32 || sigma || auth. */
2
+ import { fromHex, toHex } from './bytes.js';
3
+ import { PQ } from './constants.js';
4
+ import { HashbornError } from './errors.js';
5
+ const CHAINS = PQ.len * PQ.n;
6
+ /** `leaf u32 || sigma_0..66 || auth_0..7`, the wire form of PROTOCOL-V1 section 5. */
7
+ export function encodeSignature(signature) {
8
+ const out = new Uint8Array(PQ.signatureBytes);
9
+ if (!Number.isInteger(signature.leaf) || signature.leaf < 0 || signature.leaf >= PQ.leaves) {
10
+ throw new HashbornError(`leaf index must be an integer in 0..${PQ.leaves - 1}`);
11
+ }
12
+ new DataView(out.buffer).setUint32(0, signature.leaf, false);
13
+ out.set(fromHex(signature.wots, CHAINS, 'signature.wots'), 4);
14
+ if (!Array.isArray(signature.auth) || signature.auth.length !== PQ.height) {
15
+ throw new HashbornError(`signature.auth must have ${PQ.height} nodes`);
16
+ }
17
+ signature.auth.forEach((node, i) => out.set(fromHex(node, PQ.n, `signature.auth[${i}]`), 4 + CHAINS + i * PQ.n));
18
+ return toHex(out);
19
+ }
20
+ /** The wire form back into a signature: the inverse of `encodeSignature`. */
21
+ export function decodeSignature(wire) {
22
+ const bytes = fromHex(wire, PQ.signatureBytes, 'signature');
23
+ const leaf = new DataView(bytes.buffer, bytes.byteOffset).getUint32(0, false);
24
+ const auth = Array.from({ length: PQ.height }, (_, i) => toHex(bytes.slice(4 + CHAINS + i * PQ.n, 4 + CHAINS + (i + 1) * PQ.n)));
25
+ return { leaf, wots: toHex(bytes.slice(4, 4 + CHAINS)), auth };
26
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Verification of protocol v1 attestations with SHA-256 alone:
3
+ * finish the 67 WOTS chains over `messageHash`, hash the leaf, climb the 8-level auth path,
4
+ * compare with `root`, and check that `pqAddress` derives from `root` and `pubSeed`.
5
+ */
6
+ import { type DigestFields, type Hex, type MerkleSignature } from './constants.js';
7
+ export interface SignatureCheck {
8
+ valid: boolean;
9
+ /** The root the signature climbs to. */
10
+ computedRoot: Hex;
11
+ /** SHA-256 calls made finishing the 67 chains: 45 to 990. */
12
+ hashCalls: number;
13
+ }
14
+ /**
15
+ * Low level: does `signature` over `messageHash` climb to `root`? Throws a HashbornError on
16
+ * malformed input (bad hex, wrong sizes, a leaf index outside 0..255).
17
+ */
18
+ export declare function verifySignature(messageHash: Hex, signature: MerkleSignature, pub: {
19
+ root: Hex;
20
+ pubSeed: Hex;
21
+ }): SignatureCheck;
22
+ /** Why a check failed. */
23
+ export type FailureCode =
24
+ /** Not an attestation: a missing field, bad hex, a wrong size, a leaf outside 0..255. */
25
+ 'malformed'
26
+ /** Well formed, but not protocol v1: another version, scheme, height or domain. */
27
+ | 'unsupported'
28
+ /** The signature does not climb to the root over messageHash. */
29
+ | 'signature'
30
+ /** pqAddress does not derive from root and pubSeed. */
31
+ | 'pq-address'
32
+ /** The record's fields do not hash to messageHash. */
33
+ | 'digest'
34
+ /** The record and its signature name different leaves. */
35
+ | 'leaf';
36
+ export interface VerifyResult {
37
+ valid: boolean;
38
+ /** Set when `valid` is false. */
39
+ code?: FailureCode;
40
+ /** One plain sentence, set when `valid` is false. */
41
+ reason?: string;
42
+ domain: string | null;
43
+ /** The leaf index that signed. */
44
+ leaf: number | null;
45
+ /** The signer's pq address, derived from root and pubSeed. */
46
+ pqAddress: string | null;
47
+ messageHash: Hex | null;
48
+ /** The identity root the attestation names. */
49
+ root: Hex | null;
50
+ /** The root the signature climbs to; equal to `root` when the signature is valid. */
51
+ computedRoot: Hex | null;
52
+ /** SHA-256 calls made finishing the chains (45 to 990), 0 when they were not run. */
53
+ hashCalls: number;
54
+ }
55
+ /**
56
+ * Verifies an attestation with SHA-256 alone: its shape and sizes, protocol v1 fields (version 1,
57
+ * scheme wots-merkle, height 8, a v1 domain), the WOTS signature and Merkle path over
58
+ * `messageHash` against `root` and `pubSeed`, and that `pqAddress` derives from them.
59
+ * Never throws: a bad input is `{valid: false, code, reason}`.
60
+ *
61
+ * It does not check that the attestation was anchored on chain, nor what `messageHash` stands
62
+ * for: use `verifyRecord` with the record's fields for that.
63
+ */
64
+ export declare function verifyAttestation(attestation: unknown): VerifyResult;
65
+ export interface RecordVerifyResult extends VerifyResult {
66
+ /** The digest recomputed from the record; equal to `messageHash` when they match. */
67
+ recordDigest: Hex | null;
68
+ }
69
+ /**
70
+ * `verifyAttestation` plus the record: recomputes the digest from `record` under the
71
+ * attestation's domain and requires it to equal `messageHash`. `record` is either the fields
72
+ * object (hashed as canonical JSON, keys sorted) or the record text exactly as published on chain.
73
+ */
74
+ export declare function verifyRecord(attestation: unknown, record: DigestFields | string): RecordVerifyResult;
package/dist/verify.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Verification of protocol v1 attestations with SHA-256 alone:
3
+ * finish the 67 WOTS chains over `messageHash`, hash the leaf, climb the 8-level auth path,
4
+ * compare with `root`, and check that `pqAddress` derives from `root` and `pubSeed`.
5
+ */
6
+ import { pqAddress as derivePqAddress } from './address.js';
7
+ import { checkAttestation } from './attestation.js';
8
+ import { equalBytes, fromHex, toHex } from './bytes.js';
9
+ import { PQ } from './constants.js';
10
+ import { canonicalJson, recordDigest } from './digest.js';
11
+ import { HashbornError } from './errors.js';
12
+ import { climb, leafHash } from './merkle.js';
13
+ import { decodeSigma, wotsVerifyPk } from './wots.js';
14
+ /**
15
+ * Low level: does `signature` over `messageHash` climb to `root`? Throws a HashbornError on
16
+ * malformed input (bad hex, wrong sizes, a leaf index outside 0..255).
17
+ */
18
+ export function verifySignature(messageHash, signature, pub) {
19
+ const digest = fromHex(messageHash, PQ.n, 'messageHash');
20
+ const pubSeed = fromHex(pub.pubSeed, PQ.n, 'pubSeed');
21
+ const expectedRoot = fromHex(pub.root, PQ.n, 'root');
22
+ if (!signature || typeof signature !== 'object')
23
+ throw new HashbornError('signature must be an object');
24
+ const leaf = signature.leaf;
25
+ if (!Number.isInteger(leaf) || leaf < 0 || leaf >= PQ.leaves) {
26
+ throw new HashbornError(`leaf index must be an integer in 0..${PQ.leaves - 1}`);
27
+ }
28
+ const sigma = decodeSigma(fromHex(signature.wots, PQ.len * PQ.n, 'signature.wots'));
29
+ if (!Array.isArray(signature.auth) || signature.auth.length !== PQ.height) {
30
+ throw new HashbornError(`signature.auth must have ${PQ.height} nodes`);
31
+ }
32
+ const auth = signature.auth.map((h, i) => fromHex(h, PQ.n, `signature.auth[${i}]`));
33
+ const { pks, hashes } = wotsVerifyPk(pubSeed, leaf, digest, sigma);
34
+ const root = climb(pubSeed, leaf, leafHash(pubSeed, leaf, pks), auth, PQ.height);
35
+ return { valid: equalBytes(root, expectedRoot), computedRoot: toHex(root), hashCalls: hashes };
36
+ }
37
+ function readable(value) {
38
+ const v = (typeof value === 'object' && value !== null ? value : {});
39
+ const sig = (typeof v.signature === 'object' && v.signature !== null ? v.signature : {});
40
+ return {
41
+ domain: typeof v.domain === 'string' ? v.domain : null,
42
+ leaf: typeof sig.leaf === 'number' && Number.isInteger(sig.leaf) ? sig.leaf : null,
43
+ messageHash: typeof v.messageHash === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v.messageHash) ? v.messageHash : null,
44
+ root: typeof v.root === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v.root) ? v.root : null,
45
+ };
46
+ }
47
+ /**
48
+ * Verifies an attestation with SHA-256 alone: its shape and sizes, protocol v1 fields (version 1,
49
+ * scheme wots-merkle, height 8, a v1 domain), the WOTS signature and Merkle path over
50
+ * `messageHash` against `root` and `pubSeed`, and that `pqAddress` derives from them.
51
+ * Never throws: a bad input is `{valid: false, code, reason}`.
52
+ *
53
+ * It does not check that the attestation was anchored on chain, nor what `messageHash` stands
54
+ * for: use `verifyRecord` with the record's fields for that.
55
+ */
56
+ export function verifyAttestation(attestation) {
57
+ const base = { ...readable(attestation), valid: false, pqAddress: null, computedRoot: null, hashCalls: 0 };
58
+ let att;
59
+ try {
60
+ att = checkAttestation(attestation, false);
61
+ }
62
+ catch (err) {
63
+ const code = err instanceof HashbornError && err.code === 'unsupported' ? 'unsupported' : 'malformed';
64
+ return { ...base, code, reason: err instanceof Error ? err.message : String(err) };
65
+ }
66
+ if (att.scheme !== 'wots-merkle') {
67
+ return { ...base, code: 'unsupported', reason: `scheme ${att.scheme} is not signed in protocol v1; only wots-merkle is` };
68
+ }
69
+ const derived = derivePqAddress(att);
70
+ const sig = verifySignature(att.messageHash, att.signature, att);
71
+ const result = { ...base, pqAddress: derived, computedRoot: sig.computedRoot, hashCalls: sig.hashCalls };
72
+ if (!sig.valid) {
73
+ return { ...result, code: 'signature', reason: 'The signature does not climb to the root over messageHash.' };
74
+ }
75
+ if (derived !== att.pqAddress) {
76
+ return { ...result, code: 'pq-address', reason: `pqAddress does not derive from root and pubSeed (expected ${derived}).` };
77
+ }
78
+ return { ...result, valid: true };
79
+ }
80
+ /**
81
+ * `verifyAttestation` plus the record: recomputes the digest from `record` under the
82
+ * attestation's domain and requires it to equal `messageHash`. `record` is either the fields
83
+ * object (hashed as canonical JSON, keys sorted) or the record text exactly as published on chain.
84
+ */
85
+ export function verifyRecord(attestation, record) {
86
+ const result = verifyAttestation(attestation);
87
+ let recomputed;
88
+ try {
89
+ const domain = attestation?.domain;
90
+ if (typeof domain !== 'string')
91
+ throw new HashbornError('domain: string expected');
92
+ recomputed = recordDigest(domain, typeof record === 'string' ? record : canonicalJson(record));
93
+ }
94
+ catch (err) {
95
+ const reason = `record: ${err instanceof Error ? err.message : String(err)}`;
96
+ return result.valid ? { ...result, valid: false, code: 'malformed', reason, recordDigest: null } : { ...result, recordDigest: null };
97
+ }
98
+ if (!result.valid)
99
+ return { ...result, recordDigest: recomputed };
100
+ if (recomputed !== result.messageHash?.toLowerCase()) {
101
+ return { ...result, valid: false, code: 'digest', reason: 'The record does not hash to the signed messageHash.', recordDigest: recomputed };
102
+ }
103
+ return { ...result, recordDigest: recomputed };
104
+ }
package/dist/wots.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The verifier's half of WOTS, protocol v1 parameters: w = 16, n = 32.
3
+ *
4
+ * A 32-byte digest becomes 64 base-16 digits d_0..d_63. The checksum C = sum(15 - d_i) (at most
5
+ * 960) is written as three base-16 digits, most significant first, giving 67 digits. The
6
+ * signature value of chain i is F^{d_i}(sk_i); the verifier finishes the chain,
7
+ * F^{15 - d_i}(sigma_i), and gets the chain's public end back. 45 to 990 SHA-256 calls.
8
+ */
9
+ export declare const WOTS_SIGMA_BYTES: number;
10
+ /** The 67 digits of a digest: 64 message digits then 3 checksum digits. */
11
+ export declare function digits(digest: Uint8Array): number[];
12
+ /** Walks chain `i` of `leaf` from step `start` for `steps` steps, starting at value `x`. */
13
+ export declare function chain(pubSeed: Uint8Array, leaf: number, i: number, start: number, steps: number, x: Uint8Array): Uint8Array;
14
+ /** Finishes every chain from the signature values, returning the implied chain ends. */
15
+ export declare function wotsVerifyPk(pubSeed: Uint8Array, leaf: number, digest: Uint8Array, sigma: Uint8Array[]): {
16
+ pks: Uint8Array[];
17
+ hashes: number;
18
+ };
19
+ export declare function decodeSigma(bytes: Uint8Array): Uint8Array[];
package/dist/wots.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The verifier's half of WOTS, protocol v1 parameters: w = 16, n = 32.
3
+ *
4
+ * A 32-byte digest becomes 64 base-16 digits d_0..d_63. The checksum C = sum(15 - d_i) (at most
5
+ * 960) is written as three base-16 digits, most significant first, giving 67 digits. The
6
+ * signature value of chain i is F^{d_i}(sk_i); the verifier finishes the chain,
7
+ * F^{15 - d_i}(sigma_i), and gets the chain's public end back. 45 to 990 SHA-256 calls.
8
+ */
9
+ import { PQ } from './constants.js';
10
+ import { HashbornError } from './errors.js';
11
+ import { ADRS, F, adrs } from './hash.js';
12
+ export const WOTS_SIGMA_BYTES = PQ.len * PQ.n;
13
+ /** The 67 digits of a digest: 64 message digits then 3 checksum digits. */
14
+ export function digits(digest) {
15
+ if (digest.length !== PQ.n)
16
+ throw new HashbornError(`digest must be ${PQ.n} bytes`);
17
+ const out = new Array(PQ.len);
18
+ let csum = 0;
19
+ for (let i = 0; i < PQ.n; i++) {
20
+ const hi = digest[i] >> 4;
21
+ const lo = digest[i] & 15;
22
+ out[2 * i] = hi;
23
+ out[2 * i + 1] = lo;
24
+ csum += PQ.chainSteps - hi + (PQ.chainSteps - lo);
25
+ }
26
+ out[PQ.len1] = (csum >> 8) & 15;
27
+ out[PQ.len1 + 1] = (csum >> 4) & 15;
28
+ out[PQ.len1 + 2] = csum & 15;
29
+ return out;
30
+ }
31
+ /** Walks chain `i` of `leaf` from step `start` for `steps` steps, starting at value `x`. */
32
+ export function chain(pubSeed, leaf, i, start, steps, x) {
33
+ let v = x;
34
+ for (let s = 0; s < steps; s++)
35
+ v = F(pubSeed, adrs(ADRS.chain, leaf, i, start + s), v);
36
+ return v;
37
+ }
38
+ /** Finishes every chain from the signature values, returning the implied chain ends. */
39
+ export function wotsVerifyPk(pubSeed, leaf, digest, sigma) {
40
+ if (sigma.length !== PQ.len)
41
+ throw new HashbornError(`signature must have ${PQ.len} chain values`);
42
+ const d = digits(digest);
43
+ const pks = new Array(PQ.len);
44
+ let hashes = 0;
45
+ for (let i = 0; i < PQ.len; i++) {
46
+ const steps = PQ.chainSteps - d[i];
47
+ pks[i] = chain(pubSeed, leaf, i, d[i], steps, sigma[i]);
48
+ hashes += steps;
49
+ }
50
+ return { pks, hashes };
51
+ }
52
+ export function decodeSigma(bytes) {
53
+ if (bytes.length !== WOTS_SIGMA_BYTES)
54
+ throw new HashbornError(`signature.wots must be ${WOTS_SIGMA_BYTES} bytes`);
55
+ const out = new Array(PQ.len);
56
+ for (let i = 0; i < PQ.len; i++)
57
+ out[i] = bytes.subarray(i * PQ.n, (i + 1) * PQ.n);
58
+ return out;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@hashborn/verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.2",
4
+ "description": "Verify Hashborn (protocol pqc-rh) post-quantum records offline with SHA-256 alone: WOTS + Merkle signatures, record digests, verify links and RWA manifest roots. Supersedes @hashborne/verify.",
5
+ "keywords": [
6
+ "hashborn",
7
+ "hashborne",
8
+ "pqc-rh",
9
+ "post-quantum",
10
+ "wots",
11
+ "merkle",
12
+ "hash-based-signature",
13
+ "sha256",
14
+ "verify",
15
+ "robinhood-chain"
16
+ ],
17
+ "homepage": "https://hashborn.com/docs",
18
+ "license": "MIT",
19
+ "author": "Hashborn",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "bin": {
33
+ "hashborn-verify": "dist/cli.js",
34
+ "hashborne-verify": "dist/cli.js"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20.19"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@noble/hashes": "^2.4.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.20.1",
53
+ "tsx": "^4.21.0",
54
+ "typescript": "~5.9.3"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -p tsconfig.build.json",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "node --test --import tsx \"test/**/*.test.ts\"",
60
+ "vectors": "node --import tsx test/gen-vectors.ts"
61
+ }
6
62
  }