Skip to content

Commit 58b5d50

Browse files
indexzeroClaude
andcommitted
fix(verify) Return signer for inspection of verify results
`verify(bundle[, payload][, options])` now returns a `Signer` object containing the public key and identity information from the verification. This updates the behavior of `verify` to be consistent with the `sigstore-go` implementation where the `result` is returned to but not consumed by `cosign`. As such the corresponding `packages/cli` functionality was not updated to maximize implementation cohesion Note: technically **NOT** breaking as it is a patch to match the existing `sigstore-go` implementation Fixes: #1489 Citations: - [Existing `verify` behavior](https://github.com/sigstore/sigstore-go/blob/201a35a/pkg/verify/signed_entity.go#L797) - [Existing `cosign verify-blob` behavior](https://github.com/sigstore/cosign/blob/8e3a787/cmd/cosign/cli/verify/verify_blob.go#L192) --------- Co-authored-by: Claude <claude@anthropic.com> Signed-off-by: indexzero <charlie.robbins@gmail.com>
1 parent cee51c0 commit 58b5d50

File tree

4 files changed

+236
-38
lines changed

4 files changed

+236
-38
lines changed

.changeset/rotten-shirts-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sigstore': patch
3+
---
4+
5+
`verify(bundle[, payload][, options])` now returns a `Signer` object containing the public key and identity information from the verification.

packages/client/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ as well as the verification material necessary to verify the signature.
182182

183183
### verify(bundle[, payload][, options])
184184

185-
Verifies the signature in the supplied bundle.
185+
Verifies the signature in the supplied bundle. Returns a `Signer` object containing the public key and identity information from the verification.
186186

187187
- `bundle` `<Bundle>`: The Sigstore bundle containing the signature to be verified and the verification material necessary to verify the signature.
188188
- `payload` `<Buffer>`: The bytes of the artifact over which the signature was created. Only necessary when the `sign` function was used to generate the signature since the Bundle does not contain any information about the artifact which was signed. Not required when the `attest` function was used to generate the Bundle.

packages/client/src/__tests__/sigstore.test.ts

Lines changed: 221 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616
import type { SerializedBundle } from '@sigstore/bundle';
1717
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock';
18-
import { VerificationError } from '@sigstore/verify';
18+
import { VerificationError, Signer } from '@sigstore/verify';
1919
import { fromPartial } from '@total-typescript/shoehorn';
2020
import mocktuf, { Target } from '@tufjs/repo-mock';
2121
import { attest, createVerifier, sign, verify } from '../sigstore';
@@ -180,8 +180,12 @@ describe('#verify', () => {
180180
validBundles.v1.dsse.withSigningCert
181181
);
182182

183-
it('does not throw an error', async () => {
184-
await expect(verify(bundle, tufOptions)).resolves.toBe(undefined);
183+
it('returns a Signer object', async () => {
184+
const result = await verify(bundle, tufOptions);
185+
expect(result).toBeDefined();
186+
expect(result).toHaveProperty('key');
187+
expect(result.key).toBeDefined();
188+
expect(result).toHaveProperty('identity');
185189
}, 10000);
186190
});
187191

@@ -196,8 +200,11 @@ describe('#verify', () => {
196200
timeout: 0,
197201
};
198202

199-
it('does not throw an error', async () => {
200-
await expect(verify(bundle, options)).resolves.toBe(undefined);
203+
it('returns a Signer object', async () => {
204+
const result = await verify(bundle, options);
205+
expect(result).toBeDefined();
206+
expect(result).toHaveProperty('key');
207+
expect(result.key).toBeDefined();
201208
});
202209
});
203210

@@ -207,10 +214,12 @@ describe('#verify', () => {
207214
);
208215
const artifact = validBundles.artifact;
209216

210-
it('does not throw an error', async () => {
211-
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
212-
undefined
213-
);
217+
it('returns a Signer object', async () => {
218+
const result = await verify(bundle, artifact, tufOptions);
219+
expect(result).toBeDefined();
220+
expect(result).toHaveProperty('key');
221+
expect(result.key).toBeDefined();
222+
expect(result).toHaveProperty('identity');
214223
});
215224
});
216225

@@ -220,10 +229,12 @@ describe('#verify', () => {
220229
);
221230
const artifact = validBundles.artifact;
222231

223-
it('does not throw an error', async () => {
224-
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
225-
undefined
226-
);
232+
it('returns a Signer object', async () => {
233+
const result = await verify(bundle, artifact, tufOptions);
234+
expect(result).toBeDefined();
235+
expect(result).toHaveProperty('key');
236+
expect(result.key).toBeDefined();
237+
expect(result).toHaveProperty('identity');
227238
});
228239
});
229240

@@ -233,10 +244,12 @@ describe('#verify', () => {
233244
);
234245
const artifact = validBundles.artifact;
235246

236-
it('does not throw an error', async () => {
237-
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
238-
undefined
239-
);
247+
it('returns a Signer object', async () => {
248+
const result = await verify(bundle, artifact, tufOptions);
249+
expect(result).toBeDefined();
250+
expect(result).toHaveProperty('key');
251+
expect(result.key).toBeDefined();
252+
expect(result).toHaveProperty('identity');
240253
});
241254
});
242255

@@ -246,10 +259,12 @@ describe('#verify', () => {
246259
);
247260
const artifact = validBundles.artifact;
248261

249-
it('does not throw an error', async () => {
250-
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
251-
undefined
252-
);
262+
it('returns a Signer object', async () => {
263+
const result = await verify(bundle, artifact, tufOptions);
264+
expect(result).toBeDefined();
265+
expect(result).toHaveProperty('key');
266+
expect(result.key).toBeDefined();
267+
expect(result).toHaveProperty('identity');
253268
});
254269
});
255270

@@ -288,10 +303,107 @@ describe('#verify', () => {
288303

289304
const artifact = validBundles.artifact;
290305

291-
it('does not throw an error', async () => {
292-
await expect(verify(bundle, artifact, tufOptions)).resolves.toBe(
293-
undefined
294-
);
306+
it('returns a Signer object', async () => {
307+
const result = await verify(bundle, artifact, tufOptions);
308+
expect(result).toBeDefined();
309+
expect(result).toHaveProperty('key');
310+
expect(result.key).toBeDefined();
311+
expect(result).toHaveProperty('identity');
312+
});
313+
});
314+
});
315+
316+
describe('#verify - Signer object structure and properties', () => {
317+
let tufRepo: ReturnType<typeof mocktuf> | undefined;
318+
let tufOptions: VerifyOptions | undefined;
319+
320+
const trustedRootJSON = JSON.stringify(trustedRoot);
321+
const target: Target = {
322+
name: 'trusted_root.json',
323+
content: Buffer.from(trustedRootJSON),
324+
};
325+
326+
beforeEach(() => {
327+
tufRepo = mocktuf(target, { metadataPathPrefix: '' });
328+
tufOptions = {
329+
tufMirrorURL: tufRepo.baseURL,
330+
tufCachePath: tufRepo.cachePath,
331+
tufRootPath: path.join(tufRepo.cachePath, 'root.json'),
332+
certificateIssuer: 'https://github.com/login/oauth',
333+
};
334+
});
335+
336+
afterEach(() => tufRepo?.teardown());
337+
338+
describe('when verifying a DSSE bundle with certificate', () => {
339+
const bundle: SerializedBundle = fromPartial(
340+
validBundles.v1.dsse.withSigningCert
341+
);
342+
343+
it('returns a Signer with a valid key object', async () => {
344+
const result = await verify(bundle, tufOptions);
345+
expect(result).toMatchObject({
346+
key: expect.any(Object),
347+
identity: expect.any(Object),
348+
});
349+
350+
// Verify the key is a proper crypto.KeyObject
351+
expect(result.key).toHaveProperty('asymmetricKeyType');
352+
expect(typeof result.key.export).toBe('function');
353+
});
354+
355+
it('returns a Signer with certificate identity information', async () => {
356+
const result = await verify(bundle, tufOptions);
357+
expect(result.identity).toBeDefined();
358+
359+
// The identity should have either subjectAlternativeName or extensions
360+
expect(
361+
result.identity?.subjectAlternativeName ||
362+
result.identity?.extensions
363+
).toBeDefined();
364+
});
365+
366+
});
367+
368+
describe('when verifying a message signature bundle', () => {
369+
const bundle: SerializedBundle = fromPartial(
370+
validBundles.v1.messageSignature.withSigningCert
371+
);
372+
const artifact = validBundles.artifact;
373+
374+
it('returns a Signer object with key and identity', async () => {
375+
const result = await verify(bundle, artifact, tufOptions);
376+
377+
expect(result).toMatchObject({
378+
key: expect.any(Object),
379+
identity: expect.any(Object),
380+
});
381+
});
382+
383+
it('returns a key that can be used for cryptographic operations', async () => {
384+
const result = await verify(bundle, artifact, tufOptions);
385+
386+
// Verify we can export the public key
387+
expect(() => {
388+
result.key.export({ format: 'pem', type: 'spki' });
389+
}).not.toThrow();
390+
});
391+
});
392+
393+
describe('when verifying with public key', () => {
394+
const bundle: SerializedBundle = fromPartial(
395+
validBundles.v1.dsse.withPublicKey
396+
);
397+
const options: VerifyOptions = {
398+
...tufOptions,
399+
keySelector: (hint: string) => validBundles.publicKeys[hint],
400+
};
401+
402+
it('returns a Signer with key', async () => {
403+
const result = await verify(bundle, options);
404+
405+
expect(result).toHaveProperty('key');
406+
expect(result.key).toBeDefined();
295407
});
296408
});
297409
});
@@ -327,9 +439,13 @@ describe('#createVerifier', () => {
327439
validBundles.v1.dsse.withSigningCert
328440
);
329441

330-
it('does not throw an error when invoked', async () => {
442+
it('returns a Signer object when invoked', async () => {
331443
const verifier = await createVerifier(tufOptions!);
332-
expect(verifier.verify(bundle)).toBeUndefined();
444+
const result = verifier.verify(bundle);
445+
expect(result).toBeDefined();
446+
expect(result).toHaveProperty('key');
447+
expect(result.key).toBeDefined();
448+
expect(result).toHaveProperty('identity');
333449
});
334450
});
335451

@@ -345,4 +461,82 @@ describe('#createVerifier', () => {
345461
}).toThrowWithCode(VerificationError, 'TLOG_BODY_ERROR');
346462
});
347463
});
464+
465+
describe('#createVerifier - BundleVerifier.verify Signer return tests', () => {
466+
describe('when verifying valid bundles', () => {
467+
const bundle: SerializedBundle = fromPartial(
468+
validBundles.v1.dsse.withSigningCert
469+
);
470+
471+
it('BundleVerifier.verify returns Signer with proper structure', async () => {
472+
const verifier = await createVerifier(tufOptions!);
473+
const result = verifier.verify(bundle);
474+
475+
expect(result).toMatchObject({
476+
key: expect.any(Object),
477+
identity: expect.any(Object),
478+
});
479+
480+
// Test the Signer type properties
481+
expect(result.key).toHaveProperty('asymmetricKeyType');
482+
});
483+
484+
485+
it('BundleVerifier.verify throws error for invalid bundle but still returns Signer type when valid', async () => {
486+
const validVerifier = await createVerifier(tufOptions!);
487+
const invalidBundle: SerializedBundle = fromPartial(
488+
invalidBundles.v1.dsse.invalidBadSignature
489+
);
490+
491+
// Test that invalid bundles still throw errors
492+
expect(() => {
493+
validVerifier.verify(invalidBundle);
494+
}).toThrowWithCode(VerificationError, 'TLOG_BODY_ERROR');
495+
496+
// But valid bundles should return Signer
497+
const result = validVerifier.verify(bundle);
498+
expect(result).toMatchObject({
499+
key: expect.any(Object),
500+
identity: expect.any(Object),
501+
});
502+
});
503+
});
504+
505+
describe('when verifying with data payload', () => {
506+
const bundle: SerializedBundle = fromPartial(
507+
validBundles.v1.messageSignature.withSigningCert
508+
);
509+
const artifact = validBundles.artifact;
510+
511+
it('BundleVerifier.verify with data returns proper Signer', async () => {
512+
const verifier = await createVerifier(tufOptions!);
513+
const result = verifier.verify(bundle, artifact);
514+
515+
expect(result).toMatchObject({
516+
key: expect.any(Object),
517+
identity: expect.any(Object),
518+
});
519+
520+
// Verify identity contains expected certificate information
521+
expect(result.identity).toBeDefined();
522+
if (result.identity) {
523+
expect(
524+
result.identity.subjectAlternativeName || result.identity.extensions
525+
).toBeDefined();
526+
}
527+
});
528+
529+
it('BundleVerifier.verify returns Signer with working cryptographic key', async () => {
530+
const verifier = await createVerifier(tufOptions!);
531+
const result = verifier.verify(bundle, artifact);
532+
533+
// Ensure the key can be exported and used
534+
expect(() => {
535+
const exported = result.key.export({ format: 'pem', type: 'spki' });
536+
expect(typeof exported).toBe('string');
537+
expect(exported).toContain('-----BEGIN PUBLIC KEY-----');
538+
}).not.toThrow();
539+
});
540+
});
541+
});
348542
});

packages/client/src/sigstore.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@sigstore/bundle';
2121
import * as tuf from '@sigstore/tuf';
2222
import {
23+
Signer,
2324
Verifier,
2425
VerifierOptions,
2526
toSignedEntity,
@@ -51,31 +52,30 @@ export async function attest(
5152
export async function verify(
5253
bundle: SerializedBundle,
5354
options?: config.VerifyOptions
54-
): Promise<void>;
55+
): Promise<Signer>;
5556
export async function verify(
5657
bundle: SerializedBundle,
5758
data: Buffer,
5859
options?: config.VerifyOptions
59-
): Promise<void>;
60+
): Promise<Signer>;
6061
export async function verify(
6162
bundle: SerializedBundle,
6263
dataOrOptions?: Buffer | config.VerifyOptions,
6364
options?: config.VerifyOptions
64-
): Promise<void> {
65+
): Promise<Signer> {
6566
let data: Buffer | undefined;
6667
if (Buffer.isBuffer(dataOrOptions)) {
6768
data = dataOrOptions;
6869
} else {
6970
options = dataOrOptions;
7071
}
7172

72-
return createVerifier(options).then((verifier) =>
73-
verifier.verify(bundle, data)
74-
);
73+
const verifier = await createVerifier(options);
74+
return verifier.verify(bundle, data);
7575
}
7676

7777
export interface BundleVerifier {
78-
verify(bundle: SerializedBundle, data?: Buffer): void;
78+
verify(bundle: SerializedBundle, data?: Buffer): Signer;
7979
}
8080

8181
export async function createVerifier(
@@ -104,11 +104,10 @@ export async function createVerifier(
104104
const policy = config.createVerificationPolicy(options);
105105

106106
return {
107-
verify: (bundle: SerializedBundle, payload?: Buffer): void => {
107+
verify: (bundle: SerializedBundle, payload?: Buffer): Signer => {
108108
const deserializedBundle = bundleFromJSON(bundle);
109109
const signedEntity = toSignedEntity(deserializedBundle, payload);
110-
verifier.verify(signedEntity, policy);
111-
return;
110+
return verifier.verify(signedEntity, policy);
112111
},
113112
};
114113
}

0 commit comments

Comments
 (0)