Skip to content

Commit 697c604

Browse files
committed
Expose getCertificateTransparencyLogs for client setup
1 parent f7caef5 commit 697c604

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export type { ServerMockedEndpoint } from "./server/mocked-endpoint";
7373
// Export TLS utility methods:
7474
export {
7575
generateCACertificate,
76-
generateSPKIFingerprint
76+
generateSPKIFingerprint,
77+
getCertificateTransparencyLogs
7778
} from './util/certificates';
7879

7980
// Export various referenced utility types:

src/util/certificates.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,29 @@ export async function generateSPKIFingerprint(certPem: string): Promise<string>
297297
return Buffer.from(hashBuffer).toString('base64');
298298
}
299299

300+
/**
301+
* Derive the Certificate Transparency logs for a given CA certificate.
302+
*
303+
* When a CA is used with `certificateTransparency` enabled, Mockttp embeds SCTs
304+
* signed by two logs deterministically derived from that CA certificate. This
305+
* returns those logs' ids & public keys (SPKI, DER) - exactly matching the SCTs
306+
* embedded in generated certificates.
307+
*
308+
* Only the CA certificate is required: derivation is a pure function of the cert's
309+
* public key, independent of how the CA private key happens to be encoded.
310+
*/
311+
export function getCertificateTransparencyLogs(
312+
caCert: string
313+
): Array<{ logId: Buffer, publicKey: Buffer, usableSince: Date }> {
314+
const cert = new x509.X509Certificate(caCert);
315+
const usableSince = cert.notBefore;
316+
return deriveCTLogOperators(cert).map(op => ({
317+
logId: Buffer.from(op.logId),
318+
publicKey: Buffer.from(op.publicKey),
319+
usableSince
320+
}));
321+
}
322+
300323
// Generates a unique serial number for a certificate as a hex string:
301324
function generateSerialNumber() {
302325
return 'A' + crypto.randomUUID().replace(/-/g, '');

test/certificate-transparency.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as x509 from '@peculiar/x509';
66

77
import { HardenedHttpsAgent } from 'hardened-https-agent';
88

9-
import { getLocal } from '..';
9+
import { getLocal, getCertificateTransparencyLogs } from '..';
1010
import { expect, nodeOnly } from "./test-utils";
1111

1212
import { getCA, CA } from '../src/util/certificates';
@@ -111,6 +111,26 @@ nodeOnly(() => {
111111
expect(logs8[1].logId).to.deep.equal(logs1[1].logId);
112112
});
113113

114+
it("exposes the CA's CT logs publicly (from the cert alone), matching what is embedded", async () => {
115+
const cert = await caCert;
116+
const ca = await getCA({ key: await caKey, cert, certificateTransparency: true });
117+
118+
const published = getCertificateTransparencyLogs(cert);
119+
const embedded = ca.getCTLogDetails();
120+
121+
expect(published).to.have.length(2);
122+
expect(published[0].logId).to.have.length(32); // SHA-256 of the SPKI
123+
expect(published[0].logId).to.deep.equal(embedded[0].logId);
124+
expect(published[0].publicKey).to.deep.equal(embedded[0].publicKey);
125+
expect(published[1].logId).to.deep.equal(embedded[1].logId);
126+
expect(published[1].publicKey).to.deep.equal(embedded[1].publicKey);
127+
128+
// usableSince is the CA cert's notBefore (safe: <= every backdated SCT):
129+
const caNotBefore = new x509.X509Certificate(cert).notBefore;
130+
expect(published[0].usableSince.getTime()).to.equal(caNotBefore.getTime());
131+
expect(published[1].usableSince.getTime()).to.equal(caNotBefore.getTime());
132+
});
133+
114134
it("getCTLogDetails throws when CT is not enabled", async () => {
115135
const ca = await getCA({ key: await caKey, cert: await caCert });
116136
expect(() => ca.getCTLogDetails()).to.throw('CT not enabled');

0 commit comments

Comments
 (0)