@ggui-ai/gadget-signing 0.6.3 → 0.8.0
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/index.d.ts +77 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +126 -73
- package/dist/testing/index.d.ts +92 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +237 -0
- package/package.json +32 -1
package/dist/index.d.ts
CHANGED
|
@@ -110,8 +110,11 @@ export declare function isGadgetSignature(value: unknown): value is GadgetSignat
|
|
|
110
110
|
export declare function canonicalJson(value: unknown): string;
|
|
111
111
|
/**
|
|
112
112
|
* Derive a stable public-key identifier from a 32-byte Ed25519 public key.
|
|
113
|
-
* `
|
|
114
|
-
*
|
|
113
|
+
* `base64url(sha256(publicKey))[:16]` — RFC 4648 §5 URL-safe alphabet
|
|
114
|
+
* (`-`/`_`, no padding), because keyIds travel as URL path segments
|
|
115
|
+
* (`DELETE /author-keys/{keyId}`) and as storage row-key components;
|
|
116
|
+
* standard base64 would put `/` in ~22% of ids. Used as the registry's
|
|
117
|
+
* stable handle for a stored author public key.
|
|
115
118
|
*/
|
|
116
119
|
export declare function derivePublicKeyId(publicKey: Uint8Array): string;
|
|
117
120
|
/**
|
|
@@ -248,14 +251,41 @@ export interface VerifyBundleSigstoreInput {
|
|
|
248
251
|
readonly issuer?: string;
|
|
249
252
|
};
|
|
250
253
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
+
* TUF mirror serving the trust-root metadata + targets. When unset,
|
|
255
|
+
* the upstream verifier resolves against the Sigstore public-good
|
|
256
|
+
* mirror. Point this (together with {@link tufRootPath}) at an
|
|
257
|
+
* alternative TUF repository for private sigstore deployments and
|
|
258
|
+
* hermetic tests — verification never contacts Fulcio/Rekor
|
|
259
|
+
* directly, so the TUF trust root is THE knob that selects which
|
|
260
|
+
* signing infrastructure a verifier trusts.
|
|
254
261
|
*/
|
|
255
|
-
readonly
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
readonly tufMirrorURL?: string;
|
|
263
|
+
/**
|
|
264
|
+
* Path to the initial TUF `root.json` that anchors trust in
|
|
265
|
+
* {@link tufMirrorURL}. Required for any non-public-good mirror
|
|
266
|
+
* (the upstream client only ships seeds for the public-good
|
|
267
|
+
* repository); ignored once a cached root exists under
|
|
268
|
+
* {@link tufCachePath}.
|
|
269
|
+
*/
|
|
270
|
+
readonly tufRootPath?: string;
|
|
271
|
+
/**
|
|
272
|
+
* Writable cache directory for the sigstore TUF trust root. The
|
|
273
|
+
* upstream verifier refreshes TUF metadata into this directory
|
|
274
|
+
* before verifying; when unset it falls back to the platform
|
|
275
|
+
* app-data directory (derived from `$XDG_DATA_HOME` / `$HOME`),
|
|
276
|
+
* which read-only filesystems reject at `mkdir` time — serverless
|
|
277
|
+
* runtimes typically only allow writes under `/tmp`, so point this
|
|
278
|
+
* at e.g. `/tmp/sigstore-js` there.
|
|
279
|
+
*/
|
|
280
|
+
readonly tufCachePath?: string;
|
|
281
|
+
/**
|
|
282
|
+
* Reuse cached TUF metadata without a remote refresh while the
|
|
283
|
+
* cached copy is still valid. Keeps the trust-root network
|
|
284
|
+
* round-trip off warm verification paths; a missing, expired, or
|
|
285
|
+
* corrupt cache falls back to a normal remote refresh, so this is
|
|
286
|
+
* a latency knob — never a correctness one.
|
|
287
|
+
*/
|
|
288
|
+
readonly tufForceCache?: boolean;
|
|
259
289
|
}
|
|
260
290
|
/**
|
|
261
291
|
* Verify a sigstore-signed gadget bundle.
|
|
@@ -282,10 +312,19 @@ export interface VerifyBundleSigstoreInput {
|
|
|
282
312
|
export declare function verifyBundleSigstore(input: VerifyBundleSigstoreInput): Promise<VerifyResult>;
|
|
283
313
|
/**
|
|
284
314
|
* Extract the Fulcio leaf cert's base64 raw bytes from a
|
|
285
|
-
* {@link SigstoreSignature}'s serialized cosign bundle
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
315
|
+
* {@link SigstoreSignature}'s serialized cosign bundle. Two
|
|
316
|
+
* `verificationMaterial` shapes exist on the wire:
|
|
317
|
+
*
|
|
318
|
+
* - `certificate.rawBytes` — the single-leaf shape bundle v0.3
|
|
319
|
+
* emits. What real `sigstore.sign()` produces today (surfaced by
|
|
320
|
+
* this path's first real signing run, 2026-08-10 — the extractor
|
|
321
|
+
* previously only knew the chain shape and returned `undefined`
|
|
322
|
+
* for every genuinely-signed bundle).
|
|
323
|
+
* - `x509CertificateChain.certificates[0].rawBytes` — the v0.1/v0.2
|
|
324
|
+
* chain shape, still valid on the wire for externally-produced
|
|
325
|
+
* bundles.
|
|
326
|
+
*
|
|
327
|
+
* Returns `undefined` if the bundle is malformed or carries neither.
|
|
289
328
|
*
|
|
290
329
|
* Lives here — next to {@link verifyBundleSigstore} — so cosign
|
|
291
330
|
* bundle-format knowledge has ONE home. Registries persist the
|
|
@@ -299,4 +338,29 @@ export declare function verifyBundleSigstore(input: VerifyBundleSigstoreInput):
|
|
|
299
338
|
* a verification.
|
|
300
339
|
*/
|
|
301
340
|
export declare function extractSigstoreLeafCertPem(signature: SigstoreSignature): string | undefined;
|
|
341
|
+
/**
|
|
342
|
+
* Extract EVERY signer identity — the embedded Fulcio cert's URI and
|
|
343
|
+
* rfc822 (email) `subjectAlternativeName` values — from a
|
|
344
|
+
* {@link SigstoreSignature}'s serialized cosign bundle. Returns an
|
|
345
|
+
* EMPTY array when the bundle is malformed, carries no certificate,
|
|
346
|
+
* or the cert has no URI/email SAN.
|
|
347
|
+
*
|
|
348
|
+
* ALL SANs, deliberately: a certificate can carry BOTH a URI and an
|
|
349
|
+
* email SAN, and a policy that inspected only the first would
|
|
350
|
+
* dead-end on such certs. Callers match their policy against every
|
|
351
|
+
* returned identity and accept on ANY hit.
|
|
352
|
+
*
|
|
353
|
+
* This is a PROJECTION, not a verification: callers that make an
|
|
354
|
+
* authorization decision on the returned identities (e.g. a
|
|
355
|
+
* registry's publish-time identity binding) MUST pair it with
|
|
356
|
+
* {@link verifyBundleSigstore} over the same signature — verification
|
|
357
|
+
* is what proves the SAN-bearing cert is genuinely CA-issued and tied
|
|
358
|
+
* to the signed bytes. Parsing delegates to the same X.509 parser the
|
|
359
|
+
* verifier uses (see {@link extractSANsFromBundle}), so the identities
|
|
360
|
+
* a policy check sees can never drift from what verification enforces.
|
|
361
|
+
*
|
|
362
|
+
* Lives here — next to {@link extractSigstoreLeafCertPem} — so cosign
|
|
363
|
+
* bundle-format knowledge has ONE home.
|
|
364
|
+
*/
|
|
365
|
+
export declare function extractSigstoreSANs(signature: SigstoreSignature): readonly string[];
|
|
302
366
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { signAsync, verifyAsync, getPublicKeyAsync, utils } from "@noble/ed25519";
|
|
32
32
|
import { sha384, sha256 } from "@noble/hashes/sha2";
|
|
33
33
|
import { bundleFromJSON, bundleToJSON, } from "@sigstore/bundle";
|
|
34
|
+
import { X509Certificate } from "@sigstore/core";
|
|
34
35
|
import * as sigstoreClient from "sigstore";
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
// Canonical type guards.
|
|
@@ -91,6 +92,18 @@ function bytesToBase64(bytes) {
|
|
|
91
92
|
// `btoa` exists in browsers and modern Node (>=16) globals.
|
|
92
93
|
return btoa(binary);
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* base64url (RFC 4648 §5, unpadded) — the URL- and filename-safe
|
|
97
|
+
* alphabet. Used for identifiers that travel as URL path segments or
|
|
98
|
+
* become row-key/filename components ({@link derivePublicKeyId});
|
|
99
|
+
* signatures and digests stay standard base64 (JSON-body-only).
|
|
100
|
+
*/
|
|
101
|
+
function bytesToBase64Url(bytes) {
|
|
102
|
+
return bytesToBase64(bytes)
|
|
103
|
+
.replaceAll("+", "-")
|
|
104
|
+
.replaceAll("/", "_")
|
|
105
|
+
.replace(/=+$/, "");
|
|
106
|
+
}
|
|
94
107
|
function base64ToBytes(b64) {
|
|
95
108
|
const binary = atob(b64);
|
|
96
109
|
const out = new Uint8Array(binary.length);
|
|
@@ -141,15 +154,18 @@ function canonicalSort(value) {
|
|
|
141
154
|
// ---------------------------------------------------------------------------
|
|
142
155
|
/**
|
|
143
156
|
* Derive a stable public-key identifier from a 32-byte Ed25519 public key.
|
|
144
|
-
* `
|
|
145
|
-
*
|
|
157
|
+
* `base64url(sha256(publicKey))[:16]` — RFC 4648 §5 URL-safe alphabet
|
|
158
|
+
* (`-`/`_`, no padding), because keyIds travel as URL path segments
|
|
159
|
+
* (`DELETE /author-keys/{keyId}`) and as storage row-key components;
|
|
160
|
+
* standard base64 would put `/` in ~22% of ids. Used as the registry's
|
|
161
|
+
* stable handle for a stored author public key.
|
|
146
162
|
*/
|
|
147
163
|
export function derivePublicKeyId(publicKey) {
|
|
148
164
|
if (publicKey.length !== 32) {
|
|
149
165
|
throw new Error(`derivePublicKeyId: expected 32-byte Ed25519 public key, got ${publicKey.length}`);
|
|
150
166
|
}
|
|
151
167
|
const digest = sha256(publicKey);
|
|
152
|
-
return
|
|
168
|
+
return bytesToBase64Url(digest).slice(0, 16);
|
|
153
169
|
}
|
|
154
170
|
/**
|
|
155
171
|
* Derive the Ed25519 public key from a 32-byte private key. Deterministic
|
|
@@ -372,7 +388,7 @@ function classifySigstoreSigningError(err) {
|
|
|
372
388
|
* `signature.bundle` JSON).
|
|
373
389
|
*/
|
|
374
390
|
export async function verifyBundleSigstore(input) {
|
|
375
|
-
const { bundleBytes, signature, expectedIdentity } = input;
|
|
391
|
+
const { bundleBytes, signature, expectedIdentity, tufMirrorURL, tufRootPath, tufCachePath, tufForceCache, } = input;
|
|
376
392
|
// 1. Fast tamper check.
|
|
377
393
|
const recomputed = sha384(bundleBytes);
|
|
378
394
|
const recomputedB64 = bytesToBase64(recomputed);
|
|
@@ -406,25 +422,40 @@ export async function verifyBundleSigstore(input) {
|
|
|
406
422
|
}
|
|
407
423
|
// 3. RegExp identity pre-check. Upstream `sigstore.verify` accepts
|
|
408
424
|
// only literal-equality identity strings, so a RegExp expectation
|
|
409
|
-
// must be enforced here against the bundle's embedded
|
|
425
|
+
// must be enforced here against the bundle's embedded SANs — ANY
|
|
426
|
+
// matching SAN satisfies the pattern (a cert can carry both a URI
|
|
427
|
+
// and an email SAN).
|
|
410
428
|
if (expectedIdentity && expectedIdentity.subject instanceof RegExp) {
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
429
|
+
const sans = extractSANsFromBundle(parsedBundle);
|
|
430
|
+
if (sans.length === 0) {
|
|
413
431
|
return {
|
|
414
432
|
valid: false,
|
|
415
433
|
reason: "expectedIdentity.subject is RegExp but bundle has no subjectAlternativeName to match against",
|
|
416
434
|
};
|
|
417
435
|
}
|
|
418
|
-
|
|
436
|
+
const pattern = expectedIdentity.subject;
|
|
437
|
+
if (!sans.some((san) => pattern.test(san))) {
|
|
419
438
|
return {
|
|
420
439
|
valid: false,
|
|
421
|
-
reason: `identity mismatch: bundle SAN '${
|
|
440
|
+
reason: `identity mismatch: no bundle SAN (${sans.map((s) => `'${s}'`).join(', ')}) matches expected pattern ${pattern}`,
|
|
422
441
|
};
|
|
423
442
|
}
|
|
424
443
|
}
|
|
425
444
|
// 4. Run the full upstream verify.
|
|
426
445
|
try {
|
|
427
446
|
const verifyOpts = {};
|
|
447
|
+
if (tufMirrorURL !== undefined) {
|
|
448
|
+
verifyOpts.tufMirrorURL = tufMirrorURL;
|
|
449
|
+
}
|
|
450
|
+
if (tufRootPath !== undefined) {
|
|
451
|
+
verifyOpts.tufRootPath = tufRootPath;
|
|
452
|
+
}
|
|
453
|
+
if (tufCachePath !== undefined) {
|
|
454
|
+
verifyOpts.tufCachePath = tufCachePath;
|
|
455
|
+
}
|
|
456
|
+
if (tufForceCache !== undefined) {
|
|
457
|
+
verifyOpts.tufForceCache = tufForceCache;
|
|
458
|
+
}
|
|
428
459
|
if (expectedIdentity) {
|
|
429
460
|
if (typeof expectedIdentity.subject === "string") {
|
|
430
461
|
// Use email-shaped vs URI-shaped routing per upstream's two
|
|
@@ -455,10 +486,19 @@ export async function verifyBundleSigstore(input) {
|
|
|
455
486
|
}
|
|
456
487
|
/**
|
|
457
488
|
* Extract the Fulcio leaf cert's base64 raw bytes from a
|
|
458
|
-
* {@link SigstoreSignature}'s serialized cosign bundle
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
489
|
+
* {@link SigstoreSignature}'s serialized cosign bundle. Two
|
|
490
|
+
* `verificationMaterial` shapes exist on the wire:
|
|
491
|
+
*
|
|
492
|
+
* - `certificate.rawBytes` — the single-leaf shape bundle v0.3
|
|
493
|
+
* emits. What real `sigstore.sign()` produces today (surfaced by
|
|
494
|
+
* this path's first real signing run, 2026-08-10 — the extractor
|
|
495
|
+
* previously only knew the chain shape and returned `undefined`
|
|
496
|
+
* for every genuinely-signed bundle).
|
|
497
|
+
* - `x509CertificateChain.certificates[0].rawBytes` — the v0.1/v0.2
|
|
498
|
+
* chain shape, still valid on the wire for externally-produced
|
|
499
|
+
* bundles.
|
|
500
|
+
*
|
|
501
|
+
* Returns `undefined` if the bundle is malformed or carries neither.
|
|
462
502
|
*
|
|
463
503
|
* Lives here — next to {@link verifyBundleSigstore} — so cosign
|
|
464
504
|
* bundle-format knowledge has ONE home. Registries persist the
|
|
@@ -486,6 +526,14 @@ export function extractSigstoreLeafCertPem(signature) {
|
|
|
486
526
|
if (verificationMaterial === null || typeof verificationMaterial !== "object") {
|
|
487
527
|
return undefined;
|
|
488
528
|
}
|
|
529
|
+
// Bundle v0.3 single-leaf shape first — what real signing emits.
|
|
530
|
+
const certificate = verificationMaterial.certificate;
|
|
531
|
+
if (certificate !== null && typeof certificate === "object") {
|
|
532
|
+
const rawBytes = certificate.rawBytes;
|
|
533
|
+
if (typeof rawBytes === "string" && rawBytes.length > 0)
|
|
534
|
+
return rawBytes;
|
|
535
|
+
}
|
|
536
|
+
// v0.1/v0.2 chain shape — leaf first.
|
|
489
537
|
const chain = verificationMaterial
|
|
490
538
|
.x509CertificateChain;
|
|
491
539
|
if (chain === null || typeof chain !== "object")
|
|
@@ -502,19 +550,62 @@ export function extractSigstoreLeafCertPem(signature) {
|
|
|
502
550
|
return rawBytes;
|
|
503
551
|
}
|
|
504
552
|
/**
|
|
505
|
-
*
|
|
506
|
-
* `subjectAlternativeName`
|
|
507
|
-
*
|
|
553
|
+
* Extract EVERY signer identity — the embedded Fulcio cert's URI and
|
|
554
|
+
* rfc822 (email) `subjectAlternativeName` values — from a
|
|
555
|
+
* {@link SigstoreSignature}'s serialized cosign bundle. Returns an
|
|
556
|
+
* EMPTY array when the bundle is malformed, carries no certificate,
|
|
557
|
+
* or the cert has no URI/email SAN.
|
|
558
|
+
*
|
|
559
|
+
* ALL SANs, deliberately: a certificate can carry BOTH a URI and an
|
|
560
|
+
* email SAN, and a policy that inspected only the first would
|
|
561
|
+
* dead-end on such certs. Callers match their policy against every
|
|
562
|
+
* returned identity and accept on ANY hit.
|
|
563
|
+
*
|
|
564
|
+
* This is a PROJECTION, not a verification: callers that make an
|
|
565
|
+
* authorization decision on the returned identities (e.g. a
|
|
566
|
+
* registry's publish-time identity binding) MUST pair it with
|
|
567
|
+
* {@link verifyBundleSigstore} over the same signature — verification
|
|
568
|
+
* is what proves the SAN-bearing cert is genuinely CA-issued and tied
|
|
569
|
+
* to the signed bytes. Parsing delegates to the same X.509 parser the
|
|
570
|
+
* verifier uses (see {@link extractSANsFromBundle}), so the identities
|
|
571
|
+
* a policy check sees can never drift from what verification enforces.
|
|
572
|
+
*
|
|
573
|
+
* Lives here — next to {@link extractSigstoreLeafCertPem} — so cosign
|
|
574
|
+
* bundle-format knowledge has ONE home.
|
|
575
|
+
*/
|
|
576
|
+
export function extractSigstoreSANs(signature) {
|
|
577
|
+
let parsed;
|
|
578
|
+
try {
|
|
579
|
+
parsed = JSON.parse(signature.bundle);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
if (parsed === null || typeof parsed !== "object")
|
|
585
|
+
return [];
|
|
586
|
+
return extractSANsFromBundle(parsed);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Reach into a serialized sigstore Bundle and pull every URI/email
|
|
590
|
+
* `subjectAlternativeName` from the embedded X.509 cert (if any).
|
|
591
|
+
* Used by the RegExp-identity pre-check and
|
|
592
|
+
* {@link extractSigstoreSANs}; production verification still goes
|
|
508
593
|
* through the upstream verifier for the actual cryptographic check.
|
|
509
594
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
595
|
+
* Parsing delegates to `@sigstore/core`'s `X509Certificate` — the
|
|
596
|
+
* SAME parser the upstream verifier uses — so the SANs this pre-check
|
|
597
|
+
* sees can never drift from the SANs verification enforces. (A prior
|
|
598
|
+
* hand-rolled DER scan over-captured on real certificates —
|
|
599
|
+
* discovered on the first genuinely Fulcio-issued cert it ever saw,
|
|
600
|
+
* 2026-08-10 — and was replaced wholesale rather than patched.)
|
|
601
|
+
*
|
|
602
|
+
* Returns `[]` if the bundle has no cert (e.g. publicKey-only
|
|
603
|
+
* bundle), the cert fails to parse, or it carries no URI/email SAN.
|
|
513
604
|
*/
|
|
514
|
-
function
|
|
605
|
+
function extractSANsFromBundle(bundle) {
|
|
515
606
|
const material = bundle.verificationMaterial;
|
|
516
607
|
if (!material)
|
|
517
|
-
return
|
|
608
|
+
return [];
|
|
518
609
|
let certB64;
|
|
519
610
|
if ("certificate" in material && material.certificate) {
|
|
520
611
|
certB64 = material.certificate.rawBytes;
|
|
@@ -525,60 +616,22 @@ function extractSANFromBundle(bundle) {
|
|
|
525
616
|
certB64 = material.x509CertificateChain.certificates[0]?.rawBytes;
|
|
526
617
|
}
|
|
527
618
|
if (!certB64)
|
|
528
|
-
return
|
|
529
|
-
// Very lightweight SAN extraction: decode DER, locate the SAN
|
|
530
|
-
// extension OID (2.5.29.17), and pull the first URI/email-shaped
|
|
531
|
-
// ASCII run. This is intentionally tolerant — exact parsing happens
|
|
532
|
-
// in `@sigstore/verify` downstream.
|
|
533
|
-
let der;
|
|
619
|
+
return [];
|
|
534
620
|
try {
|
|
535
|
-
|
|
621
|
+
const cert = X509Certificate.parse(Buffer.from(certB64, "base64"));
|
|
622
|
+
// Collect BOTH GeneralName kinds the identity policies use — URI
|
|
623
|
+
// first (matching upstream's preference order), then rfc822.
|
|
624
|
+
const ext = cert.extSubjectAltName;
|
|
625
|
+
if (ext === undefined)
|
|
626
|
+
return [];
|
|
627
|
+
const sans = [];
|
|
628
|
+
if (ext.uri !== undefined)
|
|
629
|
+
sans.push(ext.uri);
|
|
630
|
+
if (ext.rfc822Name !== undefined)
|
|
631
|
+
sans.push(ext.rfc822Name);
|
|
632
|
+
return sans;
|
|
536
633
|
}
|
|
537
634
|
catch {
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
// OID 2.5.29.17 (subjectAltName) DER prefix: 06 03 55 1d 11.
|
|
541
|
-
const sanOid = [0x06, 0x03, 0x55, 0x1d, 0x11];
|
|
542
|
-
let idx = -1;
|
|
543
|
-
for (let i = 0; i < der.length - sanOid.length; i++) {
|
|
544
|
-
let match = true;
|
|
545
|
-
for (let j = 0; j < sanOid.length; j++) {
|
|
546
|
-
if (der[i + j] !== sanOid[j]) {
|
|
547
|
-
match = false;
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
if (match) {
|
|
552
|
-
idx = i;
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if (idx === -1)
|
|
557
|
-
return undefined;
|
|
558
|
-
// After the OID DER there's a BOOLEAN (critical, optional) then an
|
|
559
|
-
// OCTET STRING wrapping a SEQUENCE of GeneralName. Scan forward for
|
|
560
|
-
// the first printable ASCII run containing a URI or email shape.
|
|
561
|
-
const tail = der.subarray(idx + sanOid.length);
|
|
562
|
-
let buf = "";
|
|
563
|
-
const uriPattern = /[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]+|[\w.+-]+@[\w.-]+/;
|
|
564
|
-
for (let i = 0; i < tail.length; i++) {
|
|
565
|
-
const b = tail[i];
|
|
566
|
-
if (b >= 0x20 && b <= 0x7e) {
|
|
567
|
-
buf += String.fromCharCode(b);
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
if (buf.length >= 3) {
|
|
571
|
-
const m = buf.match(uriPattern);
|
|
572
|
-
if (m)
|
|
573
|
-
return m[0];
|
|
574
|
-
}
|
|
575
|
-
buf = "";
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
if (buf.length >= 3) {
|
|
579
|
-
const m = buf.match(uriPattern);
|
|
580
|
-
if (m)
|
|
581
|
-
return m[0];
|
|
635
|
+
return [];
|
|
582
636
|
}
|
|
583
|
-
return undefined;
|
|
584
637
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { initializeCA, initializeCTLog, initializeTLog } from '@sigstore/mock';
|
|
2
|
+
type CA = Awaited<ReturnType<typeof initializeCA>>;
|
|
3
|
+
type CTLog = Awaited<ReturnType<typeof initializeCTLog>>;
|
|
4
|
+
type TLog = Awaited<ReturnType<typeof initializeTLog>>;
|
|
5
|
+
/** Options for {@link startSigstoreMockStack}. */
|
|
6
|
+
export interface SigstoreMockStackOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Mount the Fulcio signing-cert endpoint. Default `true`. Pass
|
|
9
|
+
* `false` to exercise CA-unreachable signing failure paths.
|
|
10
|
+
*/
|
|
11
|
+
readonly fulcio?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Mount the Rekor create-entry endpoint. Default `true`. Pass
|
|
14
|
+
* `false` to exercise transparency-log failure paths.
|
|
15
|
+
*/
|
|
16
|
+
readonly rekor?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Claims for {@link SigstoreMockStack.identityToken}. */
|
|
19
|
+
export interface MockIdentityClaims {
|
|
20
|
+
/**
|
|
21
|
+
* OIDC subject — becomes the issued certificate's SAN (as a URI
|
|
22
|
+
* GeneralName; see the module docstring's mock-fidelity note).
|
|
23
|
+
*/
|
|
24
|
+
readonly sub?: string;
|
|
25
|
+
/** OIDC issuer claim — lands in the cert's issuer extension. */
|
|
26
|
+
readonly iss?: string;
|
|
27
|
+
/** Additional claims (email, GitHub Actions workflow claims, …). */
|
|
28
|
+
readonly [claim: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
/** Handle returned by {@link startSigstoreMockStack}. */
|
|
31
|
+
export interface SigstoreMockStack {
|
|
32
|
+
/** Base URL of the mock Fulcio instance. */
|
|
33
|
+
readonly fulcioURL: string;
|
|
34
|
+
/** Base URL of the mock Rekor instance. */
|
|
35
|
+
readonly rekorURL: string;
|
|
36
|
+
/**
|
|
37
|
+
* Endpoint overrides for `signBundleSigstore` — spread into its
|
|
38
|
+
* `endpoints` input.
|
|
39
|
+
*/
|
|
40
|
+
readonly signEndpoints: {
|
|
41
|
+
readonly fulcioURL: string;
|
|
42
|
+
readonly rekorURL: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* TUF trust-root overrides for `verifyBundleSigstore` — spread into
|
|
46
|
+
* its input (or a registry publish op's `sigstoreTuf` deps slot) so
|
|
47
|
+
* verification resolves the MOCK trust root instead of the
|
|
48
|
+
* public-good one.
|
|
49
|
+
*
|
|
50
|
+
* Includes `tufForceCache: true`, and load-bearingly so: the mock
|
|
51
|
+
* TUF mirror's nock interceptors are single-use, so exactly ONE
|
|
52
|
+
* remote metadata refresh succeeds per stack (discovered on this
|
|
53
|
+
* fixture's first multi-verify run — the second refresh died with
|
|
54
|
+
* `error refreshing TUF metadata`). The first verify populates the
|
|
55
|
+
* cache over the mock network; every later verify reuses it — which
|
|
56
|
+
* is also precisely the warm-start path serverless verifiers run in
|
|
57
|
+
* production, so the cache-reuse semantics get real coverage for
|
|
58
|
+
* free.
|
|
59
|
+
*/
|
|
60
|
+
readonly tuf: {
|
|
61
|
+
readonly tufMirrorURL: string;
|
|
62
|
+
readonly tufRootPath: string;
|
|
63
|
+
readonly tufCachePath: string;
|
|
64
|
+
readonly tufForceCache: true;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Subject the zero-argument {@link identityToken} carries — handy
|
|
68
|
+
* for identity-policy assertions.
|
|
69
|
+
*/
|
|
70
|
+
readonly defaultSubject: string;
|
|
71
|
+
/** Issuer the zero-argument {@link identityToken} carries. */
|
|
72
|
+
readonly defaultIssuer: string;
|
|
73
|
+
/**
|
|
74
|
+
* Mint an unsigned JWT-shaped OIDC token the mock Fulcio accepts.
|
|
75
|
+
* The mock decodes claims without verifying a signature.
|
|
76
|
+
*/
|
|
77
|
+
identityToken(claims?: MockIdentityClaims): string;
|
|
78
|
+
/**
|
|
79
|
+
* Remove ALL nock interceptors registered in this process (including
|
|
80
|
+
* other live stacks — one stack per suite), restore real network
|
|
81
|
+
* access, and delete the TUF repo's cache directory.
|
|
82
|
+
*/
|
|
83
|
+
teardown(): void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Boot a full in-process sigstore mock stack: Fulcio + Rekor
|
|
87
|
+
* interceptors plus a TUF repository serving a trusted root built from
|
|
88
|
+
* the same keys. See the module docstring for the trust wiring.
|
|
89
|
+
*/
|
|
90
|
+
export declare function startSigstoreMockStack(options?: SigstoreMockStackOptions): Promise<SigstoreMockStack>;
|
|
91
|
+
export type { CA as MockCA, CTLog as MockCTLog, TLog as MockTLog };
|
|
92
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AA2DA,OAAO,EAEL,YAAY,EACZ,eAAe,EACf,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAkBxB,KAAK,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;AACnD,KAAK,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC;AACzD,KAAK,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC;AAGvD,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE;QACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,GAAG,EAAE;QACZ,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC;KAC9B,CAAC;IACF;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,aAAa,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC;IACnD;;;;OAIG;IACH,QAAQ,IAAI,IAAI,CAAC;CAClB;AAaD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA6H5B;AAgDD,YAAY,EAAE,EAAE,IAAI,MAAM,EAAE,KAAK,IAAI,SAAS,EAAE,IAAI,IAAI,QAAQ,EAAE,CAAC"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic sigstore test infrastructure for `@ggui-ai/gadget-signing`
|
|
3
|
+
* consumers.
|
|
4
|
+
*
|
|
5
|
+
* `startSigstoreMockStack()` boots in-process mock Fulcio + Rekor HTTP
|
|
6
|
+
* endpoints (nock interceptors over `@sigstore/mock`'s CA / CTLog /
|
|
7
|
+
* TLog primitives) **plus** a mock TUF repository (via
|
|
8
|
+
* `@tufjs/repo-mock`) that serves a `trusted_root.json` built from the
|
|
9
|
+
* SAME key material the mock services sign with. The result: the real
|
|
10
|
+
* `signBundleSigstore` and `verifyBundleSigstore` code paths execute
|
|
11
|
+
* end-to-end — real ephemeral key generation, real cert issuance, real
|
|
12
|
+
* transparency-log entries, real bundle serialization, real TUF
|
|
13
|
+
* trust-root resolution, real cryptographic verification — with only
|
|
14
|
+
* the network endpoints and the root of trust substituted, all through
|
|
15
|
+
* the upstream `sigstore` package's public option surface
|
|
16
|
+
* (`fulcioURL` / `rekorURL` on sign; `tufMirrorURL` / `tufRootPath` /
|
|
17
|
+
* `tufCachePath` on verify). Nothing in the sign/verify pipeline is
|
|
18
|
+
* stubbed.
|
|
19
|
+
*
|
|
20
|
+
* Subpath export — consumers
|
|
21
|
+
* `import { startSigstoreMockStack } from '@ggui-ai/gadget-signing/testing'`.
|
|
22
|
+
* Requires `@sigstore/mock`, `@tufjs/repo-mock`, `nock`, and
|
|
23
|
+
* `@sigstore/protobuf-specs` at test-runtime (declared as optional
|
|
24
|
+
* peerDeps; add them to devDependencies alongside this package).
|
|
25
|
+
* `@tufjs/repo-mock` is pinned to exactly 4.0.1 on purpose: 4.0.2
|
|
26
|
+
* moves its nock dependency to v14, which would load a second,
|
|
27
|
+
* differently-patched `http` interceptor next to `@sigstore/mock`'s
|
|
28
|
+
* nock v13 in the same process. At 4.0.1 both resolve one shared nock.
|
|
29
|
+
*
|
|
30
|
+
* Known mock-fidelity limits (upstream `@sigstore/mock` behavior —
|
|
31
|
+
* document divergence from the public-good instance, don't paper over
|
|
32
|
+
* it):
|
|
33
|
+
*
|
|
34
|
+
* - The mock CA writes the certificate SAN as a **URI** GeneralName
|
|
35
|
+
* regardless of the subject's shape, so email-based identity
|
|
36
|
+
* policies (`certificateIdentityEmail`) can never match a
|
|
37
|
+
* mock-issued cert. Use URI-shaped subjects in identity-policy
|
|
38
|
+
* tests; the email routing itself is covered by the
|
|
39
|
+
* option-threading seam tests.
|
|
40
|
+
* - The mock Fulcio decodes (but does not cryptographically verify)
|
|
41
|
+
* the OIDC token, so {@link SigstoreMockStack.identityToken}
|
|
42
|
+
* mints unsigned JWT-shaped tokens.
|
|
43
|
+
*
|
|
44
|
+
* Interceptors are process-global (nock patches `http`), so run ONE
|
|
45
|
+
* stack per suite and call {@link SigstoreMockStack.teardown} in
|
|
46
|
+
* `afterAll`. While a stack is live the stack is HERMETIC: real
|
|
47
|
+
* network egress over `http.ClientRequest` is disabled (loopback
|
|
48
|
+
* excepted), so a request to any unmocked host fails fast and
|
|
49
|
+
* deterministically instead of silently escaping to live DNS — a
|
|
50
|
+
* `rekor: false` stack answers in milliseconds with nock's
|
|
51
|
+
* disallowed-net-connect error rather than waiting on a real
|
|
52
|
+
* NXDOMAIN. Undici-based `fetch` traffic is NOT intercepted (nor
|
|
53
|
+
* blocked) — only the `http.ClientRequest` path the sigstore/TUF
|
|
54
|
+
* clients use — so an in-process HTTP server under test keeps
|
|
55
|
+
* working alongside a live stack. `teardown()` restores real network
|
|
56
|
+
* access.
|
|
57
|
+
*/
|
|
58
|
+
import { createHash, generateKeyPairSync } from 'node:crypto';
|
|
59
|
+
import { join } from 'node:path';
|
|
60
|
+
import { fulcioHandler, initializeCA, initializeCTLog, initializeTLog, rekorHandler, } from '@sigstore/mock';
|
|
61
|
+
import { TrustedRoot } from '@sigstore/protobuf-specs';
|
|
62
|
+
import { createRequire } from 'node:module';
|
|
63
|
+
import nock from 'nock';
|
|
64
|
+
// `@tufjs/repo-mock` ships CJS with a transpiled `exports.default`.
|
|
65
|
+
// Under native Node ESM a default-import binds the whole `exports`
|
|
66
|
+
// object (Node does not unwrap transpiled `.default`), so
|
|
67
|
+
// `mocktuf(...)` throws `TypeError: mocktuf is not a function` in the
|
|
68
|
+
// shipped artifact even though vitest's interop keeps in-repo tests
|
|
69
|
+
// green. `createRequire` reads the CJS shape identically in every
|
|
70
|
+
// runtime — pinned by `dist-esm-interop.integration.test.ts`, which
|
|
71
|
+
// imports the BUILT artifact under plain `node`.
|
|
72
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
73
|
+
const repoMock = cjsRequire('@tufjs/repo-mock');
|
|
74
|
+
const mocktuf = repoMock.default;
|
|
75
|
+
/**
|
|
76
|
+
* Default SAN subject for minted identity tokens. URI-shaped on
|
|
77
|
+
* purpose — the mock CA writes URI SANs (module docstring).
|
|
78
|
+
*/
|
|
79
|
+
const DEFAULT_SUBJECT = 'https://gadgets.ggui.test/e2e-signer';
|
|
80
|
+
/** Mirrors the mock Fulcio's default issuer claim. */
|
|
81
|
+
const DEFAULT_ISSUER = 'https://fake.oidcissuer.com';
|
|
82
|
+
/** Per-process counter so parallel suites get distinct mock hosts. */
|
|
83
|
+
let stackCounter = 0;
|
|
84
|
+
/**
|
|
85
|
+
* Boot a full in-process sigstore mock stack: Fulcio + Rekor
|
|
86
|
+
* interceptors plus a TUF repository serving a trusted root built from
|
|
87
|
+
* the same keys. See the module docstring for the trust wiring.
|
|
88
|
+
*/
|
|
89
|
+
export async function startSigstoreMockStack(options = {}) {
|
|
90
|
+
const id = (stackCounter += 1);
|
|
91
|
+
const fulcioURL = `https://fulcio.mock-${id}.ggui.test`;
|
|
92
|
+
const rekorURL = `https://rekor.mock-${id}.ggui.test`;
|
|
93
|
+
const tufMirrorURL = `https://tuf.mock-${id}.ggui.test`;
|
|
94
|
+
// One P-256 keypair roots the whole stack (CA + CTLog + TLog) —
|
|
95
|
+
// mirrors `@sigstore/mock`'s own top-level `mockFulcio` /
|
|
96
|
+
// `mockRekor` composition, which shares a keypair between the CA
|
|
97
|
+
// and its CTLog.
|
|
98
|
+
const keyPair = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
99
|
+
const ctlog = await initializeCTLog(keyPair);
|
|
100
|
+
const ca = await initializeCA(keyPair, ctlog);
|
|
101
|
+
const tlog = await initializeTLog(rekorURL, keyPair);
|
|
102
|
+
// Hermetic: while the stack is live, requests to any host without a
|
|
103
|
+
// registered interceptor fail fast with nock's NetConnectNotAllowed
|
|
104
|
+
// error instead of escaping to real DNS/network (empirically: an
|
|
105
|
+
// unmocked `.test` host cost ~3s of live NXDOMAIN lookups per
|
|
106
|
+
// attempt before this). Loopback stays open for in-process HTTP
|
|
107
|
+
// servers under test.
|
|
108
|
+
nock.disableNetConnect();
|
|
109
|
+
nock.enableNetConnect((host) => host.startsWith('127.0.0.1') || host.startsWith('localhost'));
|
|
110
|
+
if (options.fulcio !== false) {
|
|
111
|
+
mount(fulcioURL, fulcioHandler(ca, { strict: true }));
|
|
112
|
+
}
|
|
113
|
+
if (options.rekor !== false) {
|
|
114
|
+
mount(rekorURL, rekorHandler(tlog, { strict: true }));
|
|
115
|
+
}
|
|
116
|
+
// Trust material for verification — the SAME roots the mock services
|
|
117
|
+
// sign with, projected onto the `trusted_root.json` shape the TUF
|
|
118
|
+
// client resolves. Built in the protobuf-specs JSON encoding (base64
|
|
119
|
+
// bytes, enum names, ISO timestamps) and round-tripped through the
|
|
120
|
+
// `TrustedRoot` codec so a field drift fails loudly here rather than
|
|
121
|
+
// deep inside the verifier. (The installed protobuf-specs build
|
|
122
|
+
// exposes only `fromJSON`/`toJSON` — no `fromPartial` — discovered
|
|
123
|
+
// on this fixture's first run.)
|
|
124
|
+
//
|
|
125
|
+
// Validity windows are backdated one minute so cert-chain checks sit
|
|
126
|
+
// comfortably inside the window even under clock skew between this
|
|
127
|
+
// init and the moment a test signs.
|
|
128
|
+
const validityStart = new Date(Date.now() - 60_000).toISOString();
|
|
129
|
+
const trustedRootJSON = JSON.stringify(TrustedRoot.toJSON(TrustedRoot.fromJSON({
|
|
130
|
+
mediaType: 'application/vnd.dev.sigstore.trustedroot+json;version=0.1',
|
|
131
|
+
certificateAuthorities: [
|
|
132
|
+
{
|
|
133
|
+
subject: { commonName: 'sigstore', organization: 'sigstore.mock' },
|
|
134
|
+
uri: fulcioURL,
|
|
135
|
+
certChain: {
|
|
136
|
+
certificates: [{ rawBytes: toBase64(ca.rootCertificate) }],
|
|
137
|
+
},
|
|
138
|
+
validFor: { start: validityStart },
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
tlogs: [
|
|
142
|
+
{
|
|
143
|
+
baseUrl: rekorURL,
|
|
144
|
+
logId: {
|
|
145
|
+
keyId: createHash('sha256').update(tlog.publicKey).digest('base64'),
|
|
146
|
+
},
|
|
147
|
+
hashAlgorithm: 'SHA2_256',
|
|
148
|
+
publicKey: {
|
|
149
|
+
rawBytes: tlog.publicKey.toString('base64'),
|
|
150
|
+
keyDetails: 'PKIX_ECDSA_P256_SHA_256',
|
|
151
|
+
validFor: { start: validityStart },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
ctlogs: [
|
|
156
|
+
{
|
|
157
|
+
baseUrl: `${fulcioURL}/ctlog`,
|
|
158
|
+
logId: { keyId: toBase64(ctlog.logID) },
|
|
159
|
+
hashAlgorithm: 'SHA2_256',
|
|
160
|
+
publicKey: {
|
|
161
|
+
rawBytes: ctlog.publicKey.toString('base64'),
|
|
162
|
+
keyDetails: 'PKIX_ECDSA_P256_SHA_256',
|
|
163
|
+
validFor: { start: validityStart },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
timestampAuthorities: [],
|
|
168
|
+
})));
|
|
169
|
+
const target = {
|
|
170
|
+
name: 'trusted_root.json',
|
|
171
|
+
content: Buffer.from(trustedRootJSON),
|
|
172
|
+
};
|
|
173
|
+
// `metadataPathPrefix: ''` matches `@sigstore/tuf`'s mirror layout
|
|
174
|
+
// (metadata at the mirror root, targets under /targets).
|
|
175
|
+
const tufRepo = mocktuf(target, { baseURL: tufMirrorURL, metadataPathPrefix: '' });
|
|
176
|
+
return {
|
|
177
|
+
fulcioURL,
|
|
178
|
+
rekorURL,
|
|
179
|
+
signEndpoints: { fulcioURL, rekorURL },
|
|
180
|
+
tuf: {
|
|
181
|
+
tufMirrorURL: tufRepo.baseURL,
|
|
182
|
+
tufCachePath: tufRepo.cachePath,
|
|
183
|
+
tufRootPath: join(tufRepo.cachePath, 'root.json'),
|
|
184
|
+
tufForceCache: true,
|
|
185
|
+
},
|
|
186
|
+
defaultSubject: DEFAULT_SUBJECT,
|
|
187
|
+
defaultIssuer: DEFAULT_ISSUER,
|
|
188
|
+
identityToken(claims = {}) {
|
|
189
|
+
return mintUnsignedJwt({
|
|
190
|
+
sub: DEFAULT_SUBJECT,
|
|
191
|
+
iss: DEFAULT_ISSUER,
|
|
192
|
+
...claims,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
teardown() {
|
|
196
|
+
nock.cleanAll();
|
|
197
|
+
nock.enableNetConnect();
|
|
198
|
+
tufRepo.teardown();
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Register a persistent nock interceptor for a `@sigstore/mock`
|
|
204
|
+
* service handler. Persistence matters: a suite signs more than once
|
|
205
|
+
* per stack, and nock interceptors are single-use by default.
|
|
206
|
+
*/
|
|
207
|
+
function mount(baseURL, handler) {
|
|
208
|
+
nock(baseURL)
|
|
209
|
+
.persist()
|
|
210
|
+
.post(handler.path)
|
|
211
|
+
.reply(async (_uri, requestBody) => {
|
|
212
|
+
const raw = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
|
|
213
|
+
const { statusCode, response, contentType } = await handler.fn(raw);
|
|
214
|
+
return [statusCode, response, { 'Content-Type': contentType ?? 'text/plain' }];
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Mint an unsigned JWT-shaped token (`<b64url-header>.<b64url-payload>.`)
|
|
219
|
+
* that JWT decoders parse without signature verification — all the
|
|
220
|
+
* mock Fulcio does with it.
|
|
221
|
+
*/
|
|
222
|
+
function mintUnsignedJwt(payload) {
|
|
223
|
+
const header = base64Url(JSON.stringify({ alg: 'none', typ: 'JWT' }));
|
|
224
|
+
const body = base64Url(JSON.stringify(payload));
|
|
225
|
+
return `${header}.${body}.`;
|
|
226
|
+
}
|
|
227
|
+
function base64Url(value) {
|
|
228
|
+
return Buffer.from(value)
|
|
229
|
+
.toString('base64')
|
|
230
|
+
.replace(/=+$/u, '')
|
|
231
|
+
.replace(/\+/gu, '-')
|
|
232
|
+
.replace(/\//gu, '_');
|
|
233
|
+
}
|
|
234
|
+
/** Base64-encode an ArrayBufferView's exact byte range. */
|
|
235
|
+
function toBase64(view) {
|
|
236
|
+
return Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString('base64');
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/gadget-signing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Gadget bundle signing + verification for the ggui gadget marketplace. Ed25519 author-key path + sigstore/cosign keyless path. Pure-TS @noble crypto for Ed25519 — browser-safe.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -25,16 +25,47 @@
|
|
|
25
25
|
"types": "./dist/index.d.ts",
|
|
26
26
|
"import": "./dist/index.js",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./testing": {
|
|
30
|
+
"types": "./dist/testing/index.d.ts",
|
|
31
|
+
"import": "./dist/testing/index.js",
|
|
32
|
+
"default": "./dist/testing/index.js"
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
35
|
"dependencies": {
|
|
31
36
|
"@noble/ed25519": "^2.2.0",
|
|
32
37
|
"@noble/hashes": "^1.6.0",
|
|
33
38
|
"@sigstore/bundle": "^4.0.0",
|
|
39
|
+
"@sigstore/core": "^3.2.1",
|
|
34
40
|
"sigstore": "^4.0.0"
|
|
35
41
|
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@sigstore/mock": "~0.12.1",
|
|
44
|
+
"@sigstore/protobuf-specs": "^0.5.1",
|
|
45
|
+
"@tufjs/repo-mock": "4.0.1",
|
|
46
|
+
"nock": "^13.5.6"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@sigstore/mock": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@sigstore/protobuf-specs": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@tufjs/repo-mock": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"nock": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
36
62
|
"devDependencies": {
|
|
63
|
+
"@sigstore/mock": "~0.12.1",
|
|
64
|
+
"@sigstore/protobuf-specs": "^0.5.1",
|
|
65
|
+
"@sigstore/verify": "^3.1.1",
|
|
66
|
+
"@tufjs/repo-mock": "4.0.1",
|
|
37
67
|
"@types/node": "^24.0.0",
|
|
68
|
+
"nock": "^13.5.6",
|
|
38
69
|
"typescript": "^5.0.0",
|
|
39
70
|
"vitest": "^3.2.6"
|
|
40
71
|
},
|