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 +1 -1
- package/lib/external_proof_verify.js +196 -189
- package/package.json +1 -1
- package/proof_sdk.js +6692 -6692
- package/scripts/verify_external_proof_signature.js +142 -128
- package/scripts/verify_paid_execute.js +133 -133
- package/scripts/verify_paywall_flag.js +86 -86
- package/server.js +4430 -4430
package/RELEASE_NOTES.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
`xoonya` 3.1.
|
|
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
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (spec.kind === "
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
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": {
|