Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` `<Bundle>`: The Sigstore bundle containing the signature to be verified and the verification material necessary to verify the signature.
- `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.
Expand Down
248 changes: 221 additions & 27 deletions packages/client/src/__tests__/sigstore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
import type { SerializedBundle } from '@sigstore/bundle';
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock';
import { VerificationError } from '@sigstore/verify';
import { VerificationError, Signer } from '@sigstore/verify';

Check failure on line 18 in packages/client/src/__tests__/sigstore.test.ts

View workflow job for this annotation

GitHub Actions / Lint/build code

'Signer' is defined but never used
import { fromPartial } from '@total-typescript/shoehorn';
import mocktuf, { Target } from '@tufjs/repo-mock';
import { attest, createVerifier, sign, verify } from '../sigstore';
Expand Down Expand Up @@ -180,8 +180,12 @@
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);
});

Expand All @@ -196,8 +200,11 @@
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();
});
});

Expand All @@ -207,10 +214,12 @@
);
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');
});
});

Expand All @@ -220,10 +229,12 @@
);
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');
});
});

Expand All @@ -233,10 +244,12 @@
);
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');
});
});

Expand All @@ -246,10 +259,12 @@
);
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');
});
});

Expand Down Expand Up @@ -288,10 +303,107 @@

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<typeof mocktuf> | 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();
});
});
});
Expand Down Expand Up @@ -327,9 +439,13 @@
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');
});
});

Expand All @@ -345,4 +461,82 @@
}).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();
});
});
});
});
19 changes: 9 additions & 10 deletions packages/client/src/sigstore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@sigstore/bundle';
import * as tuf from '@sigstore/tuf';
import {
Signer,
Verifier,
VerifierOptions,
toSignedEntity,
Expand Down Expand Up @@ -51,31 +52,30 @@ export async function attest(
export async function verify(
bundle: SerializedBundle,
options?: config.VerifyOptions
): Promise<void>;
): Promise<Signer>;
export async function verify(
bundle: SerializedBundle,
data: Buffer,
options?: config.VerifyOptions
): Promise<void>;
): Promise<Signer>;
export async function verify(
bundle: SerializedBundle,
dataOrOptions?: Buffer | config.VerifyOptions,
options?: config.VerifyOptions
): Promise<void> {
): Promise<Signer> {
let data: Buffer | undefined;
if (Buffer.isBuffer(dataOrOptions)) {
data = dataOrOptions;
} else {
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(
Expand Down Expand Up @@ -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);
},
};
}
Loading