@freedomofpress/sigstore-browser 0.1.15 → 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/dist/bundle.d.ts +2 -3
  2. package/dist/bundle.js.map +1 -1
  3. package/dist/cli.d.ts +0 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/dsse.d.ts +0 -1
  6. package/dist/dsse.js.map +1 -1
  7. package/dist/errors.d.ts +0 -1
  8. package/dist/errors.js.map +1 -1
  9. package/dist/index.d.ts +9 -10
  10. package/dist/index.js +5 -5
  11. package/dist/index.js.map +1 -1
  12. package/dist/interfaces.d.ts +1 -2
  13. package/dist/interfaces.js +1 -1
  14. package/dist/interfaces.js.map +1 -1
  15. package/dist/oid.d.ts +0 -1
  16. package/dist/oid.js.map +1 -1
  17. package/dist/policy.d.ts +0 -1
  18. package/dist/policy.js +1 -1
  19. package/dist/policy.js.map +1 -1
  20. package/dist/rfc3161/error.d.ts +0 -1
  21. package/dist/rfc3161/error.js.map +1 -1
  22. package/dist/rfc3161/index.d.ts +0 -1
  23. package/dist/rfc3161/index.js.map +1 -1
  24. package/dist/rfc3161/timestamp.d.ts +0 -1
  25. package/dist/rfc3161/timestamp.js.map +1 -1
  26. package/dist/rfc3161/tstinfo.d.ts +0 -1
  27. package/dist/rfc3161/tstinfo.js.map +1 -1
  28. package/dist/sigstore.d.ts +2 -3
  29. package/dist/sigstore.js +9 -9
  30. package/dist/sigstore.js.map +1 -1
  31. package/dist/timestamp/tsa.d.ts +1 -2
  32. package/dist/timestamp/tsa.js +1 -1
  33. package/dist/timestamp/tsa.js.map +1 -1
  34. package/dist/tlog/body.d.ts +0 -1
  35. package/dist/tlog/body.js +1 -1
  36. package/dist/tlog/body.js.map +1 -1
  37. package/dist/tlog/checkpoint.d.ts +0 -1
  38. package/dist/tlog/checkpoint.js.map +1 -1
  39. package/dist/tlog/dsse.d.ts +0 -1
  40. package/dist/tlog/dsse.js +3 -0
  41. package/dist/tlog/dsse.js.map +1 -1
  42. package/dist/tlog/hashedrekord.d.ts +0 -1
  43. package/dist/tlog/hashedrekord.js.map +1 -1
  44. package/dist/tlog/intoto.d.ts +0 -1
  45. package/dist/tlog/intoto.js.map +1 -1
  46. package/dist/tlog/merkle.d.ts +0 -1
  47. package/dist/tlog/merkle.js.map +1 -1
  48. package/dist/trust/tuf-root.d.ts +0 -1
  49. package/dist/trust/tuf-root.js.map +1 -1
  50. package/dist/trust/tuf.d.ts +0 -1
  51. package/dist/trust/tuf.js.map +1 -1
  52. package/dist/x509/cert.d.ts +0 -1
  53. package/dist/x509/cert.js +1 -1
  54. package/dist/x509/cert.js.map +1 -1
  55. package/dist/x509/chain.d.ts +0 -1
  56. package/dist/x509/chain.js +2 -2
  57. package/dist/x509/chain.js.map +1 -1
  58. package/dist/x509/ext.d.ts +0 -1
  59. package/dist/x509/ext.js.map +1 -1
  60. package/dist/x509/index.d.ts +1 -2
  61. package/dist/x509/index.js +1 -1
  62. package/dist/x509/index.js.map +1 -1
  63. package/dist/x509/sct.d.ts +0 -1
  64. package/dist/x509/sct.js.map +1 -1
  65. package/package.json +15 -8
  66. package/.github/workflows/conformance.yml +0 -29
  67. package/.github/workflows/publish.yml +0 -47
  68. package/dist/bundle.d.ts.map +0 -1
  69. package/dist/cli.d.ts.map +0 -1
  70. package/dist/dsse.d.ts.map +0 -1
  71. package/dist/errors.d.ts.map +0 -1
  72. package/dist/index.d.ts.map +0 -1
  73. package/dist/interfaces.d.ts.map +0 -1
  74. package/dist/oid.d.ts.map +0 -1
  75. package/dist/policy.d.ts.map +0 -1
  76. package/dist/rfc3161/error.d.ts.map +0 -1
  77. package/dist/rfc3161/index.d.ts.map +0 -1
  78. package/dist/rfc3161/timestamp.d.ts.map +0 -1
  79. package/dist/rfc3161/tstinfo.d.ts.map +0 -1
  80. package/dist/sigstore.d.ts.map +0 -1
  81. package/dist/timestamp/tsa.d.ts.map +0 -1
  82. package/dist/tlog/body.d.ts.map +0 -1
  83. package/dist/tlog/checkpoint.d.ts.map +0 -1
  84. package/dist/tlog/dsse.d.ts.map +0 -1
  85. package/dist/tlog/hashedrekord.d.ts.map +0 -1
  86. package/dist/tlog/intoto.d.ts.map +0 -1
  87. package/dist/tlog/merkle.d.ts.map +0 -1
  88. package/dist/trust/tuf-root.d.ts.map +0 -1
  89. package/dist/trust/tuf.d.ts.map +0 -1
  90. package/dist/x509/cert.d.ts.map +0 -1
  91. package/dist/x509/chain.d.ts.map +0 -1
  92. package/dist/x509/ext.d.ts.map +0 -1
  93. package/dist/x509/index.d.ts.map +0 -1
  94. package/dist/x509/sct.d.ts.map +0 -1
  95. package/eslint.config.mjs +0 -31
  96. package/src/bundle.ts +0 -186
  97. package/src/cli.ts +0 -290
  98. package/src/dsse.ts +0 -49
  99. package/src/ed25519.browser.test.ts +0 -76
  100. package/src/errors.ts +0 -82
  101. package/src/index.browser.test.ts +0 -301
  102. package/src/index.ts +0 -93
  103. package/src/interfaces.ts +0 -150
  104. package/src/oid.ts +0 -34
  105. package/src/policy.ts +0 -476
  106. package/src/rfc3161/error.ts +0 -16
  107. package/src/rfc3161/index.ts +0 -17
  108. package/src/rfc3161/timestamp.ts +0 -268
  109. package/src/rfc3161/tstinfo.ts +0 -78
  110. package/src/security.browser.test.ts +0 -249
  111. package/src/sigstore.ts +0 -605
  112. package/src/timestamp/tsa.ts +0 -212
  113. package/src/tlog/body.ts +0 -84
  114. package/src/tlog/checkpoint.ts +0 -154
  115. package/src/tlog/dsse.ts +0 -153
  116. package/src/tlog/hashedrekord.ts +0 -136
  117. package/src/tlog/intoto.ts +0 -109
  118. package/src/tlog/merkle.ts +0 -152
  119. package/src/trust/README.md +0 -38
  120. package/src/trust/tuf-root.ts +0 -3
  121. package/src/trust/tuf.ts +0 -233
  122. package/src/verify.browser.test.ts +0 -281
  123. package/src/x509/cert.ts +0 -569
  124. package/src/x509/chain.ts +0 -212
  125. package/src/x509/ext.ts +0 -356
  126. package/src/x509/index.ts +0 -19
  127. package/src/x509/sct.ts +0 -156
  128. package/test/fixtures/rekor2.ts +0 -14
  129. package/test/fixtures/sigstore.ts +0 -13
  130. package/tsconfig.json +0 -18
  131. package/vite.config.playwright.ts +0 -14
  132. package/vite.config.ts +0 -18
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/x509/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"sct.d.ts","sourceRoot":"","sources":["../../src/x509/sct.ts"],"names":[],"mappings":"AAiBA,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,qBAAa,0BAA0B;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;gBAEnB,OAAO,EAAE,UAAU;IAU/B,IAAI,QAAQ,IAAI,IAAI,CAEnB;IAID,IAAI,SAAS,IAAI,MAAM,CA0BtB;IAEY,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;WA8B5D,KAAK,CAAC,GAAG,EAAE,UAAU,GAAG,0BAA0B;CA6CjE"}
package/eslint.config.mjs DELETED
@@ -1,31 +0,0 @@
1
- import simpleImportSort from "eslint-plugin-simple-import-sort";
2
- import * as tseslint from "typescript-eslint";
3
-
4
- export default [
5
- ...tseslint.configs.recommended,
6
-
7
- {
8
- files: ["**/*.{js,ts}"],
9
- languageOptions: {
10
- ecmaVersion: 2022,
11
- sourceType: "module",
12
- },
13
- plugins: {
14
- "simple-import-sort": simpleImportSort,
15
- },
16
- rules: {
17
- "simple-import-sort/imports": "error",
18
- "simple-import-sort/exports": "error",
19
- "@typescript-eslint/no-non-null-assertion": "warn",
20
- "@typescript-eslint/no-unused-vars": [
21
- "error",
22
- {
23
- argsIgnorePattern: "^_",
24
- varsIgnorePattern: "^_",
25
- },
26
- ],
27
-
28
- "no-delete-var": "off",
29
- },
30
- },
31
- ];
package/src/bundle.ts DELETED
@@ -1,186 +0,0 @@
1
- type SigstoreBundleBase = {
2
- mediaType: string;
3
- verificationMaterial: VerificationMaterial;
4
- };
5
-
6
- export type SigstoreBundle =
7
- | (SigstoreBundleBase & { messageSignature: MessageSignature; dsseEnvelope?: never })
8
- | (SigstoreBundleBase & { messageSignature?: never; dsseEnvelope: DSSEEnvelope });
9
-
10
- export interface VerificationMaterial {
11
- certificate?: Certificate;
12
- x509CertificateChain?: X509CertificateChain;
13
- tlogEntries: TLogEntry[];
14
- timestampVerificationData?: TimestampVerificationData;
15
- }
16
-
17
- export interface Certificate {
18
- rawBytes: string; // Base64-encoded certificate bytes
19
- }
20
-
21
- export interface X509CertificateChain {
22
- certificates: Certificate[];
23
- }
24
-
25
- export interface TLogEntry {
26
- logIndex: string;
27
- logId: LogId;
28
- kindVersion: KindVersion;
29
- integratedTime: string | null; // UNIX timestamp (can be null for Rekor v2 bundles)
30
- inclusionPromise?: InclusionPromise;
31
- inclusionProof?: InclusionProof;
32
- canonicalizedBody: string; // Base64-encoded JSON body of the log entry
33
- }
34
-
35
- export interface LogId {
36
- keyId: string; // Base64-encoded key ID
37
- }
38
-
39
- export interface KindVersion {
40
- kind: string;
41
- version: string;
42
- }
43
-
44
- export interface InclusionPromise {
45
- signedEntryTimestamp: string; // Base64-encoded signature over the entry
46
- }
47
-
48
- export interface InclusionProof {
49
- logIndex: string;
50
- rootHash: string; // Base64-encoded root hash of the Merkle tree
51
- treeSize: string; // Number of entries in the Merkle tree
52
- hashes: string[]; // Base64-encoded sibling hashes in the Merkle tree
53
- checkpoint: Checkpoint;
54
- }
55
-
56
- export interface Checkpoint {
57
- envelope: string; // Signed envelope from the transparency log
58
- }
59
-
60
- export interface MessageSignature {
61
- messageDigest: MessageDigest;
62
- signature: string; // Base64-encoded signature over the message digest
63
- }
64
-
65
- export interface MessageDigest {
66
- algorithm: string; // Hashing algorithm, e.g., "SHA2_256"
67
- digest: string; // Base64-encoded message digest
68
- }
69
-
70
- export interface DSSEEnvelope {
71
- payload: string;
72
- payloadType: string;
73
- signatures: DSSESignature[];
74
- }
75
-
76
- export interface DSSESignature {
77
- sig: string;
78
- keyid?: string;
79
- }
80
-
81
- export interface TimestampVerificationData {
82
- rfc3161Timestamps: RFC3161Timestamp[];
83
- }
84
-
85
- export interface RFC3161Timestamp {
86
- signedTimestamp: string; // Base64-encoded RFC3161 SignedData
87
- }
88
-
89
- export interface InTotoStatement {
90
- _type: string;
91
- subject: InTotoSubject[];
92
- predicateType: string;
93
- predicate: Record<string, unknown>;
94
- }
95
-
96
- export interface InTotoSubject {
97
- name: string;
98
- digest: Record<string, string>;
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
- }
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
- }
@@ -1,76 +0,0 @@
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
- });