@synoi/sraid 0.2.0 → 0.3.0
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/README.md +51 -6
- package/dist/attestation.d.ts +8 -49
- package/dist/attestation.d.ts.map +1 -1
- package/dist/attestation.js +15 -46
- package/dist/attestation.js.map +1 -1
- package/dist/internal/attestation-core.d.ts +85 -0
- package/dist/internal/attestation-core.d.ts.map +1 -0
- package/dist/internal/attestation-core.js +78 -0
- package/dist/internal/attestation-core.js.map +1 -0
- package/dist/internal/base64-browser.d.ts +31 -0
- package/dist/internal/base64-browser.d.ts.map +1 -0
- package/dist/internal/base64-browser.js +39 -0
- package/dist/internal/base64-browser.js.map +1 -0
- package/dist/internal/base64-validate.d.ts +19 -0
- package/dist/internal/base64-validate.d.ts.map +1 -0
- package/dist/internal/base64-validate.js +33 -0
- package/dist/internal/base64-validate.js.map +1 -0
- package/dist/internal/base64.d.ts +7 -5
- package/dist/internal/base64.d.ts.map +1 -1
- package/dist/internal/base64.js +7 -19
- package/dist/internal/base64.js.map +1 -1
- package/dist/internal/content-core.d.ts +68 -0
- package/dist/internal/content-core.d.ts.map +1 -0
- package/dist/internal/content-core.js +87 -0
- package/dist/internal/content-core.js.map +1 -0
- package/dist/internal/ed25519-browser.d.ts +33 -0
- package/dist/internal/ed25519-browser.d.ts.map +1 -0
- package/dist/internal/ed25519-browser.js +79 -0
- package/dist/internal/ed25519-browser.js.map +1 -0
- package/dist/internal/mldsa-browser.d.ts +27 -0
- package/dist/internal/mldsa-browser.d.ts.map +1 -0
- package/dist/internal/mldsa-browser.js +35 -0
- package/dist/internal/mldsa-browser.js.map +1 -0
- package/dist/internal/sha256-browser.d.ts +17 -0
- package/dist/internal/sha256-browser.d.ts.map +1 -0
- package/dist/internal/sha256-browser.js +27 -0
- package/dist/internal/sha256-browser.js.map +1 -0
- package/dist/oid.d.ts +2 -52
- package/dist/oid.d.ts.map +1 -1
- package/dist/oid.js +9 -71
- package/dist/oid.js.map +1 -1
- package/dist/verify-browser.d.ts +80 -0
- package/dist/verify-browser.d.ts.map +1 -0
- package/dist/verify-browser.js +157 -0
- package/dist/verify-browser.js.map +1 -0
- package/package.json +21 -2
- package/src/attestation.ts +29 -88
- package/src/internal/attestation-core.ts +122 -0
- package/src/internal/base64-browser.ts +39 -0
- package/src/internal/base64-validate.ts +31 -0
- package/src/internal/base64.ts +7 -16
- package/src/internal/content-core.ts +88 -0
- package/src/internal/ed25519-browser.ts +85 -0
- package/src/internal/mldsa-browser.ts +39 -0
- package/src/internal/sha256-browser.ts +28 -0
- package/src/oid.ts +10 -73
- package/src/verify-browser.ts +185 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synoi/sraid/verify-browser — browser / Chrome-extension / service-worker
|
|
3
|
+
* verify surface.
|
|
4
|
+
*
|
|
5
|
+
* The default (`.`) entry statically imports `node:crypto` in three places:
|
|
6
|
+
* ed25519.ts (node-only Ed25519 verify), mldsa.ts (native ML-DSA + polyfill),
|
|
7
|
+
* and oid.ts (createHash). `node:crypto` does not exist in a browser or
|
|
8
|
+
* service-worker context, so `import '@synoi/sraid'` breaks any browser bundle.
|
|
9
|
+
* This entry is the browser-safe alternative: it carries exactly what a v2
|
|
10
|
+
* hybrid DSSE receipt verifier needs, implemented WITHOUT any static
|
|
11
|
+
* node:crypto import.
|
|
12
|
+
*
|
|
13
|
+
* WHAT THIS EXPOSES (all browser-safe):
|
|
14
|
+
* - canonicalize — pure RFC 8785 JCS serializer (shared, unchanged).
|
|
15
|
+
* - cdroContentCore, — the CDRO OID content-core projection and its
|
|
16
|
+
* CDRO_ENVELOPE_FIELDS normative strip-set (pure; shared byte-for-byte
|
|
17
|
+
* with the node entry via internal/content-core).
|
|
18
|
+
* - verifyAttestation — hybrid DSSE verify (Ed25519 AND ML-DSA-65, both
|
|
19
|
+
* required over the PAE). ASYNC here: WebCrypto
|
|
20
|
+
* Ed25519 verify is Promise-based.
|
|
21
|
+
* - pae, ALG_ED25519, — the DSSE PAE encoder and algorithm ids (pure,
|
|
22
|
+
* ALG_ML_DSA_65 shared with the node entry).
|
|
23
|
+
* - cdroOid, oidOf, — OID helpers over WebCrypto SHA-256. ASYNC here
|
|
24
|
+
* oidOfCanonical (subtle.digest is Promise-based).
|
|
25
|
+
*
|
|
26
|
+
* CRYPTO BACKENDS (browsers have no native ML-DSA and no node:crypto):
|
|
27
|
+
* - Ed25519 verify → WebCrypto (RFC 8032 cofactored, matching the node path),
|
|
28
|
+
* with a @noble/curves fallback below the WebCrypto-Ed25519
|
|
29
|
+
* support floor. See internal/ed25519-browser.ts.
|
|
30
|
+
* - ML-DSA-65 verify → @noble/post-quantum (pure JS). See internal/mldsa-browser.ts.
|
|
31
|
+
* - SHA-256 → WebCrypto subtle.digest. See internal/sha256-browser.ts.
|
|
32
|
+
*
|
|
33
|
+
* NODE PARITY: the node default entry (`@synoi/sraid`) is unchanged and stays
|
|
34
|
+
* synchronous with its node:crypto fast paths. This entry is purely additive.
|
|
35
|
+
* The only surface difference is async: verifyAttestation, cdroOid, oidOf, and
|
|
36
|
+
* oidOfCanonical return Promises here because WebCrypto is Promise-based.
|
|
37
|
+
*
|
|
38
|
+
* Downstream: @synoi/verify's browser build imports this subpath to bring v2
|
|
39
|
+
* hybrid DSSE verification (verifyReceiptV2) to the browser, replacing the
|
|
40
|
+
* fail-closed `v2-not-supported-in-browser-build` stub.
|
|
41
|
+
*/
|
|
42
|
+
import { ALG_ED25519, ALG_ML_DSA_65, pae, type VerifyAttestationInput, type VerifyAttestationResult } from './internal/attestation-core.js';
|
|
43
|
+
export { canonicalize } from './canonicalize.js';
|
|
44
|
+
export { CDRO_ENVELOPE_FIELDS, cdroContentCore } from './internal/content-core.js';
|
|
45
|
+
export { pae, ALG_ED25519, ALG_ML_DSA_65, type VerifyAttestationInput, type VerifyAttestationResult, };
|
|
46
|
+
/**
|
|
47
|
+
* Verify a hybrid DSSE attestation envelope in a browser context. Returns
|
|
48
|
+
* `valid: true` only when the envelope carries BOTH an `ed25519` and an
|
|
49
|
+
* `ml-dsa-65` signature and BOTH verify against the supplied public keys over
|
|
50
|
+
* `PAE(payloadType, payload)`. The payloadType is bound into the signed bytes,
|
|
51
|
+
* so a signature minted for a different payloadType will not verify.
|
|
52
|
+
*
|
|
53
|
+
* ASYNC counterpart of the node `verifyAttestation`: identical envelope shape,
|
|
54
|
+
* AND policy, PAE bytes, and reason strings — only the return is a Promise,
|
|
55
|
+
* because Ed25519 verify runs on WebCrypto. Never rejects: any malformed input
|
|
56
|
+
* or verification failure resolves to `{ valid: false, reasons: [...] }`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyAttestation(input: VerifyAttestationInput): Promise<VerifyAttestationResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Compute an OID over an arbitrary canonical-compatible value using WebCrypto
|
|
61
|
+
* SHA-256. Returns `sha256:` followed by 64 lowercase hex characters.
|
|
62
|
+
*
|
|
63
|
+
* ASYNC counterpart of the node `oidOf`: same canonical bytes and same
|
|
64
|
+
* `sha256:`-prefixed output, but returns a Promise because subtle.digest is
|
|
65
|
+
* Promise-based. Byte-identical result to the node entry for the same input.
|
|
66
|
+
*/
|
|
67
|
+
export declare function oidOf(canonical: unknown): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Compute an OID directly from already-canonicalized bytes using WebCrypto
|
|
70
|
+
* SHA-256. ASYNC counterpart of the node `oidOfCanonical`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function oidOfCanonical(canonical: string | Uint8Array): Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Compute the OID of a full CDRO over its content core (see `cdroContentCore`),
|
|
75
|
+
* using WebCrypto SHA-256. ASYNC counterpart of the node `cdroOid`; yields the
|
|
76
|
+
* SAME OID whether the object is pre- or post-attestation, and the SAME value
|
|
77
|
+
* the node entry produces for the same object.
|
|
78
|
+
*/
|
|
79
|
+
export declare function cdroOid(cdro: unknown): Promise<string>;
|
|
80
|
+
//# sourceMappingURL=verify-browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-browser.d.ts","sourceRoot":"","sources":["../src/verify-browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAQH,OAAO,EACL,WAAW,EACX,aAAa,EAGb,GAAG,EACH,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC7B,MAAM,gCAAgC,CAAA;AAIvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,EACL,GAAG,EACH,WAAW,EACX,aAAa,EACb,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,CAAA;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,uBAAuB,CAAC,CA+DlC;AAID;;;;;;;GAOG;AACH,wBAAsB,KAAK,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAG/D;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpF;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5D"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synoi/sraid/verify-browser — browser / Chrome-extension / service-worker
|
|
3
|
+
* verify surface.
|
|
4
|
+
*
|
|
5
|
+
* The default (`.`) entry statically imports `node:crypto` in three places:
|
|
6
|
+
* ed25519.ts (node-only Ed25519 verify), mldsa.ts (native ML-DSA + polyfill),
|
|
7
|
+
* and oid.ts (createHash). `node:crypto` does not exist in a browser or
|
|
8
|
+
* service-worker context, so `import '@synoi/sraid'` breaks any browser bundle.
|
|
9
|
+
* This entry is the browser-safe alternative: it carries exactly what a v2
|
|
10
|
+
* hybrid DSSE receipt verifier needs, implemented WITHOUT any static
|
|
11
|
+
* node:crypto import.
|
|
12
|
+
*
|
|
13
|
+
* WHAT THIS EXPOSES (all browser-safe):
|
|
14
|
+
* - canonicalize — pure RFC 8785 JCS serializer (shared, unchanged).
|
|
15
|
+
* - cdroContentCore, — the CDRO OID content-core projection and its
|
|
16
|
+
* CDRO_ENVELOPE_FIELDS normative strip-set (pure; shared byte-for-byte
|
|
17
|
+
* with the node entry via internal/content-core).
|
|
18
|
+
* - verifyAttestation — hybrid DSSE verify (Ed25519 AND ML-DSA-65, both
|
|
19
|
+
* required over the PAE). ASYNC here: WebCrypto
|
|
20
|
+
* Ed25519 verify is Promise-based.
|
|
21
|
+
* - pae, ALG_ED25519, — the DSSE PAE encoder and algorithm ids (pure,
|
|
22
|
+
* ALG_ML_DSA_65 shared with the node entry).
|
|
23
|
+
* - cdroOid, oidOf, — OID helpers over WebCrypto SHA-256. ASYNC here
|
|
24
|
+
* oidOfCanonical (subtle.digest is Promise-based).
|
|
25
|
+
*
|
|
26
|
+
* CRYPTO BACKENDS (browsers have no native ML-DSA and no node:crypto):
|
|
27
|
+
* - Ed25519 verify → WebCrypto (RFC 8032 cofactored, matching the node path),
|
|
28
|
+
* with a @noble/curves fallback below the WebCrypto-Ed25519
|
|
29
|
+
* support floor. See internal/ed25519-browser.ts.
|
|
30
|
+
* - ML-DSA-65 verify → @noble/post-quantum (pure JS). See internal/mldsa-browser.ts.
|
|
31
|
+
* - SHA-256 → WebCrypto subtle.digest. See internal/sha256-browser.ts.
|
|
32
|
+
*
|
|
33
|
+
* NODE PARITY: the node default entry (`@synoi/sraid`) is unchanged and stays
|
|
34
|
+
* synchronous with its node:crypto fast paths. This entry is purely additive.
|
|
35
|
+
* The only surface difference is async: verifyAttestation, cdroOid, oidOf, and
|
|
36
|
+
* oidOfCanonical return Promises here because WebCrypto is Promise-based.
|
|
37
|
+
*
|
|
38
|
+
* Downstream: @synoi/verify's browser build imports this subpath to bring v2
|
|
39
|
+
* hybrid DSSE verification (verifyReceiptV2) to the browser, replacing the
|
|
40
|
+
* fail-closed `v2-not-supported-in-browser-build` stub.
|
|
41
|
+
*/
|
|
42
|
+
import { canonicalize } from './canonicalize.js';
|
|
43
|
+
import { cdroContentCore } from './internal/content-core.js';
|
|
44
|
+
import { decodeBase64StrictBrowser } from './internal/base64-browser.js';
|
|
45
|
+
import { verifyEd25519Browser } from './internal/ed25519-browser.js';
|
|
46
|
+
import { verifyMlDsa65Browser } from './internal/mldsa-browser.js';
|
|
47
|
+
import { sha256HexPrefixed } from './internal/sha256-browser.js';
|
|
48
|
+
import { ALG_ED25519, ALG_ML_DSA_65, findSig, isWellFormedEnvelope, pae, } from './internal/attestation-core.js';
|
|
49
|
+
// ── Pure / shared re-exports (byte-for-byte identical to the node entry) ──────
|
|
50
|
+
export { canonicalize } from './canonicalize.js';
|
|
51
|
+
export { CDRO_ENVELOPE_FIELDS, cdroContentCore } from './internal/content-core.js';
|
|
52
|
+
export { pae, ALG_ED25519, ALG_ML_DSA_65, };
|
|
53
|
+
// ── Hybrid DSSE attestation verify (browser, ASYNC) ───────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Verify a hybrid DSSE attestation envelope in a browser context. Returns
|
|
56
|
+
* `valid: true` only when the envelope carries BOTH an `ed25519` and an
|
|
57
|
+
* `ml-dsa-65` signature and BOTH verify against the supplied public keys over
|
|
58
|
+
* `PAE(payloadType, payload)`. The payloadType is bound into the signed bytes,
|
|
59
|
+
* so a signature minted for a different payloadType will not verify.
|
|
60
|
+
*
|
|
61
|
+
* ASYNC counterpart of the node `verifyAttestation`: identical envelope shape,
|
|
62
|
+
* AND policy, PAE bytes, and reason strings — only the return is a Promise,
|
|
63
|
+
* because Ed25519 verify runs on WebCrypto. Never rejects: any malformed input
|
|
64
|
+
* or verification failure resolves to `{ valid: false, reasons: [...] }`.
|
|
65
|
+
*/
|
|
66
|
+
export async function verifyAttestation(input) {
|
|
67
|
+
const reasons = [];
|
|
68
|
+
const env = input.envelope;
|
|
69
|
+
if (!isWellFormedEnvelope(env)) {
|
|
70
|
+
return { valid: false, reasons: ['envelope-malformed'] };
|
|
71
|
+
}
|
|
72
|
+
if (input.expectedPayloadType !== undefined &&
|
|
73
|
+
env.payloadType !== input.expectedPayloadType) {
|
|
74
|
+
return { valid: false, reasons: ['payload-type-mismatch'] };
|
|
75
|
+
}
|
|
76
|
+
// The signed bytes: PAE binds payloadType + payload together.
|
|
77
|
+
const message = pae(env.payloadType, env.payload);
|
|
78
|
+
// Find the required hybrid pair. The AND policy: both must be present.
|
|
79
|
+
const edEntry = findSig(env.signatures, ALG_ED25519);
|
|
80
|
+
const mlEntry = findSig(env.signatures, ALG_ML_DSA_65);
|
|
81
|
+
if (!edEntry)
|
|
82
|
+
reasons.push('missing-ed25519');
|
|
83
|
+
if (!mlEntry)
|
|
84
|
+
reasons.push('missing-ml-dsa-65');
|
|
85
|
+
let edOk = false;
|
|
86
|
+
let mlOk = false;
|
|
87
|
+
if (edEntry) {
|
|
88
|
+
let edSig = null;
|
|
89
|
+
try {
|
|
90
|
+
edSig = decodeBase64StrictBrowser(edEntry.sig);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
reasons.push('ed25519-malformed');
|
|
94
|
+
}
|
|
95
|
+
if (edSig) {
|
|
96
|
+
try {
|
|
97
|
+
edOk = await verifyEd25519Browser(edSig, message, input.ed25519_pub);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
edOk = false;
|
|
101
|
+
}
|
|
102
|
+
if (!edOk)
|
|
103
|
+
reasons.push('ed25519-invalid');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (mlEntry) {
|
|
107
|
+
let mlSig = null;
|
|
108
|
+
try {
|
|
109
|
+
mlSig = decodeBase64StrictBrowser(mlEntry.sig);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
reasons.push('ml-dsa-malformed');
|
|
113
|
+
}
|
|
114
|
+
if (mlSig) {
|
|
115
|
+
try {
|
|
116
|
+
mlOk = verifyMlDsa65Browser(mlSig, message, input.ml_dsa_pub);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
mlOk = false;
|
|
120
|
+
}
|
|
121
|
+
if (!mlOk)
|
|
122
|
+
reasons.push('ml-dsa-invalid');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { valid: edOk && mlOk && !!edEntry && !!mlEntry, reasons };
|
|
126
|
+
}
|
|
127
|
+
// ── OID helpers (browser, ASYNC via WebCrypto SHA-256) ────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Compute an OID over an arbitrary canonical-compatible value using WebCrypto
|
|
130
|
+
* SHA-256. Returns `sha256:` followed by 64 lowercase hex characters.
|
|
131
|
+
*
|
|
132
|
+
* ASYNC counterpart of the node `oidOf`: same canonical bytes and same
|
|
133
|
+
* `sha256:`-prefixed output, but returns a Promise because subtle.digest is
|
|
134
|
+
* Promise-based. Byte-identical result to the node entry for the same input.
|
|
135
|
+
*/
|
|
136
|
+
export async function oidOf(canonical) {
|
|
137
|
+
const bytes = new TextEncoder().encode(canonicalize(canonical));
|
|
138
|
+
return sha256HexPrefixed(bytes);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Compute an OID directly from already-canonicalized bytes using WebCrypto
|
|
142
|
+
* SHA-256. ASYNC counterpart of the node `oidOfCanonical`.
|
|
143
|
+
*/
|
|
144
|
+
export async function oidOfCanonical(canonical) {
|
|
145
|
+
const bytes = typeof canonical === 'string' ? new TextEncoder().encode(canonical) : canonical;
|
|
146
|
+
return sha256HexPrefixed(bytes);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Compute the OID of a full CDRO over its content core (see `cdroContentCore`),
|
|
150
|
+
* using WebCrypto SHA-256. ASYNC counterpart of the node `cdroOid`; yields the
|
|
151
|
+
* SAME OID whether the object is pre- or post-attestation, and the SAME value
|
|
152
|
+
* the node entry produces for the same object.
|
|
153
|
+
*/
|
|
154
|
+
export async function cdroOid(cdro) {
|
|
155
|
+
return oidOf(cdroContentCore(cdro));
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=verify-browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-browser.js","sourceRoot":"","sources":["../src/verify-browser.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synoi/sraid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Reference TypeScript library for the SynOI SRAID (Self-Routing Addressable Identity Data) protocol - types, canonical object serializer, OID computation, signature verification, and validators for L0 of the SRAID Stack.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://synoi.systems",
|
|
@@ -27,6 +27,23 @@
|
|
|
27
27
|
"./canonicalize": {
|
|
28
28
|
"types": "./dist/canonicalize.d.ts",
|
|
29
29
|
"default": "./dist/canonicalize.js"
|
|
30
|
+
},
|
|
31
|
+
"./verify-browser": {
|
|
32
|
+
"types": "./dist/verify-browser.d.ts",
|
|
33
|
+
"browser": "./dist/verify-browser.js",
|
|
34
|
+
"import": "./dist/verify-browser.js",
|
|
35
|
+
"require": "./dist/verify-browser.js",
|
|
36
|
+
"default": "./dist/verify-browser.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"typesVersions": {
|
|
40
|
+
"*": {
|
|
41
|
+
"canonicalize": [
|
|
42
|
+
"dist/canonicalize.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"verify-browser": [
|
|
45
|
+
"dist/verify-browser.d.ts"
|
|
46
|
+
]
|
|
30
47
|
}
|
|
31
48
|
},
|
|
32
49
|
"files": [
|
|
@@ -40,7 +57,8 @@
|
|
|
40
57
|
"scripts": {
|
|
41
58
|
"build": "tsc",
|
|
42
59
|
"typecheck": "tsc --noEmit",
|
|
43
|
-
"test": "npx tsx test/run-all.ts"
|
|
60
|
+
"test": "npx tsx test/run-all.ts",
|
|
61
|
+
"prepublishOnly": "npm run build && npm test"
|
|
44
62
|
},
|
|
45
63
|
"dependencies": {
|
|
46
64
|
"@noble/curves": "^1.9.7",
|
|
@@ -49,6 +67,7 @@
|
|
|
49
67
|
},
|
|
50
68
|
"devDependencies": {
|
|
51
69
|
"@types/node": "^20.12.0",
|
|
70
|
+
"esbuild": "^0.28.0",
|
|
52
71
|
"tsx": "^4.22.2",
|
|
53
72
|
"typescript": "^5.4.5"
|
|
54
73
|
},
|
package/src/attestation.ts
CHANGED
|
@@ -30,76 +30,36 @@
|
|
|
30
30
|
* Identity stability: signatures live in a detached `signatures[]` array and
|
|
31
31
|
* are NEVER part of the OID hash input (see `cdroContentCore` in oid.ts), so
|
|
32
32
|
* adding, rotating, or replacing a signature never changes the object's OID.
|
|
33
|
+
*
|
|
34
|
+
* NODE ENTRY: this module verifies via node:crypto (Ed25519 native, ML-DSA-65
|
|
35
|
+
* native-with-@noble-fallback) and is SYNCHRONOUS. The PAE, algorithm ids,
|
|
36
|
+
* types, AND-policy lookup, and envelope-shape predicate are shared with the
|
|
37
|
+
* browser verify surface via ./internal/attestation-core.ts (pure, no
|
|
38
|
+
* node:crypto); only the verify orchestration below is node-specific. The
|
|
39
|
+
* browser-safe async equivalent lives in ./verify-browser.ts.
|
|
33
40
|
*/
|
|
34
41
|
|
|
35
42
|
import { verifyEd25519 } from './ed25519.js'
|
|
36
43
|
import { decodeBase64Strict } from './internal/base64.js'
|
|
37
44
|
import { verifyMlDsa65 } from './mldsa.js'
|
|
38
|
-
import
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* Binding the payloadType (and both lengths) into the signed bytes is what
|
|
57
|
-
* makes a signature non-transferable across object types. Returns the raw
|
|
58
|
-
* bytes to be signed/verified.
|
|
59
|
-
*/
|
|
60
|
-
export function pae(payloadType: string, payload: string | Uint8Array): Uint8Array {
|
|
61
|
-
const enc = new TextEncoder()
|
|
62
|
-
const typeBytes = enc.encode(payloadType)
|
|
63
|
-
const bodyBytes = typeof payload === 'string' ? enc.encode(payload) : payload
|
|
64
|
-
|
|
65
|
-
const prefix = enc.encode(
|
|
66
|
-
`DSSEv1 ${typeBytes.length} ${payloadType} ${bodyBytes.length} `,
|
|
67
|
-
)
|
|
68
|
-
const out = new Uint8Array(prefix.length + bodyBytes.length)
|
|
69
|
-
out.set(prefix, 0)
|
|
70
|
-
out.set(bodyBytes, prefix.length)
|
|
71
|
-
return out
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface VerifyAttestationInput {
|
|
75
|
-
/**
|
|
76
|
-
* The DSSE attestation envelope to verify. Its `payloadType` is bound into
|
|
77
|
-
* the PAE; its `payload` is the canonical UTF-8 string that was signed.
|
|
78
|
-
*/
|
|
79
|
-
envelope: AttestationEnvelope
|
|
80
|
-
/** Raw 32-byte Ed25519 public key. */
|
|
81
|
-
ed25519_pub: Uint8Array
|
|
82
|
-
/** Raw ML-DSA-65 public key bytes. */
|
|
83
|
-
ml_dsa_pub: Uint8Array
|
|
84
|
-
/**
|
|
85
|
-
* Optional. If supplied, the verifier asserts the envelope's `payloadType`
|
|
86
|
-
* equals this value before verifying signatures — an explicit type-pinning
|
|
87
|
-
* check on top of the structural PAE binding. A mismatch fails with
|
|
88
|
-
* `payload-type-mismatch`.
|
|
89
|
-
*/
|
|
90
|
-
expectedPayloadType?: string
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface VerifyAttestationResult {
|
|
94
|
-
/** True only when BOTH required signatures verified over the same PAE. */
|
|
95
|
-
valid: boolean
|
|
96
|
-
/**
|
|
97
|
-
* Human-readable failure reasons. Empty when valid. Possible values:
|
|
98
|
-
* 'envelope-malformed', 'payload-type-mismatch', 'missing-ed25519',
|
|
99
|
-
* 'missing-ml-dsa-65', 'ed25519-malformed', 'ml-dsa-malformed',
|
|
100
|
-
* 'ed25519-invalid', 'ml-dsa-invalid'.
|
|
101
|
-
*/
|
|
102
|
-
reasons: string[]
|
|
45
|
+
import {
|
|
46
|
+
ALG_ED25519,
|
|
47
|
+
ALG_ML_DSA_65,
|
|
48
|
+
findSig,
|
|
49
|
+
isWellFormedEnvelope,
|
|
50
|
+
pae,
|
|
51
|
+
type VerifyAttestationInput,
|
|
52
|
+
type VerifyAttestationResult,
|
|
53
|
+
} from './internal/attestation-core.js'
|
|
54
|
+
|
|
55
|
+
// Re-export the crypto-agnostic core so the public surface is unchanged:
|
|
56
|
+
// `import { pae, ALG_ED25519, ALG_ML_DSA_65 } from '@synoi/sraid'` still works.
|
|
57
|
+
export {
|
|
58
|
+
ALG_ED25519,
|
|
59
|
+
ALG_ML_DSA_65,
|
|
60
|
+
pae,
|
|
61
|
+
type VerifyAttestationInput,
|
|
62
|
+
type VerifyAttestationResult,
|
|
103
63
|
}
|
|
104
64
|
|
|
105
65
|
/**
|
|
@@ -113,14 +73,7 @@ export function verifyAttestation(input: VerifyAttestationInput): VerifyAttestat
|
|
|
113
73
|
const reasons: string[] = []
|
|
114
74
|
|
|
115
75
|
const env = input.envelope
|
|
116
|
-
if (
|
|
117
|
-
env === null ||
|
|
118
|
-
typeof env !== 'object' ||
|
|
119
|
-
typeof env.payloadType !== 'string' ||
|
|
120
|
-
env.payloadType.length === 0 ||
|
|
121
|
-
typeof env.payload !== 'string' ||
|
|
122
|
-
!Array.isArray(env.signatures)
|
|
123
|
-
) {
|
|
76
|
+
if (!isWellFormedEnvelope(env)) {
|
|
124
77
|
return { valid: false, reasons: ['envelope-malformed'] }
|
|
125
78
|
}
|
|
126
79
|
|
|
@@ -183,22 +136,10 @@ export function verifyAttestation(input: VerifyAttestationInput): VerifyAttestat
|
|
|
183
136
|
|
|
184
137
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
185
138
|
|
|
186
|
-
function findSig(
|
|
187
|
-
sigs: readonly AttestationSignature[],
|
|
188
|
-
alg: string,
|
|
189
|
-
): AttestationSignature | undefined {
|
|
190
|
-
for (const s of sigs) {
|
|
191
|
-
if (s && typeof s === 'object' && s.alg === alg && typeof s.sig === 'string') {
|
|
192
|
-
return s
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return undefined
|
|
196
|
-
}
|
|
197
|
-
|
|
198
139
|
// Strict standard base64. Throws Error('base64-malformed') on any deviation
|
|
199
|
-
// (illegal char, bad length, bad padding) rather than silently truncating.
|
|
200
|
-
//
|
|
201
|
-
//
|
|
140
|
+
// (illegal char, bad length, bad padding) rather than silently truncating. The
|
|
141
|
+
// call sites wrap this in try/catch and map the throw to a *-malformed reason,
|
|
142
|
+
// so verifyAttestation never throws.
|
|
202
143
|
function fromBase64(s: string): Uint8Array {
|
|
203
144
|
return decodeBase64Strict(s)
|
|
204
145
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synoi/sraid — internal/attestation-core.ts
|
|
3
|
+
*
|
|
4
|
+
* The crypto-agnostic core of the L2 DSSE attestation profile: the PAE
|
|
5
|
+
* encoding, the algorithm identifiers, the public verify input/result types,
|
|
6
|
+
* the both-required signature lookup, and the envelope-shape predicate.
|
|
7
|
+
*
|
|
8
|
+
* This is a PURE module — no crypto, no node:crypto, no Buffer — so the same
|
|
9
|
+
* PAE bytes, AND-policy shape, and type contract are shared byte-for-byte by
|
|
10
|
+
* BOTH the node default entry (attestation.ts, node:crypto verifiers) and the
|
|
11
|
+
* browser verify surface (verify-browser.ts, WebCrypto + @noble verifiers).
|
|
12
|
+
* Divergent PAE or shape logic between the two would be a signature-validity
|
|
13
|
+
* hazard; keeping it in one place removes that risk.
|
|
14
|
+
*
|
|
15
|
+
* The verify ORCHESTRATION (decode each sig, run both verifiers, gate on the
|
|
16
|
+
* AND policy) is intentionally NOT here: the node path is synchronous
|
|
17
|
+
* (node:crypto verify) and the browser path is asynchronous (WebCrypto verify),
|
|
18
|
+
* so a single shared body cannot serve both. Each entry keeps its own thin
|
|
19
|
+
* orchestration and reuses these leaf helpers.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { AttestationEnvelope, AttestationSignature } from '../types.js'
|
|
23
|
+
|
|
24
|
+
/** The single supported algorithm identifiers for the JSON profile. */
|
|
25
|
+
export const ALG_ED25519 = 'ed25519'
|
|
26
|
+
export const ALG_ML_DSA_65 = 'ml-dsa-65'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The DSSE Pre-Authentication Encoding (PAE).
|
|
30
|
+
*
|
|
31
|
+
* Per the DSSE spec:
|
|
32
|
+
*
|
|
33
|
+
* PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
|
|
34
|
+
*
|
|
35
|
+
* where:
|
|
36
|
+
* - SP is a single ASCII space (0x20),
|
|
37
|
+
* - LEN(x) is the ASCII-decimal byte length of x's UTF-8 encoding,
|
|
38
|
+
* - type and body are the raw UTF-8 bytes (NOT base64).
|
|
39
|
+
*
|
|
40
|
+
* Binding the payloadType (and both lengths) into the signed bytes is what
|
|
41
|
+
* makes a signature non-transferable across object types. Returns the raw
|
|
42
|
+
* bytes to be signed/verified.
|
|
43
|
+
*/
|
|
44
|
+
export function pae(payloadType: string, payload: string | Uint8Array): Uint8Array {
|
|
45
|
+
const enc = new TextEncoder()
|
|
46
|
+
const typeBytes = enc.encode(payloadType)
|
|
47
|
+
const bodyBytes = typeof payload === 'string' ? enc.encode(payload) : payload
|
|
48
|
+
|
|
49
|
+
const prefix = enc.encode(
|
|
50
|
+
`DSSEv1 ${typeBytes.length} ${payloadType} ${bodyBytes.length} `,
|
|
51
|
+
)
|
|
52
|
+
const out = new Uint8Array(prefix.length + bodyBytes.length)
|
|
53
|
+
out.set(prefix, 0)
|
|
54
|
+
out.set(bodyBytes, prefix.length)
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface VerifyAttestationInput {
|
|
59
|
+
/**
|
|
60
|
+
* The DSSE attestation envelope to verify. Its `payloadType` is bound into
|
|
61
|
+
* the PAE; its `payload` is the canonical UTF-8 string that was signed.
|
|
62
|
+
*/
|
|
63
|
+
envelope: AttestationEnvelope
|
|
64
|
+
/** Raw 32-byte Ed25519 public key. */
|
|
65
|
+
ed25519_pub: Uint8Array
|
|
66
|
+
/** Raw ML-DSA-65 public key bytes. */
|
|
67
|
+
ml_dsa_pub: Uint8Array
|
|
68
|
+
/**
|
|
69
|
+
* Optional. If supplied, the verifier asserts the envelope's `payloadType`
|
|
70
|
+
* equals this value before verifying signatures — an explicit type-pinning
|
|
71
|
+
* check on top of the structural PAE binding. A mismatch fails with
|
|
72
|
+
* `payload-type-mismatch`.
|
|
73
|
+
*/
|
|
74
|
+
expectedPayloadType?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface VerifyAttestationResult {
|
|
78
|
+
/** True only when BOTH required signatures verified over the same PAE. */
|
|
79
|
+
valid: boolean
|
|
80
|
+
/**
|
|
81
|
+
* Human-readable failure reasons. Empty when valid. Possible values:
|
|
82
|
+
* 'envelope-malformed', 'payload-type-mismatch', 'missing-ed25519',
|
|
83
|
+
* 'missing-ml-dsa-65', 'ed25519-malformed', 'ml-dsa-malformed',
|
|
84
|
+
* 'ed25519-invalid', 'ml-dsa-invalid'.
|
|
85
|
+
*/
|
|
86
|
+
reasons: string[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Structural well-formedness of an attestation envelope: a non-null object with
|
|
91
|
+
* a non-empty string `payloadType`, a string `payload`, and an array
|
|
92
|
+
* `signatures`. A false result maps to the `envelope-malformed` reason. This is
|
|
93
|
+
* the SINGLE definition of envelope shape, shared by the node and browser
|
|
94
|
+
* verifiers so they reject the same inputs.
|
|
95
|
+
*/
|
|
96
|
+
export function isWellFormedEnvelope(env: unknown): env is AttestationEnvelope {
|
|
97
|
+
return (
|
|
98
|
+
env !== null &&
|
|
99
|
+
typeof env === 'object' &&
|
|
100
|
+
typeof (env as AttestationEnvelope).payloadType === 'string' &&
|
|
101
|
+
(env as AttestationEnvelope).payloadType.length > 0 &&
|
|
102
|
+
typeof (env as AttestationEnvelope).payload === 'string' &&
|
|
103
|
+
Array.isArray((env as AttestationEnvelope).signatures)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find the first well-formed signature entry for `alg` (a `{ alg, sig }` object
|
|
109
|
+
* whose `sig` is a string). Returns undefined when absent; the AND policy in
|
|
110
|
+
* each verifier turns a missing pair member into a `missing-*` reason.
|
|
111
|
+
*/
|
|
112
|
+
export function findSig(
|
|
113
|
+
sigs: readonly AttestationSignature[],
|
|
114
|
+
alg: string,
|
|
115
|
+
): AttestationSignature | undefined {
|
|
116
|
+
for (const s of sigs) {
|
|
117
|
+
if (s && typeof s === 'object' && s.alg === alg && typeof s.sig === 'string') {
|
|
118
|
+
return s
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synoi/sraid — internal/base64-browser.ts
|
|
3
|
+
*
|
|
4
|
+
* Strict standard-base64 (RFC 4648 §4, with `=` padding) decode for the
|
|
5
|
+
* BROWSER verify surface.
|
|
6
|
+
*
|
|
7
|
+
* INTERNAL: not exported from any public entry.
|
|
8
|
+
*
|
|
9
|
+
* Why a separate decoder: the node decoder (./base64.ts `decodeBase64Strict`)
|
|
10
|
+
* finishes with `Buffer.from(s, 'base64')`. `Buffer` is a NODE global that does
|
|
11
|
+
* not exist in a browser / service-worker / Chrome-extension context — and,
|
|
12
|
+
* crucially, it is NOT a `node:` specifier, so an esbuild `node:`-reference scan
|
|
13
|
+
* would NOT catch it. A `Buffer`-based decode would bundle clean yet throw
|
|
14
|
+
* `ReferenceError: Buffer is not defined` at runtime. This decoder is
|
|
15
|
+
* Buffer-free: it reuses the pure `assertBase64` validation (regex/length/
|
|
16
|
+
* padding only, from ./base64-validate.js — NOT ./base64.js, whose graph pulls
|
|
17
|
+
* in the `Buffer` decode) and decodes via the standard `atob` global, which IS
|
|
18
|
+
* available in browsers, dedicated/shared workers, and MV3 extension service
|
|
19
|
+
* workers.
|
|
20
|
+
*
|
|
21
|
+
* Same strict contract as the node path: throws Error('base64-malformed') on
|
|
22
|
+
* any deviation (illegal char, bad length, bad padding) rather than silently
|
|
23
|
+
* truncating, so a malformed signature is rejected loudly, never decoded to a
|
|
24
|
+
* short prefix that then "verifies" against something unintended.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { assertBase64 } from './base64-validate.js'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Strictly decode standard base64 to raw bytes using only browser-safe globals.
|
|
31
|
+
* Throws Error('base64-malformed') on any non-conforming input.
|
|
32
|
+
*/
|
|
33
|
+
export function decodeBase64StrictBrowser(s: string): Uint8Array {
|
|
34
|
+
assertBase64(s)
|
|
35
|
+
const bin = atob(s)
|
|
36
|
+
const out = new Uint8Array(bin.length)
|
|
37
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @synoi/sraid — internal/base64-validate.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure strict standard-base64 (RFC 4648 §4, with `=` padding) VALIDATION.
|
|
5
|
+
*
|
|
6
|
+
* INTERNAL: not exported from any public entry.
|
|
7
|
+
*
|
|
8
|
+
* This is the Buffer-free, node:crypto-free half of base64 handling, split out
|
|
9
|
+
* so the browser decoder (./base64-browser.ts) can validate without importing
|
|
10
|
+
* ./base64.ts (whose `decodeBase64Strict` uses the node `Buffer` global — safe
|
|
11
|
+
* in node, a runtime `ReferenceError` in a browser). The node decoder re-uses
|
|
12
|
+
* this same validator, so both paths reject exactly the same inputs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Standard base64 alphabet; `=` only as 1-2 trailing pad chars. Total length
|
|
16
|
+
// must be a multiple of 4.
|
|
17
|
+
const STD_B64 = /^[A-Za-z0-9+/]*={0,2}$/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate strict standard base64. Throws Error('base64-malformed') on any
|
|
21
|
+
* non-conforming input.
|
|
22
|
+
*/
|
|
23
|
+
export function assertBase64(s: string): void {
|
|
24
|
+
if (typeof s !== 'string') throw new Error('base64-malformed')
|
|
25
|
+
if (s.length % 4 !== 0) throw new Error('base64-malformed')
|
|
26
|
+
if (!STD_B64.test(s)) throw new Error('base64-malformed')
|
|
27
|
+
// `=` may appear only in the final 1-2 positions. The regex `={0,2}` anchored
|
|
28
|
+
// at end already guarantees pad chars are contiguous and trailing, but a pad
|
|
29
|
+
// char earlier in the body would have been rejected by the alphabet class —
|
|
30
|
+
// so an in-body `=` cannot pass STD_B64. No extra check needed.
|
|
31
|
+
}
|
package/src/internal/base64.ts
CHANGED
|
@@ -14,25 +14,16 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Canonical inputs carry no surrounding whitespace, so no trim is applied;
|
|
16
16
|
* leading/trailing whitespace is itself a rejection.
|
|
17
|
+
*
|
|
18
|
+
* The strict VALIDATION (alphabet/length/padding) lives in the pure, Buffer-
|
|
19
|
+
* free ./base64-validate.ts so the browser decoder can share it; this module
|
|
20
|
+
* adds the node `Buffer`-based decode on top and re-exports `assertBase64` so
|
|
21
|
+
* existing importers of `./base64.js` are unchanged.
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
// must be a multiple of 4.
|
|
21
|
-
const STD_B64 = /^[A-Za-z0-9+/]*={0,2}$/
|
|
24
|
+
import { assertBase64 } from './base64-validate.js'
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
* Validate strict standard base64. Throws Error('base64-malformed') on any
|
|
25
|
-
* non-conforming input.
|
|
26
|
-
*/
|
|
27
|
-
export function assertBase64(s: string): void {
|
|
28
|
-
if (typeof s !== 'string') throw new Error('base64-malformed')
|
|
29
|
-
if (s.length % 4 !== 0) throw new Error('base64-malformed')
|
|
30
|
-
if (!STD_B64.test(s)) throw new Error('base64-malformed')
|
|
31
|
-
// `=` may appear only in the final 1-2 positions. The regex `={0,2}` anchored
|
|
32
|
-
// at end already guarantees pad chars are contiguous and trailing, but a pad
|
|
33
|
-
// char earlier in the body would have been rejected by the alphabet class —
|
|
34
|
-
// so an in-body `=` cannot pass STD_B64. No extra check needed.
|
|
35
|
-
}
|
|
26
|
+
export { assertBase64 }
|
|
36
27
|
|
|
37
28
|
/**
|
|
38
29
|
* Strictly decode standard base64 to raw bytes. Throws
|