xoonya 3.1.12 → 3.1.14

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/RELEASE_NOTES.md CHANGED
@@ -1 +1 @@
1
- `xoonya` 3.1.12 is a synchronized-line release with no change to the proof engine, verification behavior, vectors, routes, or package contents. It keeps the naxytra release line aligned across xytara, xoonya, and the naxytra bootstrap package.
1
+ `xoonya` 3.1.14 is a synchronized-line release with no change to the proof engine, verification behavior, vectors, routes, or package contents. It is published through npm trusted publishing (GitHub OIDC), with no npm token involved.
@@ -1,189 +1,196 @@
1
- "use strict";
2
-
3
- // Cryptographic verification of IMPORTED external proofs.
4
- //
5
- // xoonya binds external proof material into its own bundles. This module lets it
6
- // additionally VERIFY the external material's own signature — reporting the
7
- // cryptographic FACTS it can establish, without over-claiming trust.
8
- //
9
- // Honesty model (important): from a single self-contained request, xoonya can
10
- // only establish that "a signature is valid under key K" (signature_valid) and
11
- // report K's fingerprint (signing_key_ref). It CANNOT establish that K is the
12
- // legitimate issuer/attester, nor revocation or DID-resolution status. Trust in K
13
- // is the consumer's decision. The only way a caller gets a trust-anchored result
14
- // (expected_key_match=true) is by DECLARING, out of band, the key fingerprint they
15
- // already trust (expected_key_ref) and having the signer match it. A key supplied
16
- // in the same request — or a self-asserted key embedded in the proof — is NOT, by
17
- // itself, treated as trusted.
18
- //
19
- // Dependency-free (node crypto). Supports compact JWS (EdDSA, ES256/384/512,
20
- // ES256K, RS256/384/512, PS256/384/512) and generic detached signatures. Private
21
- // key material (e.g. a JWK with a `d` component) is refused.
22
-
23
- const crypto = require("crypto");
24
-
25
- function b64urlToBuffer(s) {
26
- const str = String(s || "").replace(/-/g, "+").replace(/_/g, "/");
27
- const pad = str.length % 4 === 0 ? "" : "=".repeat(4 - (str.length % 4));
28
- return Buffer.from(str + pad, "base64");
29
- }
30
-
31
- // alg -> { hash, kind, keyType?/curve? } ; curve/keyType bind the declared alg to
32
- // the actual key so the reported alg describes how it was really validated.
33
- const JWS_ALG = {
34
- EdDSA: { hash: null, kind: "eddsa", keyType: "ed25519" },
35
- ES256: { hash: "sha256", kind: "ecdsa", curve: "prime256v1" },
36
- ES384: { hash: "sha384", kind: "ecdsa", curve: "secp384r1" },
37
- ES512: { hash: "sha512", kind: "ecdsa", curve: "secp521r1" },
38
- ES256K: { hash: "sha256", kind: "ecdsa", curve: "secp256k1" },
39
- RS256: { hash: "sha256", kind: "rsa" },
40
- RS384: { hash: "sha384", kind: "rsa" },
41
- RS512: { hash: "sha512", kind: "rsa" },
42
- PS256: { hash: "sha256", kind: "rsapss" },
43
- PS384: { hash: "sha384", kind: "rsapss" },
44
- PS512: { hash: "sha512", kind: "rsapss" }
45
- };
46
-
47
- function isPrivateJwk(obj) {
48
- return Boolean(obj && typeof obj === "object" && !Buffer.isBuffer(obj) && typeof obj.d === "string" && obj.d.length > 0);
49
- }
50
-
51
- // Build a PUBLIC KeyObject from PEM / base64 DER SPKI / public JWK. Refuses
52
- // private key material. Returns null on any problem.
53
- function toPublicKeyObject(input) {
54
- if (!input) return null;
55
- try {
56
- if (typeof input === "object" && !Buffer.isBuffer(input)) {
57
- if (isPrivateJwk(input)) return null; // never accept private material
58
- return crypto.createPublicKey({ key: input, format: "jwk" });
59
- }
60
- const s = String(input).trim();
61
- if (/BEGIN (EC |RSA |DSA |OPENSSH |PGP )?PRIVATE KEY/.test(s)) return null;
62
- if (s.includes("BEGIN ")) return crypto.createPublicKey({ key: s, format: "pem" });
63
- return crypto.createPublicKey({ key: Buffer.from(s, "base64"), format: "der", type: "spki" });
64
- } catch (_) {
65
- return null;
66
- }
67
- }
68
-
69
- function keyRef(keyObject) {
70
- try {
71
- const spki = keyObject.export({ type: "spki", format: "der" });
72
- const algo = keyObject.asymmetricKeyType || "key";
73
- return `${algo}-spki-sha256:` + crypto.createHash("sha256").update(spki).digest("hex");
74
- } catch (_) {
75
- return null;
76
- }
77
- }
78
-
79
- // The declared alg must be consistent with the actual key type/curve.
80
- function keyMatchesAlg(keyObject, alg) {
81
- const spec = JWS_ALG[alg];
82
- if (!spec || !keyObject) return false;
83
- const t = keyObject.asymmetricKeyType;
84
- if (spec.kind === "eddsa") return t === "ed25519";
85
- if (spec.kind === "ecdsa") {
86
- if (t !== "ec") return false;
87
- const details = keyObject.asymmetricKeyDetails || {};
88
- return details.namedCurve === spec.curve;
89
- }
90
- if (spec.kind === "rsa" || spec.kind === "rsapss") return t === "rsa" || t === "rsa-pss";
91
- return false;
92
- }
93
-
94
- function verifyWithAlg(alg, signingInput, signature, keyObject) {
95
- const spec = JWS_ALG[alg];
96
- if (!spec || !keyObject) return false;
97
- if (!keyMatchesAlg(keyObject, alg)) return false; // bind declared alg to the key
98
- const data = Buffer.isBuffer(signingInput) ? signingInput : Buffer.from(String(signingInput), "utf8");
99
- try {
100
- if (spec.kind === "eddsa") return crypto.verify(null, data, keyObject, signature);
101
- if (spec.kind === "ecdsa") return crypto.verify(spec.hash, data, { key: keyObject, dsaEncoding: "ieee-p1363" }, signature);
102
- if (spec.kind === "rsa") return crypto.verify(spec.hash, data, keyObject, signature);
103
- if (spec.kind === "rsapss") {
104
- return crypto.verify(spec.hash, data, { key: keyObject, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, signature);
105
- }
106
- } catch (_) {
107
- return false;
108
- }
109
- return false;
110
- }
111
-
112
- function trustResult(signatureValid, keyObject, keySource, expectedKeyRef, alg, extra) {
113
- const kref = keyRef(keyObject);
114
- const expected = typeof expectedKeyRef === "string" && expectedKeyRef.trim() ? expectedKeyRef.trim() : null;
115
- const expectedMatch = expected ? kref === expected : null;
116
- return {
117
- ok: true,
118
- signature_valid: signatureValid === true,
119
- signing_key_ref: kref,
120
- key_source: keySource,
121
- expected_key_ref: expected,
122
- // true only when the caller declared a trusted key fingerprint AND the signer
123
- // matches it; null when no expectation was declared. Trust in signing_key_ref
124
- // is otherwise the consumer's decision.
125
- expected_key_match: expectedMatch,
126
- trust_anchored: signatureValid === true && expectedMatch === true,
127
- alg,
128
- ...(extra || {})
129
- };
130
- }
131
-
132
- // Verify a compact JWS. opts = { trusted_public_key?, expected_key_ref? }.
133
- function verifyJws(jwsCompact, opts) {
134
- const o = opts && typeof opts === "object" ? opts : {};
135
- const parts = String(jwsCompact || "").split(".");
136
- if (parts.length !== 3) return { ok: false, reason: "jws_malformed" };
137
- const [protectedB64, payloadB64, signatureB64] = parts;
138
- let header;
139
- try {
140
- header = JSON.parse(b64urlToBuffer(protectedB64).toString("utf8"));
141
- } catch (_) {
142
- return { ok: false, reason: "jws_header_invalid" };
143
- }
144
- const alg = header && typeof header.alg === "string" ? header.alg : null;
145
- if (alg === "none") return { ok: false, reason: "jws_alg_none_rejected" };
146
- if (!alg || !JWS_ALG[alg]) return { ok: false, reason: "jws_alg_unsupported", alg: alg || null };
147
- // Refuse unknown critical extensions and non-standard signing-input variants.
148
- if (Array.isArray(header.crit) && header.crit.length > 0) return { ok: false, reason: "jws_crit_unsupported" };
149
- if (header.b64 === false) return { ok: false, reason: "jws_unencoded_payload_unsupported" };
150
-
151
- const trustedKey = o.trusted_public_key ? toPublicKeyObject(o.trusted_public_key) : null;
152
- const embeddedKey = !trustedKey && header.jwk ? toPublicKeyObject(header.jwk) : null;
153
- const keyObject = trustedKey || embeddedKey;
154
- if (!keyObject) return { ok: false, reason: "no_public_key" };
155
-
156
- const valid = verifyWithAlg(alg, `${protectedB64}.${payloadB64}`, b64urlToBuffer(signatureB64), keyObject);
157
- return trustResult(valid, keyObject, trustedKey ? "caller_supplied" : "embedded_self_asserted", o.expected_key_ref, alg, {
158
- header,
159
- payload_b64url: payloadB64
160
- });
161
- }
162
-
163
- // Verify a detached signature over `material`. opts = { material, signature,
164
- // signature_encoding?, public_key, algorithm, expected_key_ref? }.
165
- function verifyDetachedSignature(opts) {
166
- const o = opts && typeof opts === "object" ? opts : {};
167
- const alg = typeof o.algorithm === "string" ? o.algorithm : null;
168
- if (!alg || !JWS_ALG[alg]) return { ok: false, reason: "algorithm_unsupported", alg: alg || null };
169
- const keyObject = toPublicKeyObject(o.public_key);
170
- if (!keyObject) return { ok: false, reason: "no_public_key" };
171
- if (o.material === undefined || o.material === null) return { ok: false, reason: "material_required" };
172
- const material = Buffer.isBuffer(o.material) ? o.material : Buffer.from(String(o.material), "utf8");
173
- let signature;
174
- try {
175
- const enc = o.signature_encoding === "hex" ? "hex" : o.signature_encoding === "base64url" ? null : "base64";
176
- signature = enc ? Buffer.from(String(o.signature || ""), enc) : b64urlToBuffer(o.signature);
177
- } catch (_) {
178
- return { ok: false, reason: "signature_invalid" };
179
- }
180
- if (signature.length === 0) return { ok: false, reason: "signature_invalid" };
181
- const valid = verifyWithAlg(alg, material, signature, keyObject);
182
- return trustResult(valid, keyObject, "caller_supplied", o.expected_key_ref, alg);
183
- }
184
-
185
- function looksLikeCompactJws(s) {
186
- return typeof s === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(s.trim());
187
- }
188
-
189
- module.exports = { verifyJws, verifyDetachedSignature, toPublicKeyObject, isPrivateJwk, looksLikeCompactJws };
1
+ "use strict";
2
+
3
+ // Cryptographic verification of IMPORTED external proofs.
4
+ //
5
+ // xoonya binds external proof material into its own bundles. This module lets it
6
+ // additionally VERIFY the external material's own signature — reporting the
7
+ // cryptographic FACTS it can establish, without over-claiming trust.
8
+ //
9
+ // Honesty model (important): from a single self-contained request, xoonya can
10
+ // only establish that "a signature is valid under key K" (signature_valid) and
11
+ // report K's fingerprint (signing_key_ref). It CANNOT establish that K is the
12
+ // legitimate issuer/attester, nor revocation or DID-resolution status. Trust in K
13
+ // is the consumer's decision. The only way a caller gets a trust-anchored result
14
+ // (expected_key_match=true) is by DECLARING, out of band, the key fingerprint they
15
+ // already trust (expected_key_ref) and having the signer match it. A key supplied
16
+ // in the same request — or a self-asserted key embedded in the proof — is NOT, by
17
+ // itself, treated as trusted.
18
+ //
19
+ // Dependency-free (node crypto). Supports compact JWS (EdDSA, ES256/384/512,
20
+ // ES256K, RS256/384/512, PS256/384/512) and generic detached signatures, which
21
+ // additionally accept ML-DSA-44/65/87 (FIPS-204, SPKI keys). Private key material
22
+ // (e.g. a JWK with a `d` component) is refused.
23
+
24
+ const crypto = require("crypto");
25
+
26
+ function b64urlToBuffer(s) {
27
+ const str = String(s || "").replace(/-/g, "+").replace(/_/g, "/");
28
+ const pad = str.length % 4 === 0 ? "" : "=".repeat(4 - (str.length % 4));
29
+ return Buffer.from(str + pad, "base64");
30
+ }
31
+
32
+ // alg -> { hash, kind, keyType?/curve? } ; curve/keyType bind the declared alg to
33
+ // the actual key so the reported alg describes how it was really validated.
34
+ const JWS_ALG = {
35
+ EdDSA: { hash: null, kind: "eddsa", keyType: "ed25519" },
36
+ ES256: { hash: "sha256", kind: "ecdsa", curve: "prime256v1" },
37
+ ES384: { hash: "sha384", kind: "ecdsa", curve: "secp384r1" },
38
+ ES512: { hash: "sha512", kind: "ecdsa", curve: "secp521r1" },
39
+ ES256K: { hash: "sha256", kind: "ecdsa", curve: "secp256k1" },
40
+ RS256: { hash: "sha256", kind: "rsa" },
41
+ RS384: { hash: "sha384", kind: "rsa" },
42
+ RS512: { hash: "sha512", kind: "rsa" },
43
+ PS256: { hash: "sha256", kind: "rsapss" },
44
+ PS384: { hash: "sha384", kind: "rsapss" },
45
+ PS512: { hash: "sha512", kind: "rsapss" },
46
+ // FIPS-204 ML-DSA (pure mode, as named by the JOSE ML-DSA draft). Needs Node >= 24
47
+ // (node:crypto ML-DSA); on an older runtime the key cannot be parsed and verify fails closed.
48
+ "ML-DSA-44": { hash: null, kind: "mldsa", keyType: "ml-dsa-44" },
49
+ "ML-DSA-65": { hash: null, kind: "mldsa", keyType: "ml-dsa-65" },
50
+ "ML-DSA-87": { hash: null, kind: "mldsa", keyType: "ml-dsa-87" }
51
+ };
52
+
53
+ function isPrivateJwk(obj) {
54
+ return Boolean(obj && typeof obj === "object" && !Buffer.isBuffer(obj) && typeof obj.d === "string" && obj.d.length > 0);
55
+ }
56
+
57
+ // Build a PUBLIC KeyObject from PEM / base64 DER SPKI / public JWK. Refuses
58
+ // private key material. Returns null on any problem.
59
+ function toPublicKeyObject(input) {
60
+ if (!input) return null;
61
+ try {
62
+ if (typeof input === "object" && !Buffer.isBuffer(input)) {
63
+ if (isPrivateJwk(input)) return null; // never accept private material
64
+ return crypto.createPublicKey({ key: input, format: "jwk" });
65
+ }
66
+ const s = String(input).trim();
67
+ if (/BEGIN (EC |RSA |DSA |OPENSSH |PGP )?PRIVATE KEY/.test(s)) return null;
68
+ if (s.includes("BEGIN ")) return crypto.createPublicKey({ key: s, format: "pem" });
69
+ return crypto.createPublicKey({ key: Buffer.from(s, "base64"), format: "der", type: "spki" });
70
+ } catch (_) {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function keyRef(keyObject) {
76
+ try {
77
+ const spki = keyObject.export({ type: "spki", format: "der" });
78
+ const algo = keyObject.asymmetricKeyType || "key";
79
+ return `${algo}-spki-sha256:` + crypto.createHash("sha256").update(spki).digest("hex");
80
+ } catch (_) {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ // The declared alg must be consistent with the actual key type/curve.
86
+ function keyMatchesAlg(keyObject, alg) {
87
+ const spec = JWS_ALG[alg];
88
+ if (!spec || !keyObject) return false;
89
+ const t = keyObject.asymmetricKeyType;
90
+ if (spec.kind === "eddsa") return t === "ed25519";
91
+ if (spec.kind === "mldsa") return t === spec.keyType;
92
+ if (spec.kind === "ecdsa") {
93
+ if (t !== "ec") return false;
94
+ const details = keyObject.asymmetricKeyDetails || {};
95
+ return details.namedCurve === spec.curve;
96
+ }
97
+ if (spec.kind === "rsa" || spec.kind === "rsapss") return t === "rsa" || t === "rsa-pss";
98
+ return false;
99
+ }
100
+
101
+ function verifyWithAlg(alg, signingInput, signature, keyObject) {
102
+ const spec = JWS_ALG[alg];
103
+ if (!spec || !keyObject) return false;
104
+ if (!keyMatchesAlg(keyObject, alg)) return false; // bind declared alg to the key
105
+ const data = Buffer.isBuffer(signingInput) ? signingInput : Buffer.from(String(signingInput), "utf8");
106
+ try {
107
+ if (spec.kind === "eddsa" || spec.kind === "mldsa") return crypto.verify(null, data, keyObject, signature);
108
+ if (spec.kind === "ecdsa") return crypto.verify(spec.hash, data, { key: keyObject, dsaEncoding: "ieee-p1363" }, signature);
109
+ if (spec.kind === "rsa") return crypto.verify(spec.hash, data, keyObject, signature);
110
+ if (spec.kind === "rsapss") {
111
+ return crypto.verify(spec.hash, data, { key: keyObject, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, signature);
112
+ }
113
+ } catch (_) {
114
+ return false;
115
+ }
116
+ return false;
117
+ }
118
+
119
+ function trustResult(signatureValid, keyObject, keySource, expectedKeyRef, alg, extra) {
120
+ const kref = keyRef(keyObject);
121
+ const expected = typeof expectedKeyRef === "string" && expectedKeyRef.trim() ? expectedKeyRef.trim() : null;
122
+ const expectedMatch = expected ? kref === expected : null;
123
+ return {
124
+ ok: true,
125
+ signature_valid: signatureValid === true,
126
+ signing_key_ref: kref,
127
+ key_source: keySource,
128
+ expected_key_ref: expected,
129
+ // true only when the caller declared a trusted key fingerprint AND the signer
130
+ // matches it; null when no expectation was declared. Trust in signing_key_ref
131
+ // is otherwise the consumer's decision.
132
+ expected_key_match: expectedMatch,
133
+ trust_anchored: signatureValid === true && expectedMatch === true,
134
+ alg,
135
+ ...(extra || {})
136
+ };
137
+ }
138
+
139
+ // Verify a compact JWS. opts = { trusted_public_key?, expected_key_ref? }.
140
+ function verifyJws(jwsCompact, opts) {
141
+ const o = opts && typeof opts === "object" ? opts : {};
142
+ const parts = String(jwsCompact || "").split(".");
143
+ if (parts.length !== 3) return { ok: false, reason: "jws_malformed" };
144
+ const [protectedB64, payloadB64, signatureB64] = parts;
145
+ let header;
146
+ try {
147
+ header = JSON.parse(b64urlToBuffer(protectedB64).toString("utf8"));
148
+ } catch (_) {
149
+ return { ok: false, reason: "jws_header_invalid" };
150
+ }
151
+ const alg = header && typeof header.alg === "string" ? header.alg : null;
152
+ if (alg === "none") return { ok: false, reason: "jws_alg_none_rejected" };
153
+ if (!alg || !JWS_ALG[alg]) return { ok: false, reason: "jws_alg_unsupported", alg: alg || null };
154
+ // Refuse unknown critical extensions and non-standard signing-input variants.
155
+ if (Array.isArray(header.crit) && header.crit.length > 0) return { ok: false, reason: "jws_crit_unsupported" };
156
+ if (header.b64 === false) return { ok: false, reason: "jws_unencoded_payload_unsupported" };
157
+
158
+ const trustedKey = o.trusted_public_key ? toPublicKeyObject(o.trusted_public_key) : null;
159
+ const embeddedKey = !trustedKey && header.jwk ? toPublicKeyObject(header.jwk) : null;
160
+ const keyObject = trustedKey || embeddedKey;
161
+ if (!keyObject) return { ok: false, reason: "no_public_key" };
162
+
163
+ const valid = verifyWithAlg(alg, `${protectedB64}.${payloadB64}`, b64urlToBuffer(signatureB64), keyObject);
164
+ return trustResult(valid, keyObject, trustedKey ? "caller_supplied" : "embedded_self_asserted", o.expected_key_ref, alg, {
165
+ header,
166
+ payload_b64url: payloadB64
167
+ });
168
+ }
169
+
170
+ // Verify a detached signature over `material`. opts = { material, signature,
171
+ // signature_encoding?, public_key, algorithm, expected_key_ref? }.
172
+ function verifyDetachedSignature(opts) {
173
+ const o = opts && typeof opts === "object" ? opts : {};
174
+ const alg = typeof o.algorithm === "string" ? o.algorithm : null;
175
+ if (!alg || !JWS_ALG[alg]) return { ok: false, reason: "algorithm_unsupported", alg: alg || null };
176
+ const keyObject = toPublicKeyObject(o.public_key);
177
+ if (!keyObject) return { ok: false, reason: "no_public_key" };
178
+ if (o.material === undefined || o.material === null) return { ok: false, reason: "material_required" };
179
+ const material = Buffer.isBuffer(o.material) ? o.material : Buffer.from(String(o.material), "utf8");
180
+ let signature;
181
+ try {
182
+ const enc = o.signature_encoding === "hex" ? "hex" : o.signature_encoding === "base64url" ? null : "base64";
183
+ signature = enc ? Buffer.from(String(o.signature || ""), enc) : b64urlToBuffer(o.signature);
184
+ } catch (_) {
185
+ return { ok: false, reason: "signature_invalid" };
186
+ }
187
+ if (signature.length === 0) return { ok: false, reason: "signature_invalid" };
188
+ const valid = verifyWithAlg(alg, material, signature, keyObject);
189
+ return trustResult(valid, keyObject, "caller_supplied", o.expected_key_ref, alg);
190
+ }
191
+
192
+ function looksLikeCompactJws(s) {
193
+ return typeof s === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(s.trim());
194
+ }
195
+
196
+ module.exports = { verifyJws, verifyDetachedSignature, toPublicKeyObject, isPrivateJwk, looksLikeCompactJws };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xoonya",
3
- "version": "3.1.12",
3
+ "version": "3.1.14",
4
4
  "description": "Deterministic proof and review engine with post-quantum verification (Ed25519 + ML-DSA-65 hybrid) for receipts, handoffs, governance, and the native XNYA machine-evidence format. Verified live via the paid x402 lane.",
5
5
  "main": "index.js",
6
6
  "bin": {