Skip to content

Commit

Permalink
configurable keys for mock services (#738)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <[email protected]>
  • Loading branch information
bdehamer authored Sep 8, 2023
1 parent e70c9e0 commit 7a4efe0
Show file tree
Hide file tree
Showing 18 changed files with 202 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-feet-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/mock': minor
---

Configurable keys for mock services
11 changes: 6 additions & 5 deletions packages/mock-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
TrustedRoot,
} from '@sigstore/protobuf-specs';
import { initializeTUFRepo, tufHandlers } from '@tufjs/repo-mock';
import crypto from 'crypto';
import crypto, { generateKeyPairSync } from 'crypto';
import express from 'express';

// TODO: Export these types from @sigstore/mock
Expand Down Expand Up @@ -47,21 +47,22 @@ export default class Server extends Command {
public async run(): Promise<void> {
const { flags } = await this.parse(Server);
const url = `http://localhost:${flags.port}`;
const keyPair = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });

// Set-up the fake Fuclio server
const ctlog = await initializeCTLog();
const ca = await initializeCA(ctlog);
const ctlog = await initializeCTLog(keyPair);
const ca = await initializeCA(keyPair, ctlog);
const fulcio = fulcioHandler(ca, {
strict: flags['strict'],
subjectClaim: 'email',
});

// Set-up the fake Rekor server
const tlog = await initializeTLog(url);
const tlog = await initializeTLog(url, keyPair);
const rekor = rekorHandler(tlog, { strict: flags['strict'] });

// Set-up the fake TSA server
const tsa = await initializeTSA();
const tsa = await initializeTSA(keyPair);
const timestamp = tsaHandler(tsa, { strict: flags['strict'] });

// Build the trusted root from the key material of the fake services
Expand Down
2 changes: 1 addition & 1 deletion packages/mock/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.
export const DIGEST_SHA256 = 'SHA-256';

export const KEY_ALGORITHM_ECDSA_P256 = { name: 'ECDSA', namedCurve: 'P-256' };
export const KEY_ALGORITHM_ECDSA_P384 = { name: 'ECDSA', namedCurve: 'P-384' };

export const SIGNING_ALGORITHM_ECDSA_SHA256 = {
name: 'ECDSA',
hash: 'SHA-256',
Expand Down
23 changes: 11 additions & 12 deletions packages/mock/src/fulcio/ca.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ limitations under the License.

/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Crypto } from '@peculiar/webcrypto';
import { generateKeyPairSync } from 'crypto';
import * as pkijs from 'pkijs';
import { generateKeyPair } from '../util/key';
import { initializeCA } from './ca';
import { initializeCTLog } from './ctlog';

describe('CA', () => {
const rootKeyPair = generateKeyPair();
const crypto = new pkijs.CryptoEngine({ crypto: new Crypto() });

beforeEach(() => {
Expand All @@ -37,7 +38,7 @@ describe('CA', () => {

describe('#rootCertificate', () => {
it('returns the root certificate', async () => {
const ca = await initializeCA();
const ca = await initializeCA(rootKeyPair);
const root = ca.rootCertificate;

const cert = pkijs.Certificate.fromBER(root);
Expand All @@ -51,15 +52,13 @@ describe('CA', () => {
});

describe('#issueCertificate', () => {
const { publicKey } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
});
const { publicKey } = generateKeyPair('P-256');

const keyBytes = publicKey.export({ format: 'der', type: 'spki' });

describe('when no CT log is provided', () => {
it('issues a cert', async () => {
const ca = await initializeCA();
const ca = await initializeCA(rootKeyPair);

// Issue a certificate with the key above
const signingCert = await ca.issueCertificate({
Expand All @@ -78,7 +77,7 @@ describe('CA', () => {
});

it('issue a cert with the correct key', async () => {
const ca = await initializeCA();
const ca = await initializeCA(rootKeyPair);

// Issue a certificate with the key above
const signingCert = await ca.issueCertificate({
Expand All @@ -96,7 +95,7 @@ describe('CA', () => {
});

it('issue a cert with the correct SAN', async () => {
const ca = await initializeCA();
const ca = await initializeCA(rootKeyPair);

// Issue a certificate with the key above
const signingCert = await ca.issueCertificate({
Expand All @@ -116,7 +115,7 @@ describe('CA', () => {
});

it('issue a cert chained back to the root cert', async () => {
const ca = await initializeCA();
const ca = await initializeCA(rootKeyPair);

// Issue a certificate with the key above
const signingCert = await ca.issueCertificate({
Expand All @@ -141,7 +140,7 @@ describe('CA', () => {

describe('when a custom extension is provided', () => {
it('issues a cert with the extension', async () => {
const ca = await initializeCA();
const ca = await initializeCA(rootKeyPair);

// Issue a certificate with the key above
const signingCert = await ca.issueCertificate({
Expand All @@ -163,8 +162,8 @@ describe('CA', () => {

describe('when a CT log is provided', () => {
it('issues a cert with a verifiable SCT', async () => {
const ctLog = await initializeCTLog();
const ca = await initializeCA(ctLog);
const ctLog = await initializeCTLog(rootKeyPair);
const ca = await initializeCA(rootKeyPair, ctLog);

// Issue a certificate with the key above
const signingCert = await ca.issueCertificate({
Expand Down
15 changes: 12 additions & 3 deletions packages/mock/src/fulcio/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ limitations under the License.
import { Crypto } from '@peculiar/webcrypto';
import x509 from '@peculiar/x509';
import * as asn1js from 'asn1js';
import type { KeyPairKeyObjectResult } from 'crypto';
import {
DIGEST_SHA256,
KEY_ALGORITHM_ECDSA_P256,
KEY_ALGORITHM_ECDSA_P384,
SIGNING_ALGORITHM_ECDSA_SHA384,
} from '../constants';
import { keyObjectToCryptoKey } from '../util/key';
import { createRootCertificate } from '../util/root-cert';
import type { CTLog } from './ctlog';

Expand Down Expand Up @@ -53,10 +54,18 @@ interface ExtensionValue {
// the #issueCertificate method. Doing this work in a function allows us to
// work around the fact that some of these operations are async and can't be
// done in the constructor.
export async function initializeCA(ctLog?: CTLog): Promise<CA> {
export async function initializeCA(
keyPair: KeyPairKeyObjectResult,
ctLog?: CTLog
): Promise<CA> {
const cryptoKeyPair = {
privateKey: await keyObjectToCryptoKey(keyPair.privateKey),
publicKey: await keyObjectToCryptoKey(keyPair.publicKey),
};

const root = await createRootCertificate(
ISSUER,
KEY_ALGORITHM_ECDSA_P384,
cryptoKeyPair,
SIGNING_ALGORITHM_ECDSA_SHA384
);

Expand Down
10 changes: 6 additions & 4 deletions packages/mock/src/fulcio/ctlog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,33 @@ limitations under the License.
*/

import crypto from 'crypto';
import { generateKeyPair } from '../util/key';
import { CTLog, initializeCTLog } from './ctlog';

describe('CTLog', () => {
const keyPair = generateKeyPair();
describe('initializeCTLog', () => {
it('returns a CTLog', async () => {
const ctLog: CTLog = await initializeCTLog();
const ctLog: CTLog = await initializeCTLog(keyPair);
expect(ctLog).toBeDefined();
});
});

describe('#publicKey', () => {
it('returns a key', async () => {
const ctLog: CTLog = await initializeCTLog();
const ctLog: CTLog = await initializeCTLog(keyPair);
expect(ctLog.publicKey).toBeDefined();
});
});

describe('#logID', () => {
it('returns the log ID', async () => {
const ctLog: CTLog = await initializeCTLog();
const ctLog: CTLog = await initializeCTLog(keyPair);
expect(ctLog.logID.toString('hex')).toMatch(/^[0-9a-f]{64}$/);
});

it('returns the log ID matching the public key', async () => {
const ctLog: CTLog = await initializeCTLog();
const ctLog: CTLog = await initializeCTLog(keyPair);

const logID = ctLog.logID.toString('hex');
const keyDigest = crypto
Expand Down
20 changes: 9 additions & 11 deletions packages/mock/src/fulcio/ctlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ limitations under the License.
import { Crypto, CryptoKey } from '@peculiar/webcrypto';
import * as asn1js from 'asn1js';
import * as bs from 'bytestreamjs';
import type { KeyPairKeyObjectResult } from 'crypto';
import * as pkijs from 'pkijs';
import * as pvutils from 'pvutils';
import {
DIGEST_SHA256,
KEY_ALGORITHM_ECDSA_P256,
SIGNING_ALGORITHM_ECDSA_SHA256,
} from '../constants';
import { DIGEST_SHA256, SIGNING_ALGORITHM_ECDSA_SHA256 } from '../constants';
import { keyObjectToCryptoKey } from '../util/key';

export interface CTLog {
publicKey: Buffer;
Expand All @@ -34,14 +32,14 @@ export interface CTLog {
) => Promise<ArrayBuffer>;
}

export async function initializeCTLog(): Promise<CTLog> {
export async function initializeCTLog(
keyPair: KeyPairKeyObjectResult
): Promise<CTLog> {
const crypto = new Crypto();

const { publicKey, privateKey } = await crypto.subtle.generateKey(
KEY_ALGORITHM_ECDSA_P256,
true,
['sign', 'verify']
);
// Need to translate between the Node.js crypto API and the WebCrypto API
const privateKey = await keyObjectToCryptoKey(keyPair.privateKey);
const publicKey = await keyObjectToCryptoKey(keyPair.publicKey);

const publicKeyBytes = await crypto.subtle
.exportKey('spki', publicKey)
Expand Down
9 changes: 6 additions & 3 deletions packages/mock/src/fulcio/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,24 @@ limitations under the License.
*/

import { generateKeyPairSync } from 'crypto';
import { generateKeyPair } from '../util/key';
import { CA, initializeCA } from './ca';
import { fulcioHandler } from './handler';

describe('fulcioHandler', () => {
const keyPair = generateKeyPair('prime256v1');

describe('#path', () => {
it('returns the correct path', async () => {
const ca = await initializeCA();
const ca = await initializeCA(keyPair);
const handler = fulcioHandler(ca);
expect(handler.path).toBe('/api/v2/signingCert');
});
});

describe('#fn', () => {
it('returns a function', async () => {
const ca = await initializeCA();
const ca = await initializeCA(keyPair);
const handler = fulcioHandler(ca);
expect(handler.fn).toBeInstanceOf(Function);
});
Expand Down Expand Up @@ -58,7 +61,7 @@ describe('fulcioHandler', () => {
};

it('returns a certificate chain', async () => {
const ca = await initializeCA();
const ca = await initializeCA(keyPair);
const { fn } = fulcioHandler(ca);

// Make a request
Expand Down
16 changes: 12 additions & 4 deletions packages/mock/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import type { KeyPairKeyObjectResult } from 'crypto';
import { fulcioHandler, initializeCA, initializeCTLog } from './fulcio';
import { mock } from './mock';
import { initializeTLog, rekorHandler } from './rekor';
import { initializeTSA, tsaHandler } from './timestamp';
import { generateKeyPair } from './util/key';

const DEFAULT_FULCIO_URL = 'https://fulcio.sigstore.dev';
const DEFAULT_REKOR_URL = 'https://rekor.sigstore.dev';
Expand All @@ -26,31 +28,36 @@ const DEFAULT_TSA_URL = 'https://timestamp.sigstore.dev';
interface FulcioOptions {
baseURL?: string;
strict?: boolean;
keyPair?: KeyPairKeyObjectResult;
}

interface RekorOptions {
baseURL?: string;
strict?: boolean;
keyPair?: KeyPairKeyObjectResult;
}

interface TSAOptions {
baseURL?: string;
strict?: boolean;
keyPair?: KeyPairKeyObjectResult;
}

export async function mockFulcio(options: FulcioOptions = {}) {
const url = options.baseURL || DEFAULT_FULCIO_URL;
const strict = options.strict ?? true;
const handler = await initializeCTLog()
.then((ctlog) => initializeCA(ctlog))
const keyPair = options.keyPair || generateKeyPair('prime256v1');
const handler = await initializeCTLog(keyPair)
.then((ctlog) => initializeCA(keyPair, ctlog))
.then((ca) => fulcioHandler(ca, { strict }));
mock(url, handler);
}

export async function mockRekor(options: RekorOptions = {}) {
const url = options.baseURL || DEFAULT_REKOR_URL;
const strict = options.strict ?? true;
const handler = await initializeTLog(url).then((tlog) =>
const keyPair = options.keyPair || generateKeyPair('prime256v1');
const handler = await initializeTLog(url, keyPair).then((tlog) =>
rekorHandler(tlog, { strict })
);
mock(url, handler);
Expand All @@ -59,7 +66,8 @@ export async function mockRekor(options: RekorOptions = {}) {
export async function mockTSA(options: TSAOptions = {}) {
const url = options.baseURL || DEFAULT_TSA_URL;
const strict = options.strict ?? true;
const handler = await initializeTSA().then((tsa) =>
const keyPair = options.keyPair || generateKeyPair('prime256v1');
const handler = await initializeTSA(keyPair).then((tsa) =>
tsaHandler(tsa, { strict })
);
mock(url, handler);
Expand Down
8 changes: 5 additions & 3 deletions packages/mock/src/rekor/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,25 @@ limitations under the License.

import { fromPartial } from '@total-typescript/shoehorn';
import crypto from 'crypto';
import { generateKeyPair } from '../util/key';
import { rekorHandler } from './handler';
import { initializeTLog, TLog } from './tlog';

describe('rekorHandler', () => {
const url = 'https://rekor.sigstore.dev';
const keyPair = generateKeyPair();

describe('#path', () => {
it('returns the correct path', async () => {
const tlog = await initializeTLog(url);
const tlog = await initializeTLog(url, keyPair);
const handler = rekorHandler(tlog);
expect(handler.path).toBe('/api/v1/log/entries');
});
});

describe('#fn', () => {
it('returns a function', async () => {
const tlog = await initializeTLog(url);
const tlog = await initializeTLog(url, keyPair);
const handler = rekorHandler(tlog);
expect(handler.fn).toBeInstanceOf(Function);
});
Expand All @@ -44,7 +46,7 @@ describe('rekorHandler', () => {
};

it('returns a tlog entry', async () => {
const tlog = await initializeTLog(url);
const tlog = await initializeTLog(url, keyPair);
const { fn } = rekorHandler(tlog);

const resp = await fn(JSON.stringify(proposedEntry));
Expand Down
Loading

0 comments on commit 7a4efe0

Please sign in to comment.