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