@freedomofpress/sigstore-browser 0.1.14 → 0.1.16

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 (132) hide show
  1. package/README.md +20 -16
  2. package/dist/bundle.d.ts +3 -2
  3. package/dist/bundle.js +66 -1
  4. package/dist/bundle.js.map +1 -1
  5. package/dist/cli.d.ts +0 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dsse.d.ts +0 -1
  8. package/dist/dsse.js.map +1 -1
  9. package/dist/errors.d.ts +0 -1
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +9 -10
  12. package/dist/index.js +5 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/interfaces.d.ts +18 -4
  15. package/dist/interfaces.js +15 -1
  16. package/dist/interfaces.js.map +1 -1
  17. package/dist/oid.d.ts +0 -1
  18. package/dist/oid.js.map +1 -1
  19. package/dist/policy.d.ts +3 -4
  20. package/dist/policy.js +5 -5
  21. package/dist/policy.js.map +1 -1
  22. package/dist/rfc3161/error.d.ts +0 -1
  23. package/dist/rfc3161/error.js.map +1 -1
  24. package/dist/rfc3161/index.d.ts +0 -1
  25. package/dist/rfc3161/index.js.map +1 -1
  26. package/dist/rfc3161/timestamp.d.ts +0 -1
  27. package/dist/rfc3161/timestamp.js.map +1 -1
  28. package/dist/rfc3161/tstinfo.d.ts +0 -1
  29. package/dist/rfc3161/tstinfo.js.map +1 -1
  30. package/dist/sigstore.d.ts +10 -20
  31. package/dist/sigstore.js +167 -400
  32. package/dist/sigstore.js.map +1 -1
  33. package/dist/timestamp/tsa.d.ts +6 -3
  34. package/dist/timestamp/tsa.js +16 -19
  35. package/dist/timestamp/tsa.js.map +1 -1
  36. package/dist/tlog/body.d.ts +3 -2
  37. package/dist/tlog/body.js +19 -6
  38. package/dist/tlog/body.js.map +1 -1
  39. package/dist/tlog/checkpoint.d.ts +2 -3
  40. package/dist/tlog/checkpoint.js +17 -95
  41. package/dist/tlog/checkpoint.js.map +1 -1
  42. package/dist/tlog/dsse.d.ts +3 -3
  43. package/dist/tlog/dsse.js +11 -5
  44. package/dist/tlog/dsse.js.map +1 -1
  45. package/dist/tlog/hashedrekord.d.ts +3 -3
  46. package/dist/tlog/hashedrekord.js +8 -5
  47. package/dist/tlog/hashedrekord.js.map +1 -1
  48. package/dist/tlog/intoto.d.ts +3 -3
  49. package/dist/tlog/intoto.js +3 -1
  50. package/dist/tlog/intoto.js.map +1 -1
  51. package/dist/tlog/merkle.d.ts +0 -1
  52. package/dist/tlog/merkle.js.map +1 -1
  53. package/dist/trust/tuf-root.d.ts +0 -1
  54. package/dist/trust/tuf-root.js.map +1 -1
  55. package/dist/trust/tuf.d.ts +0 -1
  56. package/dist/trust/tuf.js.map +1 -1
  57. package/dist/x509/cert.d.ts +0 -1
  58. package/dist/x509/cert.js +1 -1
  59. package/dist/x509/cert.js.map +1 -1
  60. package/dist/x509/chain.d.ts +0 -1
  61. package/dist/x509/chain.js +2 -2
  62. package/dist/x509/chain.js.map +1 -1
  63. package/dist/x509/ext.d.ts +0 -1
  64. package/dist/x509/ext.js.map +1 -1
  65. package/dist/x509/index.d.ts +1 -2
  66. package/dist/x509/index.js +1 -1
  67. package/dist/x509/index.js.map +1 -1
  68. package/dist/x509/sct.d.ts +0 -1
  69. package/dist/x509/sct.js.map +1 -1
  70. package/package.json +15 -8
  71. package/.github/workflows/conformance.yml +0 -29
  72. package/.github/workflows/publish.yml +0 -47
  73. package/dist/bundle.d.ts.map +0 -1
  74. package/dist/cli.d.ts.map +0 -1
  75. package/dist/dsse.d.ts.map +0 -1
  76. package/dist/errors.d.ts.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/interfaces.d.ts.map +0 -1
  79. package/dist/oid.d.ts.map +0 -1
  80. package/dist/policy.d.ts.map +0 -1
  81. package/dist/rfc3161/error.d.ts.map +0 -1
  82. package/dist/rfc3161/index.d.ts.map +0 -1
  83. package/dist/rfc3161/timestamp.d.ts.map +0 -1
  84. package/dist/rfc3161/tstinfo.d.ts.map +0 -1
  85. package/dist/sigstore.d.ts.map +0 -1
  86. package/dist/timestamp/tsa.d.ts.map +0 -1
  87. package/dist/tlog/body.d.ts.map +0 -1
  88. package/dist/tlog/checkpoint.d.ts.map +0 -1
  89. package/dist/tlog/dsse.d.ts.map +0 -1
  90. package/dist/tlog/hashedrekord.d.ts.map +0 -1
  91. package/dist/tlog/intoto.d.ts.map +0 -1
  92. package/dist/tlog/merkle.d.ts.map +0 -1
  93. package/dist/trust/tuf-root.d.ts.map +0 -1
  94. package/dist/trust/tuf.d.ts.map +0 -1
  95. package/dist/x509/cert.d.ts.map +0 -1
  96. package/dist/x509/chain.d.ts.map +0 -1
  97. package/dist/x509/ext.d.ts.map +0 -1
  98. package/dist/x509/index.d.ts.map +0 -1
  99. package/dist/x509/sct.d.ts.map +0 -1
  100. package/eslint.config.mjs +0 -31
  101. package/src/bundle.ts +0 -99
  102. package/src/cli.ts +0 -290
  103. package/src/dsse.ts +0 -49
  104. package/src/errors.ts +0 -82
  105. package/src/index.browser.test.ts +0 -290
  106. package/src/index.ts +0 -93
  107. package/src/interfaces.ts +0 -123
  108. package/src/oid.ts +0 -34
  109. package/src/policy.ts +0 -476
  110. package/src/rfc3161/error.ts +0 -16
  111. package/src/rfc3161/index.ts +0 -17
  112. package/src/rfc3161/timestamp.ts +0 -268
  113. package/src/rfc3161/tstinfo.ts +0 -78
  114. package/src/sigstore.ts +0 -948
  115. package/src/timestamp/tsa.ts +0 -211
  116. package/src/tlog/body.ts +0 -65
  117. package/src/tlog/checkpoint.ts +0 -254
  118. package/src/tlog/dsse.ts +0 -147
  119. package/src/tlog/hashedrekord.ts +0 -130
  120. package/src/tlog/intoto.ts +0 -106
  121. package/src/tlog/merkle.ts +0 -152
  122. package/src/trust/README.md +0 -38
  123. package/src/trust/tuf-root.ts +0 -3
  124. package/src/trust/tuf.ts +0 -233
  125. package/src/x509/cert.ts +0 -569
  126. package/src/x509/chain.ts +0 -212
  127. package/src/x509/ext.ts +0 -356
  128. package/src/x509/index.ts +0 -19
  129. package/src/x509/sct.ts +0 -156
  130. package/tsconfig.json +0 -18
  131. package/vite.config.playwright.ts +0 -14
  132. package/vite.config.ts +0 -18
package/src/cli.ts DELETED
@@ -1,290 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createHash, webcrypto } from "node:crypto";
3
- import { access, readFile } from "node:fs/promises";
4
- import path from "node:path";
5
-
6
- import {
7
- base64ToUint8Array,
8
- hexToUint8Array,
9
- Uint8ArrayToHex,
10
- Uint8ArrayToString,
11
- } from "@freedomofpress/crypto-browser";
12
- import { SigstoreBundle } from "./bundle.js";
13
- import { TrustedRoot } from "./interfaces.js";
14
- import { SigstoreVerifier } from "./sigstore.js";
15
- import { TrustedRootProvider } from "./trust/tuf.js";
16
-
17
- // Ensure the global Web Crypto implementation is available when running under Node.js
18
- if (typeof globalThis.crypto === "undefined") {
19
- globalThis.crypto = webcrypto as unknown as Crypto;
20
- }
21
-
22
- interface CLIOptions {
23
- bundlePath: string;
24
- certificateIdentity: string;
25
- certificateOidcIssuer: string;
26
- trustedRootPath: string | undefined;
27
- artifactInput: string;
28
- }
29
-
30
- type ArtifactInput =
31
- | { type: "file"; path: string; data: Uint8Array; digestHex: string }
32
- | { type: "digest"; digestHex: string };
33
-
34
- const USAGE = `Usage: sigstore-browser verify-bundle [--staging] --bundle FILE --certificate-identity IDENTITY --certificate-oidc-issuer URL [--trusted-root FILE] FILE_OR_DIGEST`;
35
-
36
- function printUsage(): void {
37
- console.error(USAGE);
38
- }
39
-
40
- function parseArgs(argv: string[]): CLIOptions {
41
- const options: Partial<CLIOptions> = {};
42
- let command: string | undefined;
43
- let artifactInput: string | undefined;
44
-
45
- for (let i = 0; i < argv.length; i++) {
46
- const current = argv[i];
47
-
48
- if (current === "--help" || current === "-h") {
49
- printUsage();
50
- process.exit(0);
51
- }
52
-
53
- if (current.startsWith("--")) {
54
- const [name, providedValue] = current.split("=", 2);
55
-
56
- switch (name) {
57
- case "--bundle": {
58
- const value = providedValue ?? argv[++i];
59
- if (!value) {
60
- throw new Error("Missing value for --bundle");
61
- }
62
- options.bundlePath = value;
63
- break;
64
- }
65
- case "--certificate-identity": {
66
- const value = providedValue ?? argv[++i];
67
- if (!value) {
68
- throw new Error("Missing value for --certificate-identity");
69
- }
70
- options.certificateIdentity = value;
71
- break;
72
- }
73
- case "--certificate-oidc-issuer": {
74
- const value = providedValue ?? argv[++i];
75
- if (!value) {
76
- throw new Error("Missing value for --certificate-oidc-issuer");
77
- }
78
- options.certificateOidcIssuer = value;
79
- break;
80
- }
81
- case "--trusted-root": {
82
- const value = providedValue ?? argv[++i];
83
- if (!value) {
84
- throw new Error("Missing value for --trusted-root");
85
- }
86
- options.trustedRootPath = value;
87
- break;
88
- }
89
- default:
90
- throw new Error(`Unknown option: ${name}`);
91
- }
92
-
93
- continue;
94
- }
95
-
96
- if (!command) {
97
- command = current;
98
- continue;
99
- }
100
-
101
- if (!artifactInput) {
102
- artifactInput = current;
103
- continue;
104
- }
105
-
106
- throw new Error(`Unexpected argument: ${current}`);
107
- }
108
-
109
- if (command !== "verify-bundle") {
110
- throw new Error("No command provided. Expected 'verify-bundle'.");
111
- }
112
-
113
- if (!artifactInput) {
114
- throw new Error("Missing FILE_OR_DIGEST argument.");
115
- }
116
-
117
- if (!options.bundlePath) {
118
- throw new Error("Missing required option --bundle");
119
- }
120
-
121
- if (!options.certificateIdentity) {
122
- throw new Error("Missing required option --certificate-identity");
123
- }
124
-
125
- if (!options.certificateOidcIssuer) {
126
- throw new Error("Missing required option --certificate-oidc-issuer");
127
- }
128
-
129
- return {
130
- bundlePath: options.bundlePath,
131
- certificateIdentity: options.certificateIdentity,
132
- certificateOidcIssuer: options.certificateOidcIssuer,
133
- trustedRootPath: options.trustedRootPath,
134
- artifactInput,
135
- };
136
- }
137
-
138
- async function fileExists(filePath: string): Promise<boolean> {
139
- try {
140
- await access(filePath);
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
-
147
- async function resolveArtifact(input: string): Promise<ArtifactInput> {
148
- const digestPrefix = "sha256:";
149
-
150
- if (
151
- input.startsWith(digestPrefix) &&
152
- input.length === digestPrefix.length + 64 &&
153
- /^[0-9a-fA-F]+$/.test(input.slice(digestPrefix.length)) &&
154
- !(await fileExists(input))
155
- ) {
156
- return {
157
- type: "digest",
158
- digestHex: input.slice(digestPrefix.length).toLowerCase(),
159
- };
160
- }
161
-
162
- const resolvedPath = path.resolve(input);
163
- const artifactBuffer = await readFile(resolvedPath);
164
- const digestHex = createHash("sha256").update(artifactBuffer).digest("hex");
165
-
166
- return {
167
- type: "file",
168
- path: resolvedPath,
169
- data: new Uint8Array(artifactBuffer),
170
- digestHex,
171
- };
172
- }
173
-
174
- async function loadTrustedRoot(
175
- pathInput: string | undefined,
176
- ): Promise<TrustedRoot> {
177
- if (!pathInput) {
178
- // Use TUF to fetch the latest trusted root (matches sigstore-js behavior)
179
- const tufProvider = new TrustedRootProvider({
180
- metadataUrl: "https://tuf-repo-cdn.sigstore.dev/",
181
- targetBaseUrl: "https://tuf-repo-cdn.sigstore.dev/targets/",
182
- });
183
- return await tufProvider.getTrustedRoot();
184
- }
185
- const resolvedPath = path.resolve(pathInput);
186
- const raw = await readFile(resolvedPath, "utf8");
187
- return JSON.parse(raw) as TrustedRoot;
188
- }
189
-
190
- async function loadBundle(bundlePath: string): Promise<SigstoreBundle> {
191
- const resolved = path.resolve(bundlePath);
192
- const raw = await readFile(resolved, "utf8");
193
- return JSON.parse(raw) as SigstoreBundle;
194
- }
195
-
196
- async function verifyBundle(options: CLIOptions): Promise<void> {
197
- const [trustedRoot, bundle, artifact] = await Promise.all([
198
- loadTrustedRoot(options.trustedRootPath),
199
- loadBundle(options.bundlePath),
200
- resolveArtifact(options.artifactInput),
201
- ]);
202
-
203
- const verifier = new SigstoreVerifier();
204
- await verifier.loadSigstoreRoot(trustedRoot);
205
-
206
- // Extract and verify the artifact digest from the bundle
207
- let bundleDigestHex: string;
208
- let bundleDigestBytes: Uint8Array;
209
-
210
- if (bundle.messageSignature) {
211
- // Regular bundle: digest is in messageSignature
212
- if (bundle.messageSignature.messageDigest.algorithm !== "SHA2_256") {
213
- throw new Error(
214
- `Unsupported message digest algorithm: ${bundle.messageSignature.messageDigest.algorithm}`,
215
- );
216
- }
217
-
218
- bundleDigestBytes = base64ToUint8Array(
219
- bundle.messageSignature.messageDigest.digest,
220
- );
221
- bundleDigestHex = Uint8ArrayToHex(bundleDigestBytes).toLowerCase();
222
- } else if (bundle.dsseEnvelope) {
223
- // DSSE bundle: digest is in the in-toto statement payload
224
- const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
225
- const payload = JSON.parse(Uint8ArrayToString(payloadBytes));
226
-
227
- if (!payload.subject || payload.subject.length === 0) {
228
- throw new Error("DSSE payload has no subject");
229
- }
230
-
231
- // Find matching subject by comparing artifact digest with all subjects
232
- let matchedSubject = null;
233
- for (const subject of payload.subject) {
234
- const subjectDigest = subject.digest?.["sha256"];
235
- if (subjectDigest && artifact.digestHex === subjectDigest.toLowerCase()) {
236
- matchedSubject = subject;
237
- break;
238
- }
239
- }
240
-
241
- if (!matchedSubject) {
242
- throw new Error(
243
- `Artifact digest ${artifact.digestHex} does not match any subject in DSSE payload`
244
- );
245
- }
246
-
247
- bundleDigestHex = matchedSubject.digest["sha256"].toLowerCase();
248
-
249
- // Convert hex digest to bytes for verification target
250
- bundleDigestBytes = hexToUint8Array(bundleDigestHex);
251
- } else {
252
- throw new Error("Bundle does not contain a message signature or DSSE envelope");
253
- }
254
-
255
- // For non-DSSE bundles, verify the digest matches
256
- if (bundle.messageSignature && artifact.digestHex !== bundleDigestHex) {
257
- throw new Error(
258
- "Artifact digest does not match the digest embedded in the bundle.",
259
- );
260
- }
261
-
262
- const verificationTarget =
263
- artifact.type === "file" ? artifact.data : bundleDigestBytes;
264
- const isDigestOnly = artifact.type === "digest";
265
-
266
- await verifier.verifyArtifact(
267
- options.certificateIdentity,
268
- options.certificateOidcIssuer,
269
- bundle,
270
- verificationTarget,
271
- isDigestOnly,
272
- );
273
- }
274
-
275
- async function main(): Promise<void> {
276
- try {
277
- const options = parseArgs(process.argv.slice(2));
278
- await verifyBundle(options);
279
- console.log("Bundle verification succeeded.");
280
- } catch (error) {
281
- if (error instanceof Error) {
282
- console.error(error.message);
283
- } else {
284
- console.error(error);
285
- }
286
- process.exitCode = 1;
287
- }
288
- }
289
-
290
- await main();
package/src/dsse.ts DELETED
@@ -1,49 +0,0 @@
1
- /*
2
- Copyright 2023 The Sigstore Authors.
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
- const PAE_PREFIX = "DSSEv1";
17
-
18
- // DSSE Pre-Authentication Encoding
19
- export function preAuthEncoding(
20
- payloadType: string,
21
- payload: Uint8Array,
22
- ): Uint8Array {
23
- const prefix = [
24
- PAE_PREFIX,
25
- payloadType.length,
26
- payloadType,
27
- payload.length,
28
- "",
29
- ].join(" ");
30
-
31
- // DSSE spec requires ASCII encoding for the prefix
32
- // TextEncoder will throw if the string contains non-ASCII characters
33
- const encoder = new TextEncoder();
34
- const prefixBuffer = encoder.encode(prefix);
35
-
36
- // Verify all characters are ASCII (< 128)
37
- for (let i = 0; i < prefixBuffer.length; i++) {
38
- if (prefixBuffer[i] > 127) {
39
- throw new Error(`Invalid non-ASCII character in PAE prefix at position ${i}`);
40
- }
41
- }
42
-
43
- // Concatenate prefix and payload
44
- const combinedArray = new Uint8Array(prefixBuffer.length + payload.length);
45
-
46
- combinedArray.set(prefixBuffer, 0);
47
- combinedArray.set(payload, prefixBuffer.length);
48
- return combinedArray;
49
- }
package/src/errors.ts DELETED
@@ -1,82 +0,0 @@
1
- /*
2
- Copyright 2023 The Sigstore Authors.
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- /**
18
- * Base class for all verification errors
19
- * Provides structured error handling with error codes
20
- */
21
- export class VerificationError extends Error {
22
- constructor(public code: string, message: string) {
23
- super(message);
24
- this.name = 'VerificationError';
25
- Object.setPrototypeOf(this, VerificationError.prototype);
26
- }
27
- }
28
-
29
- /**
30
- * Error thrown when timestamp verification fails
31
- */
32
- export class TimestampError extends VerificationError {
33
- constructor(message: string) {
34
- super('TIMESTAMP_ERROR', message);
35
- this.name = 'TimestampError';
36
- Object.setPrototypeOf(this, TimestampError.prototype);
37
- }
38
- }
39
-
40
- /**
41
- * Error thrown when certificate verification fails
42
- */
43
- export class CertificateError extends VerificationError {
44
- constructor(message: string) {
45
- super('CERTIFICATE_ERROR', message);
46
- this.name = 'CertificateError';
47
- Object.setPrototypeOf(this, CertificateError.prototype);
48
- }
49
- }
50
-
51
- /**
52
- * Error thrown when transparency log verification fails
53
- */
54
- export class TLogError extends VerificationError {
55
- constructor(message: string) {
56
- super('TLOG_ERROR', message);
57
- this.name = 'TLogError';
58
- Object.setPrototypeOf(this, TLogError.prototype);
59
- }
60
- }
61
-
62
- /**
63
- * Error thrown when signature verification fails
64
- */
65
- export class SignatureError extends VerificationError {
66
- constructor(message: string) {
67
- super('SIGNATURE_ERROR', message);
68
- this.name = 'SignatureError';
69
- Object.setPrototypeOf(this, SignatureError.prototype);
70
- }
71
- }
72
-
73
- /**
74
- * Error thrown when policy verification fails
75
- */
76
- export class PolicyError extends VerificationError {
77
- constructor(message: string) {
78
- super('POLICY_ERROR', message);
79
- this.name = 'PolicyError';
80
- Object.setPrototypeOf(this, PolicyError.prototype);
81
- }
82
- }
@@ -1,290 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { SigstoreVerifier, assertRekorV2Timestamp } from "./sigstore.js";
3
- import { X509Certificate } from "./x509/cert.js";
4
-
5
- describe("Sigstore Browser Integration Tests", () => {
6
- it("should initialize SigstoreVerifier in browser", () => {
7
- const verifier = new SigstoreVerifier();
8
- expect(verifier).toBeDefined();
9
- expect(verifier).toBeInstanceOf(SigstoreVerifier);
10
- });
11
-
12
- it("should have crypto.subtle available for ECDSA P-256 verification", async () => {
13
- expect(globalThis.crypto).toBeDefined();
14
- expect(globalThis.crypto.subtle).toBeDefined();
15
-
16
- // Test ECDSA P-256 which is what Sigstore uses
17
- const keyPair = await crypto.subtle.generateKey(
18
- {
19
- name: "ECDSA",
20
- namedCurve: "P-256",
21
- },
22
- true,
23
- ["sign", "verify"]
24
- );
25
-
26
- const data = new TextEncoder().encode("test");
27
- const signature = await crypto.subtle.sign(
28
- { name: "ECDSA", hash: "SHA-256" },
29
- keyPair.privateKey,
30
- data
31
- );
32
-
33
- const valid = await crypto.subtle.verify(
34
- { name: "ECDSA", hash: "SHA-256" },
35
- keyPair.publicKey,
36
- signature,
37
- data
38
- );
39
-
40
- expect(valid).toBe(true);
41
- });
42
-
43
- it("should have crypto.subtle available for ECDSA P-384 verification", async () => {
44
- // Test ECDSA P-384 which is also used by Sigstore
45
- const keyPair = await crypto.subtle.generateKey(
46
- {
47
- name: "ECDSA",
48
- namedCurve: "P-384",
49
- },
50
- true,
51
- ["sign", "verify"]
52
- );
53
-
54
- const data = new TextEncoder().encode("test");
55
- const signature = await crypto.subtle.sign(
56
- { name: "ECDSA", hash: "SHA-384" },
57
- keyPair.privateKey,
58
- data
59
- );
60
-
61
- const valid = await crypto.subtle.verify(
62
- { name: "ECDSA", hash: "SHA-384" },
63
- keyPair.publicKey,
64
- signature,
65
- data
66
- );
67
-
68
- expect(valid).toBe(true);
69
- });
70
-
71
- it("should support SHA-256 hashing in browser", async () => {
72
- const data = new TextEncoder().encode("test data");
73
- const hash = await crypto.subtle.digest("SHA-256", data);
74
-
75
- expect(hash).toBeInstanceOf(ArrayBuffer);
76
- expect(hash.byteLength).toBe(32);
77
- });
78
-
79
- it("should support SHA-384 hashing in browser", async () => {
80
- const data = new TextEncoder().encode("test data");
81
- const hash = await crypto.subtle.digest("SHA-384", data);
82
-
83
- expect(hash).toBeInstanceOf(ArrayBuffer);
84
- expect(hash.byteLength).toBe(48);
85
- });
86
-
87
- it("should support SHA-512 hashing in browser", async () => {
88
- const data = new TextEncoder().encode("test data");
89
- const hash = await crypto.subtle.digest("SHA-512", data);
90
-
91
- expect(hash).toBeInstanceOf(ArrayBuffer);
92
- expect(hash.byteLength).toBe(64);
93
- });
94
-
95
- it("should support X.509 certificate key import (SPKI format)", async () => {
96
- const keyPair = await crypto.subtle.generateKey(
97
- {
98
- name: "ECDSA",
99
- namedCurve: "P-256",
100
- },
101
- true,
102
- ["sign", "verify"]
103
- );
104
-
105
- const exported = await crypto.subtle.exportKey("spki", keyPair.publicKey);
106
- const imported = await crypto.subtle.importKey(
107
- "spki",
108
- exported,
109
- { name: "ECDSA", namedCurve: "P-256" },
110
- true,
111
- ["verify"]
112
- );
113
-
114
- expect(imported).toBeDefined();
115
- expect(imported.type).toBe("public");
116
- });
117
-
118
- it("should support RSA-PSS for signature verification", async () => {
119
- const keyPair = await crypto.subtle.generateKey(
120
- {
121
- name: "RSA-PSS",
122
- modulusLength: 2048,
123
- publicExponent: new Uint8Array([1, 0, 1]),
124
- hash: "SHA-256",
125
- },
126
- true,
127
- ["sign", "verify"]
128
- );
129
-
130
- const data = new TextEncoder().encode("test");
131
- const signature = await crypto.subtle.sign(
132
- {
133
- name: "RSA-PSS",
134
- saltLength: 32,
135
- },
136
- keyPair.privateKey,
137
- data
138
- );
139
-
140
- const valid = await crypto.subtle.verify(
141
- {
142
- name: "RSA-PSS",
143
- saltLength: 32,
144
- },
145
- keyPair.publicKey,
146
- signature,
147
- data
148
- );
149
-
150
- expect(valid).toBe(true);
151
- });
152
-
153
- it("should verify RSA-PSS signed X.509 certificate", async () => {
154
- // Self-signed certificate with standard RSA key but RSA-PSS signature (SHA-256, salt=32)
155
- // WebCrypto requires rsaEncryption OID for the public key, not id-RSASSA-PSS
156
- const rsaPssCertPem = `-----BEGIN CERTIFICATE-----
157
- MIIDhTCCAjmgAwIBAgIUYzfguLRx71Cbn2Ty7xCt3GH+N6AwQQYJKoZIhvcNAQEK
158
- MDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF
159
- AKIDAgEgMB4xHDAaBgNVBAMME1Rlc3QgUlNBLVBTUyBTaWduZWQwHhcNMjUxMTMw
160
- MDMwMDU2WhcNMjYxMTMwMDMwMDU2WjAeMRwwGgYDVQQDDBNUZXN0IFJTQS1QU1Mg
161
- U2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA29B/88QFBILg
162
- YCWp1pF3xBj7FVhBUCv6JwyZAouYWUxgGmpFg6RLgbaWk59oDtEFQ5xRGSDXLuMW
163
- GL5wZVCjSnmYcBsBDROO+HfLKwDRcHw09bJY979PDHPy5EPZale/JsdxE+fepUw1
164
- XQDvlK5Dyp6WKl9UBjTtl6+LHKV/KTo+SiaQP2vsbZvTIt3s8MHybV/ixYXlSPQt
165
- Kju50REAaqSSYqxNzsq8Z2sjJbFBcatt5C1JhfoSyIHz5D0slriv8835DYhk0u4n
166
- bvNzXhWeF7htHTiPWCYwaTr5cgjyhEWg3SNP+Tu9dxCYVXm7/jWTHRdPqUPQqFsq
167
- wXqsrzSP+wIDAQABo1MwUTAdBgNVHQ4EFgQU+UcBxKusOtY4lSjqyFuxQfN5zdIw
168
- HwYDVR0jBBgwFoAU+UcBxKusOtY4lSjqyFuxQfN5zdIwDwYDVR0TAQH/BAUwAwEB
169
- /zBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEI
170
- MA0GCWCGSAFlAwQCAQUAogMCASADggEBAFIn3VpfydVvpI82NwfeGxWlhZhgD2dX
171
- gJUhj6pcXqwnlCzpyRI4e3uNO1h9nr9UrogI0AV2IDhVdUYxz3F2/680JtxXpB1x
172
- J02BSAYllvXgf9efc20lFzbPDzvMzKnwx3S3proewbJ4uVPHzvKubWsx8MwEuI0X
173
- 8xkI1qMIXgevTCt+LGhp+0C/u8WsW3or5ZI7GnZNowPJEAtZnfO+VORB7FvbzEGr
174
- ewS+2T7Qz4oXaQMidPOjr1Q8WKqaKO4yCtC8cz4qVWi3lNqAcAGtonQMXUiflEWV
175
- 9kqKQx+LJYJqxKkBtDRnRLnVNn4+inotrgnV126LsCe3uLaGwJIFy1U=
176
- -----END CERTIFICATE-----`;
177
-
178
- const cert = X509Certificate.parse(rsaPssCertPem);
179
-
180
- // Verify it's an RSA-PSS certificate
181
- expect(cert.signatureAlgorithmOid).toBe("1.2.840.113549.1.1.10");
182
- expect(cert.signatureAlgorithm).toBe("sha256");
183
-
184
- // Verify the self-signed certificate (signature verification)
185
- const valid = await cert.verify();
186
- expect(valid).toBe(true);
187
- });
188
-
189
- it("should support TextEncoder/TextDecoder for UTF-8", () => {
190
- const encoder = new TextEncoder();
191
- const decoder = new TextDecoder();
192
-
193
- const text = "Hello Sigstore 🔐";
194
- const encoded = encoder.encode(text);
195
- const decoded = decoder.decode(encoded);
196
-
197
- expect(decoded).toBe(text);
198
- expect(encoded).toBeInstanceOf(Uint8Array);
199
- });
200
-
201
- it("should support base64 encoding/decoding (atob/btoa)", () => {
202
- const text = "Hello Sigstore";
203
- const base64 = btoa(text);
204
- const decoded = atob(base64);
205
-
206
- expect(decoded).toBe(text);
207
- expect(base64).toBe("SGVsbG8gU2lnc3RvcmU=");
208
- });
209
-
210
- it("should support Uint8Array operations", () => {
211
- const arr1 = new Uint8Array([1, 2, 3, 4, 5]);
212
- const arr2 = new Uint8Array([6, 7, 8, 9, 10]);
213
-
214
- const combined = new Uint8Array(arr1.length + arr2.length);
215
- combined.set(arr1, 0);
216
- combined.set(arr2, arr1.length);
217
-
218
- expect(combined.length).toBe(10);
219
- expect(combined[0]).toBe(1);
220
- expect(combined[5]).toBe(6);
221
- });
222
-
223
- it("should support ArrayBuffer operations", () => {
224
- const buffer = new ArrayBuffer(32);
225
- const view = new Uint8Array(buffer);
226
-
227
- view[0] = 255;
228
- view[31] = 128;
229
-
230
- expect(buffer.byteLength).toBe(32);
231
- expect(view[0]).toBe(255);
232
- expect(view[31]).toBe(128);
233
- });
234
-
235
- it("should support DataView for byte manipulation", () => {
236
- const buffer = new ArrayBuffer(8);
237
- const view = new DataView(buffer);
238
-
239
- view.setUint32(0, 0x12345678, false); // big-endian
240
- view.setUint32(4, 0x9abcdef0, false);
241
-
242
- expect(view.getUint32(0, false)).toBe(0x12345678);
243
- expect(view.getUint8(0)).toBe(0x12);
244
- expect(view.getUint8(1)).toBe(0x34);
245
- });
246
-
247
- it("should support JSON parsing of complex structures", () => {
248
- const json = '{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.3","verificationMaterial":{"tlogEntries":[{"logIndex":"123"}]}}';
249
- const parsed = JSON.parse(json);
250
-
251
- expect(parsed.mediaType).toBeDefined();
252
- expect(parsed.verificationMaterial).toBeDefined();
253
- expect(parsed.verificationMaterial.tlogEntries).toBeInstanceOf(Array);
254
- expect(parsed.verificationMaterial.tlogEntries[0].logIndex).toBe("123");
255
- });
256
- });
257
-
258
- describe("Rekor v2 observer-timestamp enforcement", () => {
259
- // Regression test: a Rekor-v2-shaped bundle (no integratedTime in the tlog
260
- // entry) must carry a signed RFC3161 timestamp. A previous guard only checked
261
- // that `timestampVerificationData` was present, which an empty object ({})
262
- // satisfies in JavaScript — letting a crafted bundle pass verification with
263
- // zero verified timestamps and the signing certificate's validity window left
264
- // completely unanchored in time.
265
-
266
- it("rejects a missing timestampVerificationData", () => {
267
- expect(() => assertRekorV2Timestamp(undefined)).toThrow(
268
- "Rekor v2 bundles require a timestamp for verification.",
269
- );
270
- });
271
-
272
- it("rejects an empty timestampVerificationData object ({})", () => {
273
- // This is the exact bypass the fix closes: {} is truthy.
274
- expect(() => assertRekorV2Timestamp({})).toThrow(
275
- "Rekor v2 bundles require a timestamp for verification.",
276
- );
277
- });
278
-
279
- it("rejects timestampVerificationData with an empty rfc3161Timestamps array", () => {
280
- expect(() =>
281
- assertRekorV2Timestamp({ rfc3161Timestamps: [] }),
282
- ).toThrow("Rekor v2 bundles require a timestamp for verification.");
283
- });
284
-
285
- it("accepts timestampVerificationData with at least one RFC3161 timestamp", () => {
286
- expect(() =>
287
- assertRekorV2Timestamp({ rfc3161Timestamps: [{ signedTimestamp: "…" }] }),
288
- ).not.toThrow();
289
- });
290
- });