diff --git a/.changeset/beige-keys-appear.md b/.changeset/beige-keys-appear.md new file mode 100644 index 00000000..4108600d --- /dev/null +++ b/.changeset/beige-keys-appear.md @@ -0,0 +1,5 @@ +--- +'@sigstore/verify': minor +--- + +Export `VerificationPolicy` type diff --git a/.changeset/gold-walls-smoke.md b/.changeset/gold-walls-smoke.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/gold-walls-smoke.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/client/src/__tests__/config.test.ts b/packages/client/src/__tests__/config.test.ts index fe5660b6..471a9ea0 100644 --- a/packages/client/src/__tests__/config.test.ts +++ b/packages/client/src/__tests__/config.test.ts @@ -18,7 +18,11 @@ import { MessageSignatureBundleBuilder, } from '@sigstore/sign'; import { VerificationError } from '@sigstore/verify'; -import { createBundleBuilder, createKeyFinder } from '../config'; +import { + createBundleBuilder, + createKeyFinder, + createVerificationPolicy, +} from '../config'; import { publicKeys } from './__fixtures__/bundles/valid'; describe('createBundleBuilder', () => { @@ -91,3 +95,43 @@ describe('createKeyFinder', () => { }); }); }); + +describe('createVerificationPolicy', () => { + describe('when the options specify a certificateIdentityEmail', () => { + const options = { certificateIdentityEmail: 'foo@bar.com' }; + + it('returns a verification policy', () => { + const policy = createVerificationPolicy(options); + expect(policy).toBeDefined(); + expect(policy.subjectAlternativeName).toEqual( + options.certificateIdentityEmail + ); + expect(policy.extensions).toBeUndefined(); + }); + }); + + describe('when the options specify a certificateIdentityURI', () => { + const options = { certificateIdentityURI: 'https://foo.bar.com' }; + + it('returns a verification policy', () => { + const policy = createVerificationPolicy(options); + expect(policy).toBeDefined(); + expect(policy.subjectAlternativeName).toEqual( + options.certificateIdentityURI + ); + expect(policy.extensions).toBeUndefined(); + }); + }); + + describe('when the options specify a certificateIssuer', () => { + const options = { certificateIssuer: 'https://bar.foo.com' }; + + it('returns a verification policy', () => { + const policy = createVerificationPolicy(options); + expect(policy).toBeDefined(); + expect(policy.extensions).toBeDefined(); + expect(policy.extensions?.issuer).toEqual(options.certificateIssuer); + expect(policy.subjectAlternativeName).toBeUndefined(); + }); + }); +}); diff --git a/packages/client/src/__tests__/sigstore.test.ts b/packages/client/src/__tests__/sigstore.test.ts index b48ae498..9b402ec7 100644 --- a/packages/client/src/__tests__/sigstore.test.ts +++ b/packages/client/src/__tests__/sigstore.test.ts @@ -129,6 +129,7 @@ describe('#verify', () => { tufOptions = { tufMirrorURL: tufRepo.baseURL, tufCachePath: tufRepo.cachePath, + certificateIssuer: 'https://github.com/login/oauth', }; }); diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index d048f838..c235cc5c 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -27,7 +27,11 @@ import { TSAWitness, Witness, } from '@sigstore/sign'; -import { KeyFinderFunc, VerificationError } from '@sigstore/verify'; +import { + KeyFinderFunc, + VerificationError, + VerificationPolicy, +} from '@sigstore/verify'; import type { MakeFetchHappenOptions } from 'make-fetch-happen'; @@ -112,6 +116,24 @@ export function createKeyFinder(keySelector: KeySelector): KeyFinderFunc { }; } +export function createVerificationPolicy( + options: VerifyOptions +): VerificationPolicy { + const policy: VerificationPolicy = {}; + + const san = + options.certificateIdentityEmail || options.certificateIdentityURI; + if (san) { + policy.subjectAlternativeName = san; + } + + if (options.certificateIssuer) { + policy.extensions = { issuer: options.certificateIssuer }; + } + + return policy; +} + // Instantiate the FulcioSigner based on the supplied options. function initSigner(options: SignOptions): Signer { return new FulcioSigner({ diff --git a/packages/client/src/sigstore.ts b/packages/client/src/sigstore.ts index fb31525a..e3187b0c 100644 --- a/packages/client/src/sigstore.ts +++ b/packages/client/src/sigstore.ts @@ -100,12 +100,13 @@ export async function createVerifier( tlogThreshold: options.tlogThreshold, }; const verifier = new Verifier(trustMaterial, verifierOptions); + const policy = config.createVerificationPolicy(options); return { verify: (bundle: SerializedBundle, payload?: Buffer): void => { const deserializedBundle = bundleFromJSON(bundle); const signedEntity = toSignedEntity(deserializedBundle, payload); - verifier.verify(signedEntity); + verifier.verify(signedEntity, policy); return; }, }; diff --git a/packages/verify/src/__tests__/index.test.ts b/packages/verify/src/__tests__/index.test.ts index 5281ecf7..708001e3 100644 --- a/packages/verify/src/__tests__/index.test.ts +++ b/packages/verify/src/__tests__/index.test.ts @@ -21,6 +21,7 @@ import { Signer, TrustMaterial, VerificationError, + VerificationPolicy, Verifier, VerifierOptions, toSignedEntity, @@ -53,4 +54,7 @@ it('exports types', async () => { const keyFinderFunc: KeyFinderFunc = fromPartial({}); expect(keyFinderFunc).toBeDefined(); + + const verificationPolicy: VerificationPolicy = fromPartial({}); + expect(verificationPolicy).toBeDefined(); }); diff --git a/packages/verify/src/__tests__/key/index.test.ts b/packages/verify/src/__tests__/key/index.test.ts index 37dfa3c5..b2782bdb 100644 --- a/packages/verify/src/__tests__/key/index.test.ts +++ b/packages/verify/src/__tests__/key/index.test.ts @@ -135,7 +135,7 @@ describe('verifyCertificate', () => { expect(result.signer).toBeDefined(); expect(result.signer.identity).toBeDefined(); expect(result.signer.identity?.subjectAlternativeName).toBeDefined(); - expect(result.signer.identity?.extensions.issuer).toEqual( + expect(result.signer.identity?.extensions?.issuer).toEqual( 'https://github.com/login/oauth' ); expect(result.signer.key).toBeDefined(); diff --git a/packages/verify/src/__tests__/policty.test.ts b/packages/verify/src/__tests__/policty.test.ts index 59e2b260..209e6ae1 100644 --- a/packages/verify/src/__tests__/policty.test.ts +++ b/packages/verify/src/__tests__/policty.test.ts @@ -28,6 +28,14 @@ describe('verifySubjectAlternativeName', () => { describe('verifyExtensions', () => { describe('when the signer extensions are undefined', () => { + it('throws an error', () => { + expect(() => + verifyExtensions({ issuer: 'foo' }, undefined) + ).toThrowWithCode(PolicyError, 'UNTRUSTED_SIGNER_ERROR'); + }); + }); + + describe('when the signer extension values are undefined', () => { it('throws an error', () => { expect(() => verifyExtensions({ issuer: 'foo' }, { issuer: undefined }) diff --git a/packages/verify/src/index.ts b/packages/verify/src/index.ts index 6e206c3a..41f6bd5f 100644 --- a/packages/verify/src/index.ts +++ b/packages/verify/src/index.ts @@ -18,4 +18,4 @@ export { PolicyError, VerificationError } from './error'; export { KeyFinderFunc, TrustMaterial, toTrustMaterial } from './trust'; export { Verifier, VerifierOptions } from './verifier'; -export type { SignedEntity, Signer } from './shared.types'; +export type { SignedEntity, Signer, VerificationPolicy } from './shared.types'; diff --git a/packages/verify/src/policy.ts b/packages/verify/src/policy.ts index 4cda4bed..39b08fa4 100644 --- a/packages/verify/src/policy.ts +++ b/packages/verify/src/policy.ts @@ -15,16 +15,14 @@ export function verifySubjectAlternativeName( export function verifyExtensions( policyExtensions: CertificateExtensions, - signerExtensions: CertificateExtensions + signerExtensions: CertificateExtensions = {} ): void { - if (policyExtensions.issuer) { - const policyIssuer = policyExtensions.issuer; - const signerIssuer = signerExtensions.issuer; - - if (signerIssuer === undefined || signerIssuer !== policyIssuer) { + let key: keyof typeof policyExtensions; + for (key in policyExtensions) { + if (signerExtensions[key] !== policyExtensions[key]) { throw new PolicyError({ code: 'UNTRUSTED_SIGNER_ERROR', - message: `invalid certificate issuer - expected ${policyIssuer}, got ${signerIssuer}`, + message: `invalid certificate extension - expected ${key}=${policyExtensions[key]}, got ${key}=${signerExtensions[key]}`, }); } } diff --git a/packages/verify/src/shared.types.ts b/packages/verify/src/shared.types.ts index 29c4524e..2bb4e605 100644 --- a/packages/verify/src/shared.types.ts +++ b/packages/verify/src/shared.types.ts @@ -16,15 +16,18 @@ limitations under the License. import type { TransparencyLogEntry } from '@sigstore/bundle'; import type { X509Certificate, crypto } from '@sigstore/core'; +export type CertificateExtensionName = 'issuer'; export type CertificateExtensions = { - issuer?: string; + [key in CertificateExtensionName]?: string; }; export type CertificateIdentity = { subjectAlternativeName?: string; - extensions: CertificateExtensions; + extensions?: CertificateExtensions; }; +export type VerificationPolicy = CertificateIdentity; + export type Signer = { key: crypto.KeyObject; identity?: CertificateIdentity; diff --git a/packages/verify/src/verifier.ts b/packages/verify/src/verifier.ts index 7a2f07e4..ee275c4b 100644 --- a/packages/verify/src/verifier.ts +++ b/packages/verify/src/verifier.ts @@ -14,13 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ import { isDeepStrictEqual } from 'util'; -import { PolicyError, VerificationError } from './error'; +import { VerificationError } from './error'; import { verifyCertificate, verifyPublicKey } from './key'; import { verifyExtensions, verifySubjectAlternativeName } from './policy'; import { verifyTLogTimestamp, verifyTSATimestamp } from './timestamp'; import { verifyTLogBody } from './tlog'; -import type { CertificateIdentity, SignedEntity, Signer } from './shared.types'; +import type { + CertificateIdentity, + SignedEntity, + Signer, + VerificationPolicy, +} from './shared.types'; import type { TrustMaterial } from './trust'; export type VerifierOptions = { @@ -42,17 +47,14 @@ export class Verifier { }; } - public verify( - entity: SignedEntity, - policy?: Required - ): Signer { + public verify(entity: SignedEntity, policy?: VerificationPolicy): Signer { const timestamps = this.verifyTimestamps(entity); const signer = this.verifySigningKey(entity, timestamps); this.verifyTLogs(entity); this.verifySignature(entity, signer); if (policy) { - this.verifyPolicy(signer, policy); + this.verifyPolicy(policy, signer.identity || {}); } return signer; @@ -155,22 +157,22 @@ export class Verifier { } } - private verifyPolicy(signer: Signer, policy: Required) { - if (!signer.identity) { - throw new PolicyError({ - code: 'UNTRUSTED_SIGNER_ERROR', - message: 'no signer identity', - }); - } - + private verifyPolicy( + policy: VerificationPolicy, + identity: CertificateIdentity + ) { // Check the subject alternative name of the signer matches the policy - verifySubjectAlternativeName( - policy.subjectAlternativeName, - signer.identity.subjectAlternativeName - ); + if (policy.subjectAlternativeName) { + verifySubjectAlternativeName( + policy.subjectAlternativeName, + identity.subjectAlternativeName + ); + } // Check that the extensions of the signer match the policy - verifyExtensions(policy.extensions, signer.identity.extensions); + if (policy.extensions) { + verifyExtensions(policy.extensions, identity.extensions); + } } }