diff --git a/package-lock.json b/package-lock.json index f50cc5fa..70c76a63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13186,7 +13186,7 @@ }, "packages/sign": { "name": "@sigstore/sign", - "version": "4.0.0", + "version": "4.0.1", "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", diff --git a/packages/client/README.md b/packages/client/README.md index d84ed635..7819207b 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -182,7 +182,7 @@ as well as the verification material necessary to verify the signature. ### verify(bundle[, payload][, options]) -Verifies the signature in the supplied bundle. +Verifies the signature in the supplied bundle. Returns a `Signer` object containing the public key and identity information from the verification. - `bundle` ``: The Sigstore bundle containing the signature to be verified and the verification material necessary to verify the signature. - `payload` ``: 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. diff --git a/packages/client/src/__tests__/sigstore.test.ts b/packages/client/src/__tests__/sigstore.test.ts index 78bca4e5..1f9c8cfe 100644 --- a/packages/client/src/__tests__/sigstore.test.ts +++ b/packages/client/src/__tests__/sigstore.test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type { SerializedBundle } from '@sigstore/bundle'; import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'; -import { VerificationError } from '@sigstore/verify'; +import { VerificationError, Signer } from '@sigstore/verify'; import { fromPartial } from '@total-typescript/shoehorn'; import mocktuf, { Target } from '@tufjs/repo-mock'; import { attest, createVerifier, sign, verify } from '../sigstore'; @@ -180,8 +180,12 @@ describe('#verify', () => { validBundles.v1.dsse.withSigningCert ); - it('does not throw an error', async () => { - await expect(verify(bundle, tufOptions)).resolves.toBe(undefined); + it('returns a Signer object', async () => { + const result = await verify(bundle, tufOptions); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); }, 10000); }); @@ -196,8 +200,11 @@ describe('#verify', () => { timeout: 0, }; - it('does not throw an error', async () => { - await expect(verify(bundle, options)).resolves.toBe(undefined); + it('returns a Signer object', async () => { + const result = await verify(bundle, options); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); }); }); @@ -207,10 +214,12 @@ describe('#verify', () => { ); const artifact = validBundles.artifact; - it('does not throw an error', async () => { - await expect(verify(bundle, artifact, tufOptions)).resolves.toBe( - undefined - ); + it('returns a Signer object', async () => { + const result = await verify(bundle, artifact, tufOptions); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); }); }); @@ -220,10 +229,12 @@ describe('#verify', () => { ); const artifact = validBundles.artifact; - it('does not throw an error', async () => { - await expect(verify(bundle, artifact, tufOptions)).resolves.toBe( - undefined - ); + it('returns a Signer object', async () => { + const result = await verify(bundle, artifact, tufOptions); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); }); }); @@ -233,10 +244,12 @@ describe('#verify', () => { ); const artifact = validBundles.artifact; - it('does not throw an error', async () => { - await expect(verify(bundle, artifact, tufOptions)).resolves.toBe( - undefined - ); + it('returns a Signer object', async () => { + const result = await verify(bundle, artifact, tufOptions); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); }); }); @@ -246,10 +259,12 @@ describe('#verify', () => { ); const artifact = validBundles.artifact; - it('does not throw an error', async () => { - await expect(verify(bundle, artifact, tufOptions)).resolves.toBe( - undefined - ); + it('returns a Signer object', async () => { + const result = await verify(bundle, artifact, tufOptions); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); }); }); @@ -288,10 +303,107 @@ describe('#verify', () => { const artifact = validBundles.artifact; - it('does not throw an error', async () => { - await expect(verify(bundle, artifact, tufOptions)).resolves.toBe( - undefined - ); + it('returns a Signer object', async () => { + const result = await verify(bundle, artifact, tufOptions); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); + }); + }); +}); + +describe('#verify - Signer object structure and properties', () => { + let tufRepo: ReturnType | undefined; + let tufOptions: VerifyOptions | undefined; + + const trustedRootJSON = JSON.stringify(trustedRoot); + const target: Target = { + name: 'trusted_root.json', + content: Buffer.from(trustedRootJSON), + }; + + beforeEach(() => { + tufRepo = mocktuf(target, { metadataPathPrefix: '' }); + tufOptions = { + tufMirrorURL: tufRepo.baseURL, + tufCachePath: tufRepo.cachePath, + tufRootPath: path.join(tufRepo.cachePath, 'root.json'), + certificateIssuer: 'https://github.com/login/oauth', + }; + }); + + afterEach(() => tufRepo?.teardown()); + + describe('when verifying a DSSE bundle with certificate', () => { + const bundle: SerializedBundle = fromPartial( + validBundles.v1.dsse.withSigningCert + ); + + it('returns a Signer with a valid key object', async () => { + const result = await verify(bundle, tufOptions); + expect(result).toMatchObject({ + key: expect.any(Object), + identity: expect.any(Object), + }); + + // Verify the key is a proper crypto.KeyObject + expect(result.key).toHaveProperty('asymmetricKeyType'); + expect(typeof result.key.export).toBe('function'); + }); + + it('returns a Signer with certificate identity information', async () => { + const result = await verify(bundle, tufOptions); + expect(result.identity).toBeDefined(); + + // The identity should have either subjectAlternativeName or extensions + expect( + result.identity?.subjectAlternativeName || + result.identity?.extensions + ).toBeDefined(); + }); + + }); + + describe('when verifying a message signature bundle', () => { + const bundle: SerializedBundle = fromPartial( + validBundles.v1.messageSignature.withSigningCert + ); + const artifact = validBundles.artifact; + + it('returns a Signer object with key and identity', async () => { + const result = await verify(bundle, artifact, tufOptions); + + expect(result).toMatchObject({ + key: expect.any(Object), + identity: expect.any(Object), + }); + }); + + it('returns a key that can be used for cryptographic operations', async () => { + const result = await verify(bundle, artifact, tufOptions); + + // Verify we can export the public key + expect(() => { + result.key.export({ format: 'pem', type: 'spki' }); + }).not.toThrow(); + }); + }); + + describe('when verifying with public key', () => { + const bundle: SerializedBundle = fromPartial( + validBundles.v1.dsse.withPublicKey + ); + const options: VerifyOptions = { + ...tufOptions, + keySelector: (hint: string) => validBundles.publicKeys[hint], + }; + + it('returns a Signer with key', async () => { + const result = await verify(bundle, options); + + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); }); }); }); @@ -327,9 +439,13 @@ describe('#createVerifier', () => { validBundles.v1.dsse.withSigningCert ); - it('does not throw an error when invoked', async () => { + it('returns a Signer object when invoked', async () => { const verifier = await createVerifier(tufOptions!); - expect(verifier.verify(bundle)).toBeUndefined(); + const result = verifier.verify(bundle); + expect(result).toBeDefined(); + expect(result).toHaveProperty('key'); + expect(result.key).toBeDefined(); + expect(result).toHaveProperty('identity'); }); }); @@ -345,4 +461,82 @@ describe('#createVerifier', () => { }).toThrowWithCode(VerificationError, 'TLOG_BODY_ERROR'); }); }); + + describe('#createVerifier - BundleVerifier.verify Signer return tests', () => { + describe('when verifying valid bundles', () => { + const bundle: SerializedBundle = fromPartial( + validBundles.v1.dsse.withSigningCert + ); + + it('BundleVerifier.verify returns Signer with proper structure', async () => { + const verifier = await createVerifier(tufOptions!); + const result = verifier.verify(bundle); + + expect(result).toMatchObject({ + key: expect.any(Object), + identity: expect.any(Object), + }); + + // Test the Signer type properties + expect(result.key).toHaveProperty('asymmetricKeyType'); + }); + + + it('BundleVerifier.verify throws error for invalid bundle but still returns Signer type when valid', async () => { + const validVerifier = await createVerifier(tufOptions!); + const invalidBundle: SerializedBundle = fromPartial( + invalidBundles.v1.dsse.invalidBadSignature + ); + + // Test that invalid bundles still throw errors + expect(() => { + validVerifier.verify(invalidBundle); + }).toThrowWithCode(VerificationError, 'TLOG_BODY_ERROR'); + + // But valid bundles should return Signer + const result = validVerifier.verify(bundle); + expect(result).toMatchObject({ + key: expect.any(Object), + identity: expect.any(Object), + }); + }); + }); + + describe('when verifying with data payload', () => { + const bundle: SerializedBundle = fromPartial( + validBundles.v1.messageSignature.withSigningCert + ); + const artifact = validBundles.artifact; + + it('BundleVerifier.verify with data returns proper Signer', async () => { + const verifier = await createVerifier(tufOptions!); + const result = verifier.verify(bundle, artifact); + + expect(result).toMatchObject({ + key: expect.any(Object), + identity: expect.any(Object), + }); + + // Verify identity contains expected certificate information + expect(result.identity).toBeDefined(); + if (result.identity) { + expect( + result.identity.subjectAlternativeName || result.identity.extensions + ).toBeDefined(); + } + }); + + it('BundleVerifier.verify returns Signer with working cryptographic key', async () => { + const verifier = await createVerifier(tufOptions!); + const result = verifier.verify(bundle, artifact); + + // Ensure the key can be exported and used + expect(() => { + const exported = result.key.export({ format: 'pem', type: 'spki' }); + expect(typeof exported).toBe('string'); + expect(exported).toContain('-----BEGIN PUBLIC KEY-----'); + }).not.toThrow(); + }); + }); + }); }); diff --git a/packages/client/src/sigstore.ts b/packages/client/src/sigstore.ts index 59ae15f3..0bbd31ee 100644 --- a/packages/client/src/sigstore.ts +++ b/packages/client/src/sigstore.ts @@ -20,6 +20,7 @@ import { } from '@sigstore/bundle'; import * as tuf from '@sigstore/tuf'; import { + Signer, Verifier, VerifierOptions, toSignedEntity, @@ -51,17 +52,17 @@ export async function attest( export async function verify( bundle: SerializedBundle, options?: config.VerifyOptions -): Promise; +): Promise; export async function verify( bundle: SerializedBundle, data: Buffer, options?: config.VerifyOptions -): Promise; +): Promise; export async function verify( bundle: SerializedBundle, dataOrOptions?: Buffer | config.VerifyOptions, options?: config.VerifyOptions -): Promise { +): Promise { let data: Buffer | undefined; if (Buffer.isBuffer(dataOrOptions)) { data = dataOrOptions; @@ -69,13 +70,12 @@ export async function verify( options = dataOrOptions; } - return createVerifier(options).then((verifier) => - verifier.verify(bundle, data) - ); + const verifier = await createVerifier(options); + return verifier.verify(bundle, data); } export interface BundleVerifier { - verify(bundle: SerializedBundle, data?: Buffer): void; + verify(bundle: SerializedBundle, data?: Buffer): Signer; } export async function createVerifier( @@ -104,11 +104,10 @@ export async function createVerifier( const policy = config.createVerificationPolicy(options); return { - verify: (bundle: SerializedBundle, payload?: Buffer): void => { + verify: (bundle: SerializedBundle, payload?: Buffer): Signer => { const deserializedBundle = bundleFromJSON(bundle); const signedEntity = toSignedEntity(deserializedBundle, payload); - verifier.verify(signedEntity, policy); - return; + return verifier.verify(signedEntity, policy); }, }; }