Skip to content

Commit

Permalink
add verification policy support to client package (#893)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <[email protected]>
  • Loading branch information
bdehamer authored Dec 8, 2023
1 parent 34ed08e commit bf1d432
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-keys-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/verify': minor
---

Export `VerificationPolicy` type
2 changes: 2 additions & 0 deletions .changeset/gold-walls-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
46 changes: 45 additions & 1 deletion packages/client/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -91,3 +95,43 @@ describe('createKeyFinder', () => {
});
});
});

describe('createVerificationPolicy', () => {
describe('when the options specify a certificateIdentityEmail', () => {
const options = { certificateIdentityEmail: '[email protected]' };

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();
});
});
});
1 change: 1 addition & 0 deletions packages/client/src/__tests__/sigstore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ describe('#verify', () => {
tufOptions = {
tufMirrorURL: tufRepo.baseURL,
tufCachePath: tufRepo.cachePath,
certificateIssuer: 'https://github.com/login/oauth',
};
});

Expand Down
24 changes: 23 additions & 1 deletion packages/client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/sigstore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
};
Expand Down
4 changes: 4 additions & 0 deletions packages/verify/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Signer,
TrustMaterial,
VerificationError,
VerificationPolicy,
Verifier,
VerifierOptions,
toSignedEntity,
Expand Down Expand Up @@ -53,4 +54,7 @@ it('exports types', async () => {

const keyFinderFunc: KeyFinderFunc = fromPartial({});
expect(keyFinderFunc).toBeDefined();

const verificationPolicy: VerificationPolicy = fromPartial({});
expect(verificationPolicy).toBeDefined();
});
2 changes: 1 addition & 1 deletion packages/verify/src/__tests__/key/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions packages/verify/src/__tests__/policty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
2 changes: 1 addition & 1 deletion packages/verify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
12 changes: 5 additions & 7 deletions packages/verify/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]}`,
});
}
}
Expand Down
7 changes: 5 additions & 2 deletions packages/verify/src/shared.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 22 additions & 20 deletions packages/verify/src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -42,17 +47,14 @@ export class Verifier {
};
}

public verify(
entity: SignedEntity,
policy?: Required<CertificateIdentity>
): 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;
Expand Down Expand Up @@ -155,22 +157,22 @@ export class Verifier {
}
}

private verifyPolicy(signer: Signer, policy: Required<CertificateIdentity>) {
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);
}
}
}

Expand Down

0 comments on commit bf1d432

Please sign in to comment.