@ggui-ai/gadget-signing 0.6.2 → 0.7.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 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
- * `base64(sha256(publicKey))[:16]`. Used as the registry's stable handle
114
- * for a stored author public key.
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
- * Optional endpoint overrides — only meaningful when paired with a
252
- * non-prod TUF mirror. Production gadgets verify against the prod
253
- * Sigstore TUF root.
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 endpoints?: {
256
- readonly fulcioURL?: string;
257
- readonly rekorURL?: string;
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 (per
286
- * `@sigstore/bundle` v0.3, `verificationMaterial.x509CertificateChain
287
- * .certificates[0].rawBytes`). Returns `undefined` if the bundle is
288
- * malformed or missing the cert chain.
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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
- * `base64(sha256(publicKey))[:16]`. Used as the registry's stable handle
145
- * for a stored author public key.
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 bytesToBase64(digest).slice(0, 16);
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);
@@ -425,6 +441,18 @@ export async function verifyBundleSigstore(input) {
425
441
  // 4. Run the full upstream verify.
426
442
  try {
427
443
  const verifyOpts = {};
444
+ if (tufMirrorURL !== undefined) {
445
+ verifyOpts.tufMirrorURL = tufMirrorURL;
446
+ }
447
+ if (tufRootPath !== undefined) {
448
+ verifyOpts.tufRootPath = tufRootPath;
449
+ }
450
+ if (tufCachePath !== undefined) {
451
+ verifyOpts.tufCachePath = tufCachePath;
452
+ }
453
+ if (tufForceCache !== undefined) {
454
+ verifyOpts.tufForceCache = tufForceCache;
455
+ }
428
456
  if (expectedIdentity) {
429
457
  if (typeof expectedIdentity.subject === "string") {
430
458
  // Use email-shaped vs URI-shaped routing per upstream's two
@@ -455,10 +483,19 @@ export async function verifyBundleSigstore(input) {
455
483
  }
456
484
  /**
457
485
  * Extract the Fulcio leaf cert's base64 raw bytes from a
458
- * {@link SigstoreSignature}'s serialized cosign bundle (per
459
- * `@sigstore/bundle` v0.3, `verificationMaterial.x509CertificateChain
460
- * .certificates[0].rawBytes`). Returns `undefined` if the bundle is
461
- * malformed or missing the cert chain.
486
+ * {@link SigstoreSignature}'s serialized cosign bundle. Two
487
+ * `verificationMaterial` shapes exist on the wire:
488
+ *
489
+ * - `certificate.rawBytes` — the single-leaf shape bundle v0.3
490
+ * emits. What real `sigstore.sign()` produces today (surfaced by
491
+ * this path's first real signing run, 2026-08-10 — the extractor
492
+ * previously only knew the chain shape and returned `undefined`
493
+ * for every genuinely-signed bundle).
494
+ * - `x509CertificateChain.certificates[0].rawBytes` — the v0.1/v0.2
495
+ * chain shape, still valid on the wire for externally-produced
496
+ * bundles.
497
+ *
498
+ * Returns `undefined` if the bundle is malformed or carries neither.
462
499
  *
463
500
  * Lives here — next to {@link verifyBundleSigstore} — so cosign
464
501
  * bundle-format knowledge has ONE home. Registries persist the
@@ -486,6 +523,14 @@ export function extractSigstoreLeafCertPem(signature) {
486
523
  if (verificationMaterial === null || typeof verificationMaterial !== "object") {
487
524
  return undefined;
488
525
  }
526
+ // Bundle v0.3 single-leaf shape first — what real signing emits.
527
+ const certificate = verificationMaterial.certificate;
528
+ if (certificate !== null && typeof certificate === "object") {
529
+ const rawBytes = certificate.rawBytes;
530
+ if (typeof rawBytes === "string" && rawBytes.length > 0)
531
+ return rawBytes;
532
+ }
533
+ // v0.1/v0.2 chain shape — leaf first.
489
534
  const chain = verificationMaterial
490
535
  .x509CertificateChain;
491
536
  if (chain === null || typeof chain !== "object")
@@ -507,9 +552,15 @@ export function extractSigstoreLeafCertPem(signature) {
507
552
  * by the RegExp-identity pre-check; production verification still goes
508
553
  * through the upstream verifier for the actual cryptographic check.
509
554
  *
555
+ * Parsing delegates to `@sigstore/core`'s `X509Certificate` — the
556
+ * SAME parser the upstream verifier uses — so the SAN this pre-check
557
+ * sees can never drift from the SAN verification enforces. (A prior
558
+ * hand-rolled DER scan over-captured on real certificates —
559
+ * discovered on the first genuinely Fulcio-issued cert it ever saw,
560
+ * 2026-08-10 — and was replaced wholesale rather than patched.)
561
+ *
510
562
  * Returns `undefined` if the bundle has no cert (e.g. publicKey-only
511
- * bundle) or no extractable SAN. Uses lightweight base64-DER scanning —
512
- * defers to the upstream verifier for the trust-chain semantics.
563
+ * bundle), the cert fails to parse, or it carries no URI/email SAN.
513
564
  */
514
565
  function extractSANFromBundle(bundle) {
515
566
  const material = bundle.verificationMaterial;
@@ -526,59 +577,14 @@ function extractSANFromBundle(bundle) {
526
577
  }
527
578
  if (!certB64)
528
579
  return undefined;
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;
534
580
  try {
535
- der = base64ToBytes(certB64);
581
+ const cert = X509Certificate.parse(Buffer.from(certB64, "base64"));
582
+ // `subjectAltName` is the URI GeneralName when present, else the
583
+ // rfc822Name — the exact preference order the verifier's identity
584
+ // policy uses.
585
+ return cert.subjectAltName;
536
586
  }
537
587
  catch {
538
588
  return undefined;
539
589
  }
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];
582
- }
583
- return undefined;
584
590
  }
@@ -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.6.2",
3
+ "version": "0.7.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
  },