@freedomofpress/sigstore-browser 0.1.13 → 0.1.15

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 (60) hide show
  1. package/.github/workflows/conformance.yml +1 -1
  2. package/.github/workflows/publish.yml +1 -1
  3. package/README.md +20 -16
  4. package/dist/bundle.d.ts +2 -0
  5. package/dist/bundle.d.ts.map +1 -1
  6. package/dist/bundle.js +66 -1
  7. package/dist/bundle.js.map +1 -1
  8. package/dist/interfaces.d.ts +17 -2
  9. package/dist/interfaces.d.ts.map +1 -1
  10. package/dist/interfaces.js +14 -0
  11. package/dist/interfaces.js.map +1 -1
  12. package/dist/policy.d.ts +3 -3
  13. package/dist/policy.d.ts.map +1 -1
  14. package/dist/policy.js +4 -4
  15. package/dist/policy.js.map +1 -1
  16. package/dist/sigstore.d.ts +8 -8
  17. package/dist/sigstore.d.ts.map +1 -1
  18. package/dist/sigstore.js +159 -362
  19. package/dist/sigstore.js.map +1 -1
  20. package/dist/timestamp/tsa.d.ts +6 -2
  21. package/dist/timestamp/tsa.d.ts.map +1 -1
  22. package/dist/timestamp/tsa.js +15 -18
  23. package/dist/timestamp/tsa.js.map +1 -1
  24. package/dist/tlog/body.d.ts +3 -1
  25. package/dist/tlog/body.d.ts.map +1 -1
  26. package/dist/tlog/body.js +18 -5
  27. package/dist/tlog/body.js.map +1 -1
  28. package/dist/tlog/checkpoint.d.ts +2 -2
  29. package/dist/tlog/checkpoint.d.ts.map +1 -1
  30. package/dist/tlog/checkpoint.js +17 -95
  31. package/dist/tlog/checkpoint.js.map +1 -1
  32. package/dist/tlog/dsse.d.ts +3 -2
  33. package/dist/tlog/dsse.d.ts.map +1 -1
  34. package/dist/tlog/dsse.js +8 -5
  35. package/dist/tlog/dsse.js.map +1 -1
  36. package/dist/tlog/hashedrekord.d.ts +3 -2
  37. package/dist/tlog/hashedrekord.d.ts.map +1 -1
  38. package/dist/tlog/hashedrekord.js +8 -5
  39. package/dist/tlog/hashedrekord.js.map +1 -1
  40. package/dist/tlog/intoto.d.ts +3 -2
  41. package/dist/tlog/intoto.d.ts.map +1 -1
  42. package/dist/tlog/intoto.js +3 -1
  43. package/dist/tlog/intoto.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/bundle.ts +87 -0
  46. package/src/ed25519.browser.test.ts +76 -0
  47. package/src/index.browser.test.ts +46 -1
  48. package/src/interfaces.ts +29 -2
  49. package/src/policy.ts +5 -5
  50. package/src/security.browser.test.ts +249 -0
  51. package/src/sigstore.ts +191 -497
  52. package/src/timestamp/tsa.ts +29 -28
  53. package/src/tlog/body.ts +24 -5
  54. package/src/tlog/checkpoint.ts +22 -122
  55. package/src/tlog/dsse.ts +12 -6
  56. package/src/tlog/hashedrekord.ts +14 -8
  57. package/src/tlog/intoto.ts +5 -2
  58. package/src/verify.browser.test.ts +281 -0
  59. package/test/fixtures/rekor2.ts +14 -0
  60. package/test/fixtures/sigstore.ts +13 -0
@@ -10,22 +10,24 @@
10
10
  * - Adds verifyHashedRekordV002Body() - new functionality (reference only supports v0.0.1)
11
11
  */
12
12
  import { base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
13
- export async function verifyHashedRekordBody(entry, bundle) {
13
+ import { assertLoggedCertificate } from "./body.js";
14
+ export async function verifyHashedRekordBody(entry, bundle, cert) {
14
15
  const hashedRekordEntry = entry;
15
16
  switch (hashedRekordEntry.apiVersion) {
16
17
  case "0.0.1":
17
- return verifyHashedRekordV001Body(hashedRekordEntry, bundle);
18
+ return verifyHashedRekordV001Body(hashedRekordEntry, bundle, cert);
18
19
  case "0.0.2":
19
- return verifyHashedRekordV002Body(hashedRekordEntry, bundle);
20
+ return verifyHashedRekordV002Body(hashedRekordEntry, bundle, cert);
20
21
  default:
21
22
  throw new Error(`Unsupported hashedrekord version: ${hashedRekordEntry.apiVersion}`);
22
23
  }
23
24
  }
24
- function verifyHashedRekordV001Body(entry, bundle) {
25
+ function verifyHashedRekordV001Body(entry, bundle, cert) {
25
26
  const spec = entry.spec;
26
27
  if (!bundle.messageSignature) {
27
28
  throw new Error("Bundle missing messageSignature for hashedrekord entry");
28
29
  }
30
+ assertLoggedCertificate(cert, spec.signature?.publicKey?.content, true);
29
31
  const tlogSig = spec.signature.content || "";
30
32
  const tlogSigBytes = base64ToUint8Array(tlogSig);
31
33
  const bundleSigBytes = base64ToUint8Array(bundle.messageSignature.signature);
@@ -40,11 +42,12 @@ function verifyHashedRekordV001Body(entry, bundle) {
40
42
  }
41
43
  }
42
44
  // New functionality for HashedRekord v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
43
- function verifyHashedRekordV002Body(entry, bundle) {
45
+ function verifyHashedRekordV002Body(entry, bundle, cert) {
44
46
  const spec = entry.spec.hashedRekordV002;
45
47
  if (!bundle.messageSignature) {
46
48
  throw new Error("Bundle missing messageSignature for hashedrekord v0.0.2 entry");
47
49
  }
50
+ assertLoggedCertificate(cert, spec?.signature?.verifier?.x509Certificate?.rawBytes, false);
48
51
  // NOTE: HashedRekord v0.0.2 uses single base64 encoding (unlike intoto which is double-encoded)
49
52
  // Verified against Sigstore conformance test suite bundles
50
53
  const tlogSig = spec.signature.content || "";
@@ -1 +1 @@
1
- {"version":3,"file":"hashedrekord.js","sourceRoot":"","sources":["../../src/tlog/hashedrekord.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hashedrekord.js","sourceRoot":"","sources":["../../src/tlog/hashedrekord.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { SigstoreBundle } from "../bundle.js";
2
- import type { RekorEntry } from "./body.js";
3
- export declare function verifyIntotoBody(entry: RekorEntry, bundle: SigstoreBundle): Promise<void>;
2
+ import type { X509Certificate } from "../x509/cert.js";
3
+ import { type RekorEntry } from "./body.js";
4
+ export declare function verifyIntotoBody(entry: RekorEntry, bundle: SigstoreBundle, cert: X509Certificate): Promise<void>;
4
5
  //# sourceMappingURL=intoto.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"intoto.d.ts","sourceRoot":"","sources":["../../src/tlog/intoto.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAiC5C,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,IAAI,CAAC,CAsDf"}
1
+ {"version":3,"file":"intoto.d.ts","sourceRoot":"","sources":["../../src/tlog/intoto.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAiCrE,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAuDf"}
@@ -11,7 +11,8 @@
11
11
  */
12
12
  import { base64Decode, base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "@freedomofpress/crypto-browser";
13
13
  import { getHashAlgorithm } from "../interfaces.js";
14
- export async function verifyIntotoBody(entry, bundle) {
14
+ import { assertLoggedCertificate } from "./body.js";
15
+ export async function verifyIntotoBody(entry, bundle, cert) {
15
16
  const intotoEntry = entry;
16
17
  if (intotoEntry.apiVersion !== "0.0.2") {
17
18
  throw new Error(`Unsupported intoto version: ${intotoEntry.apiVersion}`);
@@ -23,6 +24,7 @@ export async function verifyIntotoBody(entry, bundle) {
23
24
  if (!tlogEnvelope.signatures || tlogEnvelope.signatures.length !== 1) {
24
25
  throw new Error("Intoto entry must have exactly one signature");
25
26
  }
27
+ assertLoggedCertificate(cert, tlogEnvelope.signatures[0].publicKey, true);
26
28
  // The signature in the intoto tlog entry is double-base64-encoded:
27
29
  // 1. Raw signature bytes are base64-encoded (normal)
28
30
  // 2. That base64 string is base64-encoded again (for storage in JSON)
@@ -1 +1 @@
1
- {"version":3,"file":"intoto.js","sourceRoot":"","sources":["../../src/tlog/intoto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACpH,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAmCpD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAiB,EACjB,MAAsB;IAEtB,MAAM,WAAW,GAAG,KAAoB,CAAC;IAEzC,IAAI,WAAW,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,+BAA+B,WAAW,CAAC,UAAU,EAAE,CACxD,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;IAEvD,IAAI,CAAC,YAAY,CAAC,UAAU,IAAI,YAAY,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IAED,mEAAmE;IACnE,qDAAqD;IACrD,sEAAsE;IACtE,0DAA0D;IAC1D,MAAM,aAAa,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACrD,MAAM,cAAc,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,+DAA+D;IACnH,MAAM,YAAY,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,4CAA4C;IAErG,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,cAAc,GAAG,kBAAkB,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAEjF,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,cAAc,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAChE,CAAC;QAED,MAAM,OAAO,GAAG,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACjF,MAAM,aAAa,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAElF,MAAM,YAAY,GAAG,kBAAkB,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrE,MAAM,eAAe,GAAG,IAAI,UAAU,CACpC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAA4B,CAAC,CAClE,CAAC;QAEF,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE,eAAe,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"intoto.js","sourceRoot":"","sources":["../../src/tlog/intoto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACpH,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,OAAO,EAAE,uBAAuB,EAAmB,MAAM,WAAW,CAAC;AAiCrE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAiB,EACjB,MAAsB,EACtB,IAAqB;IAErB,MAAM,WAAW,GAAG,KAAoB,CAAC;IAEzC,IAAI,WAAW,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,+BAA+B,WAAW,CAAC,UAAU,EAAE,CACxD,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;IAEvD,IAAI,CAAC,YAAY,CAAC,UAAU,IAAI,YAAY,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,uBAAuB,CAAC,IAAI,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAE1E,mEAAmE;IACnE,qDAAqD;IACrD,sEAAsE;IACtE,0DAA0D;IAC1D,MAAM,aAAa,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACrD,MAAM,cAAc,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,+DAA+D;IACnH,MAAM,YAAY,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,4CAA4C;IAErG,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,cAAc,GAAG,kBAAkB,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAEjF,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,cAAc,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAChE,CAAC;QAED,MAAM,OAAO,GAAG,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACjF,MAAM,aAAa,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAElF,MAAM,YAAY,GAAG,kBAAkB,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrE,MAAM,eAAe,GAAG,IAAI,UAAU,CACpC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAA4B,CAAC,CAClE,CAAC;QAEF,IAAI,CAAC,eAAe,CAAC,aAAa,EAAE,eAAe,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freedomofpress/sigstore-browser",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "A minimal browser-only Sigstore verification library.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "vitest": "^3.2.4"
41
41
  },
42
42
  "dependencies": {
43
- "@freedomofpress/crypto-browser": "^0.1.7",
43
+ "@freedomofpress/crypto-browser": "^0.1.8",
44
44
  "@freedomofpress/tuf-browser": "^0.1.11",
45
45
  "@noble/curves": "^2.0.1"
46
46
  }
package/src/bundle.ts CHANGED
@@ -97,3 +97,90 @@ export interface InTotoSubject {
97
97
  name: string;
98
98
  digest: Record<string, string>;
99
99
  }
100
+
101
+ const isStr = (v: unknown): v is string => typeof v === "string";
102
+ // Bound the aggregate encoded data this verifier will decode, parse, or hash.
103
+ const MAX_BUNDLE_STRING_CHARS = 16 * 1024 * 1024;
104
+ function boundedStrings(): (v: unknown) => v is string {
105
+ let total = 0;
106
+ return (v: unknown): v is string => {
107
+ if (!isStr(v)) return false;
108
+ total += v.length;
109
+ return total <= MAX_BUNDLE_STRING_CHARS;
110
+ };
111
+ }
112
+ // Protobuf log indices, tree sizes and times are non-negative int64 values.
113
+ // Bound the string before BigInt conversion to avoid unbounded arithmetic.
114
+ const isInt64 = (v: unknown): v is string =>
115
+ isStr(v) && /^\d{1,19}$/.test(v) && BigInt(v) <= 9223372036854775807n;
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ const isObj = (v: unknown): v is Record<string, any> => typeof v === "object" && v !== null;
118
+
119
+ function check(cond: unknown, what: string): asserts cond {
120
+ if (!cond) throw new Error(`Invalid bundle: ${what}`);
121
+ }
122
+
123
+ function checkTimestampVerificationData(
124
+ data: unknown,
125
+ isBoundedStr: (v: unknown) => v is string,
126
+ ): void {
127
+ if (data == null) return;
128
+ check(isObj(data), "timestampVerificationData");
129
+ const ts = data.rfc3161Timestamps;
130
+ check(
131
+ ts === undefined || (Array.isArray(ts) && ts.length <= 32 && ts.every((t: unknown) => isObj(t) && isBoundedStr(t.signedTimestamp))),
132
+ "rfc3161Timestamps",
133
+ );
134
+ }
135
+
136
+ // Lives in verifyBundleTimestamp(), which every caller routes through.
137
+ export function assertTimestampVerificationData(data: unknown): void {
138
+ checkTimestampVerificationData(data, boundedStrings());
139
+ }
140
+
141
+ // Structural validation of a parsed bundle, replacing the compile-time-only type with runtime checks.
142
+ // Entry count is capped at 32 like sigstore-go does.
143
+ export function assertBundle(b: unknown): asserts b is SigstoreBundle {
144
+ const isBoundedStr = boundedStrings();
145
+ check(isObj(b) && isBoundedStr(b.mediaType) && isObj(b.verificationMaterial), "missing mediaType or verificationMaterial");
146
+ const vm = b.verificationMaterial;
147
+
148
+ const certs = vm.certificate ? [vm.certificate] : vm.x509CertificateChain?.certificates;
149
+ check(Array.isArray(certs) && certs.length > 0 && certs.length <= 32 && certs.every((c: unknown) => isObj(c) && isBoundedStr(c.rawBytes)), "certificate");
150
+
151
+ check(Array.isArray(vm.tlogEntries) && vm.tlogEntries.length > 0 && vm.tlogEntries.length <= 32, "tlogEntries");
152
+ for (const e of vm.tlogEntries) {
153
+ check(isObj(e) && isInt64(e.logIndex) && isBoundedStr(e.logId?.keyId) && isBoundedStr(e.canonicalizedBody), "tlog entry");
154
+ check(isBoundedStr(e.kindVersion?.kind) && isBoundedStr(e.kindVersion?.version), "tlog entry kindVersion");
155
+ check(
156
+ e.integratedTime == null || (isInt64(e.integratedTime) && Number.isFinite(new Date(Number(e.integratedTime) * 1000).getTime())),
157
+ "tlog entry integratedTime",
158
+ );
159
+ // A SET signs the integrated time, so a promise without one is malformed.
160
+ // SET canonicalization uses Number, which must not round the signed log index.
161
+ const set = e.inclusionPromise?.signedEntryTimestamp;
162
+ check(
163
+ e.inclusionPromise === undefined || (isBoundedStr(set) && set !== "" && isInt64(e.integratedTime) && Number.isSafeInteger(Number(e.logIndex))),
164
+ "inclusion promise",
165
+ );
166
+ const p = e.inclusionProof;
167
+ // An int64-sized tree requires at most 63 sibling hashes.
168
+ check(
169
+ p === undefined ||
170
+ (isObj(p) && isInt64(p.logIndex) && isInt64(p.treeSize) && isBoundedStr(p.rootHash) &&
171
+ Array.isArray(p.hashes) && p.hashes.length <= 63 && p.hashes.every(isBoundedStr) && isBoundedStr(p.checkpoint?.envelope)),
172
+ "inclusion proof",
173
+ );
174
+ }
175
+
176
+ checkTimestampVerificationData(vm.timestampVerificationData, isBoundedStr);
177
+
178
+ const { messageSignature: ms, dsseEnvelope: env } = b;
179
+ check((ms === undefined) !== (env === undefined), "exactly one of messageSignature or dsseEnvelope");
180
+ if (ms !== undefined) {
181
+ check(isObj(ms) && isBoundedStr(ms.signature) && isBoundedStr(ms.messageDigest?.algorithm) && isBoundedStr(ms.messageDigest?.digest), "messageSignature");
182
+ } else {
183
+ check(isObj(env) && isBoundedStr(env.payload) && isBoundedStr(env.payloadType), "dsseEnvelope");
184
+ check(Array.isArray(env.signatures) && env.signatures.length === 1 && isBoundedStr(env.signatures[0]?.sig), "dsseEnvelope must have exactly one signature");
185
+ }
186
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ // Replace crypto.subtle with a wrapper whose Ed25519 operations throw NotSupportedError, as browsers without native
4
+ // Ed25519 do. It must be in place before crypto-browser is first imported, because it probes once and caches the result.
5
+ const real = globalThis.crypto.subtle;
6
+ const noEd25519 = new Proxy(real, {
7
+ get(target, prop) {
8
+ const value = Reflect.get(target, prop) as unknown;
9
+ if (typeof value !== "function") return value;
10
+ return (...args: unknown[]) => {
11
+ const alg = (prop === "importKey" ? args[2] : args[0]) as { name?: string } | undefined;
12
+ if (alg?.name === "Ed25519") {
13
+ return Promise.reject(new DOMException("Unrecognized name.", "NotSupportedError"));
14
+ }
15
+ return (value as (...a: unknown[]) => unknown).apply(target, args);
16
+ };
17
+ },
18
+ });
19
+ Object.defineProperty(globalThis.crypto, "subtle", { value: noEd25519, configurable: true });
20
+
21
+ describe("Fix 7: Ed25519 log keys without native WebCrypto support", () => {
22
+ it("imports trusted root Ed25519 keys through the fallback and verifies a checkpoint", async () => {
23
+ await expect(crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"])).rejects.toThrow();
24
+
25
+ const { SigstoreVerifier } = await import("./sigstore.js");
26
+ const { verifyCheckpoint } = await import("./tlog/checkpoint.js");
27
+ const { Uint8ArrayToBase64 } = await import("@freedomofpress/crypto-browser");
28
+ const { ed25519 } = await import("@noble/curves/ed25519.js");
29
+ const { trustRoot } = await import("../test/fixtures/sigstore.js");
30
+
31
+ // The real trusted root carries an SPKI-encoded Ed25519 key for Rekor v2.
32
+ const keys = await new SigstoreVerifier().loadLog(trustRoot().tlogs);
33
+ const rekorV2 = keys.find((k) => k.publicKey.algorithm.name === "Ed25519");
34
+ expect((rekorV2?.publicKey as unknown as { bytes: Uint8Array }).bytes).toHaveLength(32);
35
+
36
+ // A synthetic log whose key we control, signing a checkpoint note.
37
+ const priv = ed25519.utils.randomSecretKey();
38
+ const pub = ed25519.getPublicKey(priv);
39
+ const spki = new Uint8Array([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ...pub]);
40
+ const logId = new Uint8Array(await crypto.subtle.digest("SHA-256", spki));
41
+ const [log] = await new SigstoreVerifier().loadLog([
42
+ {
43
+ baseUrl: "https://log.test",
44
+ hashAlgorithm: "SHA2_256",
45
+ publicKey: { rawBytes: Uint8ArrayToBase64(spki), keyDetails: "PKIX_ED25519", validFor: { start: "2025-01-01T00:00:00Z" } },
46
+ logId: { keyId: Uint8ArrayToBase64(logId) },
47
+ },
48
+ ]);
49
+
50
+ const rootHash = Uint8ArrayToBase64(new Uint8Array(32));
51
+ const note = `log.test\n7\n${rootHash}\n`;
52
+ const sig = ed25519.sign(new TextEncoder().encode(note), priv);
53
+ const sigLine = Uint8ArrayToBase64(new Uint8Array([...logId.subarray(0, 4), ...sig]));
54
+ const proof = (n: string) => ({
55
+ logIndex: "0",
56
+ hashes: [],
57
+ treeSize: "7",
58
+ rootHash,
59
+ checkpoint: { envelope: `${n}\n— log.test ${sigLine}\n` },
60
+ });
61
+ const entry = (n = note) => ({
62
+ logIndex: "0",
63
+ logId: { keyId: Uint8ArrayToBase64(logId) },
64
+ kindVersion: { kind: "dsse", version: "0.0.2" },
65
+ integratedTime: null,
66
+ canonicalizedBody: "",
67
+ inclusionProof: proof(n),
68
+ });
69
+
70
+ await expect(verifyCheckpoint(entry(), log)).resolves.toBeUndefined();
71
+ await expect(verifyCheckpoint(entry(`log.test\n8\n${rootHash}\n`), log)).rejects.toThrow("Invalid checkpoint signature");
72
+ const bad = entry();
73
+ bad.inclusionProof.treeSize = "8";
74
+ await expect(verifyCheckpoint(bad, log)).rejects.toThrow("does not match inclusion proof");
75
+ });
76
+ });
@@ -1,4 +1,6 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, expect,it } from "vitest";
2
+
3
+ import { assertBundle } from "./bundle.js";
2
4
  import { SigstoreVerifier } from "./sigstore.js";
3
5
  import { X509Certificate } from "./x509/cert.js";
4
6
 
@@ -254,3 +256,46 @@ ewS+2T7Qz4oXaQMidPOjr1Q8WKqaKO4yCtC8cz4qVWi3lNqAcAGtonQMXUiflEWV
254
256
  expect(parsed.verificationMaterial.tlogEntries[0].logIndex).toBe("123");
255
257
  });
256
258
  });
259
+
260
+ describe("Bundle structural validation", () => {
261
+ const entry = {
262
+ logIndex: "1",
263
+ logId: { keyId: "AA==" },
264
+ kindVersion: { kind: "dsse", version: "0.0.1" },
265
+ integratedTime: "1",
266
+ inclusionPromise: { signedEntryTimestamp: "AA==" },
267
+ canonicalizedBody: "e30=",
268
+ };
269
+ const bundle = () => structuredClone({
270
+ mediaType: "application/vnd.dev.sigstore.bundle.v0.3+json",
271
+ verificationMaterial: { certificate: { rawBytes: "AA==" }, tlogEntries: [entry] },
272
+ dsseEnvelope: { payload: "AA==", payloadType: "t", signatures: [{ sig: "AA==" }] },
273
+ });
274
+ const rejects = (mutate: (b: ReturnType<typeof bundle>) => void, message: string) => {
275
+ const b = bundle();
276
+ mutate(b);
277
+ expect(() => assertBundle(b)).toThrow(message);
278
+ };
279
+
280
+ it("accepts a well-formed bundle", () => {
281
+ expect(() => assertBundle(bundle())).not.toThrow();
282
+ });
283
+
284
+ it("rejects malformed fields, empty promises and missing integrated times", () => {
285
+ rejects((b) => { b.verificationMaterial.tlogEntries = []; }, "tlogEntries");
286
+ rejects((b) => { (b.verificationMaterial.tlogEntries[0] as Record<string, unknown>).logIndex = 1; }, "tlog entry");
287
+ rejects((b) => { (b.verificationMaterial.tlogEntries[0] as Record<string, unknown>).integratedTime = 1; }, "integratedTime");
288
+ rejects((b) => { b.verificationMaterial.tlogEntries[0].inclusionPromise.signedEntryTimestamp = ""; }, "inclusion promise");
289
+ rejects((b) => { (b.verificationMaterial.tlogEntries[0] as Record<string, unknown>).integratedTime = null; }, "inclusion promise");
290
+ });
291
+
292
+ it("caps entries at 32", () => {
293
+ rejects((b) => { b.verificationMaterial.tlogEntries = Array(33).fill(entry); }, "tlogEntries");
294
+ });
295
+
296
+ it("requires exactly one signature source with exactly one signature", () => {
297
+ rejects((b) => { (b as Record<string, unknown>).messageSignature = { messageDigest: { algorithm: "SHA2_256", digest: "AA==" }, signature: "AA==" }; }, "exactly one of");
298
+ rejects((b) => { delete (b as Partial<ReturnType<typeof bundle>>).dsseEnvelope; }, "exactly one of");
299
+ rejects((b) => { b.dsseEnvelope.signatures.push({ sig: "AA==" }); }, "exactly one signature");
300
+ });
301
+ });
package/src/interfaces.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  import { X509Certificate } from "./x509/index.js";
2
2
 
3
+ export function parseValidityPeriod(
4
+ validity: { start?: unknown; end?: unknown } | undefined,
5
+ ): { start: Date; end: Date } {
6
+ if (
7
+ typeof validity?.start !== "string" ||
8
+ (validity.end !== undefined && typeof validity.end !== "string")
9
+ ) {
10
+ throw new Error("Invalid authority validity period");
11
+ }
12
+ const start = new Date(validity.start);
13
+ const end = validity.end === undefined
14
+ ? new Date(8640000000000000)
15
+ : new Date(validity.end);
16
+ if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start > end) {
17
+ throw new Error("Invalid authority validity period");
18
+ }
19
+ return { start, end };
20
+ }
21
+
3
22
  export enum SigstoreRoots {
4
23
  certificateAuthorities = "certificateAuthorities",
5
24
  ctlogs = "ctlogs",
@@ -31,11 +50,13 @@ export interface RawTimestampAuthority {
31
50
  start: string;
32
51
  end?: string;
33
52
  };
53
+ operator?: string;
34
54
  }
35
55
 
36
56
  export interface CTLog {
37
57
  logID: Uint8Array;
38
58
  publicKey: CryptoKey;
59
+ operator: string;
39
60
  validFor: {
40
61
  start: Date;
41
62
  end: Date;
@@ -45,6 +66,12 @@ export interface CTLog {
45
66
  export interface RekorKeyInfo {
46
67
  publicKey: CryptoKey;
47
68
  logId: Uint8Array;
69
+ hashAlgorithm: string;
70
+ operator: string;
71
+ validFor: {
72
+ start: Date;
73
+ end: Date;
74
+ };
48
75
  }
49
76
 
50
77
  export interface CertAuthority {
@@ -56,10 +83,9 @@ export interface CertAuthority {
56
83
  }
57
84
 
58
85
  export interface Sigstore {
59
- rekor: RekorKeyInfo | undefined;
86
+ rekor: RekorKeyInfo[];
60
87
  ctlogs: CTLog[];
61
88
  certificateAuthorities: CertAuthority[];
62
- timestampAuthorities: CertAuthority[];
63
89
  }
64
90
 
65
91
  export interface RawLog {
@@ -76,6 +102,7 @@ export interface RawLog {
76
102
  logId: {
77
103
  keyId: string;
78
104
  };
105
+ operator?: string;
79
106
  }
80
107
 
81
108
  export type RawLogs = RawLog[];
package/src/policy.ts CHANGED
@@ -49,7 +49,7 @@ export const GITHUB_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
49
49
  * Interface that all verification policies conform to.
50
50
  */
51
51
  export interface VerificationPolicy {
52
- verify(cert: X509Certificate): void;
52
+ verify(cert: X509Certificate): void | Promise<void>;
53
53
  }
54
54
 
55
55
  /**
@@ -384,10 +384,10 @@ export class AnyOf implements VerificationPolicy {
384
384
  this.children = children;
385
385
  }
386
386
 
387
- verify(cert: X509Certificate): void {
387
+ async verify(cert: X509Certificate): Promise<void> {
388
388
  for (const child of this.children) {
389
389
  try {
390
- child.verify(cert);
390
+ await child.verify(cert);
391
391
  return;
392
392
  } catch {
393
393
  // Continue to next policy
@@ -408,12 +408,12 @@ export class AllOf implements VerificationPolicy {
408
408
  this.children = children;
409
409
  }
410
410
 
411
- verify(cert: X509Certificate): void {
411
+ async verify(cert: X509Certificate): Promise<void> {
412
412
  if (this.children.length < 1) {
413
413
  throw new PolicyError("no child policies to verify");
414
414
  }
415
415
  for (const child of this.children) {
416
- child.verify(cert);
416
+ await child.verify(cert);
417
417
  }
418
418
  }
419
419
  }