@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.
- package/dist/bundle.d.ts +2 -3
- package/dist/bundle.js.map +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/dsse.d.ts +0 -1
- package/dist/dsse.js.map +1 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -10
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +1 -2
- package/dist/interfaces.js +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/oid.d.ts +0 -1
- package/dist/oid.js.map +1 -1
- package/dist/policy.d.ts +0 -1
- package/dist/policy.js +1 -1
- package/dist/policy.js.map +1 -1
- package/dist/rfc3161/error.d.ts +0 -1
- package/dist/rfc3161/error.js.map +1 -1
- package/dist/rfc3161/index.d.ts +0 -1
- package/dist/rfc3161/index.js.map +1 -1
- package/dist/rfc3161/timestamp.d.ts +0 -1
- package/dist/rfc3161/timestamp.js.map +1 -1
- package/dist/rfc3161/tstinfo.d.ts +0 -1
- package/dist/rfc3161/tstinfo.js.map +1 -1
- package/dist/sigstore.d.ts +2 -3
- package/dist/sigstore.js +9 -9
- package/dist/sigstore.js.map +1 -1
- package/dist/timestamp/tsa.d.ts +1 -2
- package/dist/timestamp/tsa.js +1 -1
- package/dist/timestamp/tsa.js.map +1 -1
- package/dist/tlog/body.d.ts +0 -1
- package/dist/tlog/body.js +1 -1
- package/dist/tlog/body.js.map +1 -1
- package/dist/tlog/checkpoint.d.ts +0 -1
- package/dist/tlog/checkpoint.js.map +1 -1
- package/dist/tlog/dsse.d.ts +0 -1
- package/dist/tlog/dsse.js +3 -0
- package/dist/tlog/dsse.js.map +1 -1
- package/dist/tlog/hashedrekord.d.ts +0 -1
- package/dist/tlog/hashedrekord.js.map +1 -1
- package/dist/tlog/intoto.d.ts +0 -1
- package/dist/tlog/intoto.js.map +1 -1
- package/dist/tlog/merkle.d.ts +0 -1
- package/dist/tlog/merkle.js.map +1 -1
- package/dist/trust/tuf-root.d.ts +0 -1
- package/dist/trust/tuf-root.js.map +1 -1
- package/dist/trust/tuf.d.ts +0 -1
- package/dist/trust/tuf.js.map +1 -1
- package/dist/x509/cert.d.ts +0 -1
- package/dist/x509/cert.js +1 -1
- package/dist/x509/cert.js.map +1 -1
- package/dist/x509/chain.d.ts +0 -1
- package/dist/x509/chain.js +2 -2
- package/dist/x509/chain.js.map +1 -1
- package/dist/x509/ext.d.ts +0 -1
- package/dist/x509/ext.js.map +1 -1
- package/dist/x509/index.d.ts +1 -2
- package/dist/x509/index.js +1 -1
- package/dist/x509/index.js.map +1 -1
- package/dist/x509/sct.d.ts +0 -1
- package/dist/x509/sct.js.map +1 -1
- package/package.json +15 -8
- package/.github/workflows/conformance.yml +0 -29
- package/.github/workflows/publish.yml +0 -47
- package/dist/bundle.d.ts.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/dsse.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/interfaces.d.ts.map +0 -1
- package/dist/oid.d.ts.map +0 -1
- package/dist/policy.d.ts.map +0 -1
- package/dist/rfc3161/error.d.ts.map +0 -1
- package/dist/rfc3161/index.d.ts.map +0 -1
- package/dist/rfc3161/timestamp.d.ts.map +0 -1
- package/dist/rfc3161/tstinfo.d.ts.map +0 -1
- package/dist/sigstore.d.ts.map +0 -1
- package/dist/timestamp/tsa.d.ts.map +0 -1
- package/dist/tlog/body.d.ts.map +0 -1
- package/dist/tlog/checkpoint.d.ts.map +0 -1
- package/dist/tlog/dsse.d.ts.map +0 -1
- package/dist/tlog/hashedrekord.d.ts.map +0 -1
- package/dist/tlog/intoto.d.ts.map +0 -1
- package/dist/tlog/merkle.d.ts.map +0 -1
- package/dist/trust/tuf-root.d.ts.map +0 -1
- package/dist/trust/tuf.d.ts.map +0 -1
- package/dist/x509/cert.d.ts.map +0 -1
- package/dist/x509/chain.d.ts.map +0 -1
- package/dist/x509/ext.d.ts.map +0 -1
- package/dist/x509/index.d.ts.map +0 -1
- package/dist/x509/sct.d.ts.map +0 -1
- package/eslint.config.mjs +0 -31
- package/src/bundle.ts +0 -186
- package/src/cli.ts +0 -290
- package/src/dsse.ts +0 -49
- package/src/ed25519.browser.test.ts +0 -76
- package/src/errors.ts +0 -82
- package/src/index.browser.test.ts +0 -301
- package/src/index.ts +0 -93
- package/src/interfaces.ts +0 -150
- package/src/oid.ts +0 -34
- package/src/policy.ts +0 -476
- package/src/rfc3161/error.ts +0 -16
- package/src/rfc3161/index.ts +0 -17
- package/src/rfc3161/timestamp.ts +0 -268
- package/src/rfc3161/tstinfo.ts +0 -78
- package/src/security.browser.test.ts +0 -249
- package/src/sigstore.ts +0 -605
- package/src/timestamp/tsa.ts +0 -212
- package/src/tlog/body.ts +0 -84
- package/src/tlog/checkpoint.ts +0 -154
- package/src/tlog/dsse.ts +0 -153
- package/src/tlog/hashedrekord.ts +0 -136
- package/src/tlog/intoto.ts +0 -109
- package/src/tlog/merkle.ts +0 -152
- package/src/trust/README.md +0 -38
- package/src/trust/tuf-root.ts +0 -3
- package/src/trust/tuf.ts +0 -233
- package/src/verify.browser.test.ts +0 -281
- package/src/x509/cert.ts +0 -569
- package/src/x509/chain.ts +0 -212
- package/src/x509/ext.ts +0 -356
- package/src/x509/index.ts +0 -19
- package/src/x509/sct.ts +0 -156
- package/test/fixtures/rekor2.ts +0 -14
- package/test/fixtures/sigstore.ts +0 -13
- package/tsconfig.json +0 -18
- package/vite.config.playwright.ts +0 -14
- package/vite.config.ts +0 -18
package/dist/x509/index.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/x509/sct.d.ts.map
DELETED
|
@@ -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
|
-
});
|