@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.
Files changed (57) hide show
  1. package/README.md +51 -6
  2. package/dist/attestation.d.ts +8 -49
  3. package/dist/attestation.d.ts.map +1 -1
  4. package/dist/attestation.js +15 -46
  5. package/dist/attestation.js.map +1 -1
  6. package/dist/internal/attestation-core.d.ts +85 -0
  7. package/dist/internal/attestation-core.d.ts.map +1 -0
  8. package/dist/internal/attestation-core.js +78 -0
  9. package/dist/internal/attestation-core.js.map +1 -0
  10. package/dist/internal/base64-browser.d.ts +31 -0
  11. package/dist/internal/base64-browser.d.ts.map +1 -0
  12. package/dist/internal/base64-browser.js +39 -0
  13. package/dist/internal/base64-browser.js.map +1 -0
  14. package/dist/internal/base64-validate.d.ts +19 -0
  15. package/dist/internal/base64-validate.d.ts.map +1 -0
  16. package/dist/internal/base64-validate.js +33 -0
  17. package/dist/internal/base64-validate.js.map +1 -0
  18. package/dist/internal/base64.d.ts +7 -5
  19. package/dist/internal/base64.d.ts.map +1 -1
  20. package/dist/internal/base64.js +7 -19
  21. package/dist/internal/base64.js.map +1 -1
  22. package/dist/internal/content-core.d.ts +68 -0
  23. package/dist/internal/content-core.d.ts.map +1 -0
  24. package/dist/internal/content-core.js +87 -0
  25. package/dist/internal/content-core.js.map +1 -0
  26. package/dist/internal/ed25519-browser.d.ts +33 -0
  27. package/dist/internal/ed25519-browser.d.ts.map +1 -0
  28. package/dist/internal/ed25519-browser.js +79 -0
  29. package/dist/internal/ed25519-browser.js.map +1 -0
  30. package/dist/internal/mldsa-browser.d.ts +27 -0
  31. package/dist/internal/mldsa-browser.d.ts.map +1 -0
  32. package/dist/internal/mldsa-browser.js +35 -0
  33. package/dist/internal/mldsa-browser.js.map +1 -0
  34. package/dist/internal/sha256-browser.d.ts +17 -0
  35. package/dist/internal/sha256-browser.d.ts.map +1 -0
  36. package/dist/internal/sha256-browser.js +27 -0
  37. package/dist/internal/sha256-browser.js.map +1 -0
  38. package/dist/oid.d.ts +2 -52
  39. package/dist/oid.d.ts.map +1 -1
  40. package/dist/oid.js +9 -71
  41. package/dist/oid.js.map +1 -1
  42. package/dist/verify-browser.d.ts +80 -0
  43. package/dist/verify-browser.d.ts.map +1 -0
  44. package/dist/verify-browser.js +157 -0
  45. package/dist/verify-browser.js.map +1 -0
  46. package/package.json +21 -2
  47. package/src/attestation.ts +29 -88
  48. package/src/internal/attestation-core.ts +122 -0
  49. package/src/internal/base64-browser.ts +39 -0
  50. package/src/internal/base64-validate.ts +31 -0
  51. package/src/internal/base64.ts +7 -16
  52. package/src/internal/content-core.ts +88 -0
  53. package/src/internal/ed25519-browser.ts +85 -0
  54. package/src/internal/mldsa-browser.ts +39 -0
  55. package/src/internal/sha256-browser.ts +28 -0
  56. package/src/oid.ts +10 -73
  57. 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAwB,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAA;AACxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EACL,WAAW,EACX,aAAa,EACb,OAAO,EACP,oBAAoB,EACpB,GAAG,GAGJ,MAAM,gCAAgC,CAAA;AAEvC,iFAAiF;AAEjF,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,GAGd,CAAA;AAED,iFAAiF;AAEjF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAA6B;IAE7B,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAA;IAC1B,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,oBAAoB,CAAC,EAAE,CAAA;IAC1D,CAAC;IAED,IACE,KAAK,CAAC,mBAAmB,KAAK,SAAS;QACvC,GAAG,CAAC,WAAW,KAAK,KAAK,CAAC,mBAAmB,EAC7C,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,uBAAuB,CAAC,EAAE,CAAA;IAC7D,CAAC;IAED,8DAA8D;IAC9D,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IAEjD,uEAAuE;IACvE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;IACpD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;IAEtD,IAAI,CAAC,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;IAC7C,IAAI,CAAC,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;IAE/C,IAAI,IAAI,GAAG,KAAK,CAAA;IAChB,IAAI,IAAI,GAAG,KAAK,CAAA;IAEhB,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,KAAK,GAAsB,IAAI,CAAA;QACnC,IAAI,CAAC;YACH,KAAK,GAAG,yBAAyB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACnC,CAAC;QACD,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC;gBACH,IAAI,GAAG,MAAM,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;YACtE,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,GAAG,KAAK,CAAA;YACd,CAAC;YACD,IAAI,CAAC,IAAI;gBAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,KAAK,GAAsB,IAAI,CAAA;QACnC,IAAI,CAAC;YACH,KAAK,GAAG,yBAAyB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;QAClC,CAAC;QACD,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC;gBACH,IAAI,GAAG,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;YAC/D,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,GAAG,KAAK,CAAA;YACd,CAAC;YACD,IAAI,CAAC,IAAI;gBAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QAC3C,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,CAAA;AACnE,CAAC;AAED,iFAAiF;AAEjF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,SAAkB;IAC5C,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAA;IAC/D,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAA;AACjC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAA8B;IACjE,MAAM,KAAK,GACT,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACjF,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAA;AACjC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAa;IACzC,OAAO,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAA;AACrC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synoi/sraid",
3
- "version": "0.2.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
  },
@@ -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 type { AttestationEnvelope, AttestationSignature } from './types.js'
39
-
40
- /** The single supported algorithm identifiers for the JSON profile. */
41
- export const ALG_ED25519 = 'ed25519'
42
- export const ALG_ML_DSA_65 = 'ml-dsa-65'
43
-
44
- /**
45
- * The DSSE Pre-Authentication Encoding (PAE).
46
- *
47
- * Per the DSSE spec:
48
- *
49
- * PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
50
- *
51
- * where:
52
- * - SP is a single ASCII space (0x20),
53
- * - LEN(x) is the ASCII-decimal byte length of x's UTF-8 encoding,
54
- * - type and body are the raw UTF-8 bytes (NOT base64).
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
- // Both call sites wrap this in try/catch and map the throw to a *-malformed
201
- // reason, so verifyAttestation never throws.
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
+ }
@@ -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
- // Standard base64 alphabet; `=` only as 1-2 trailing pad chars. Total length
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