Skip to content

Commit

Permalink
add int cert for mock tsa (#908)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <bdehamer@github.com>
  • Loading branch information
bdehamer authored Dec 20, 2023
1 parent 8cbcd04 commit 123389f
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/nine-donuts-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sigstore/mock": patch
---

Introduce intermediate certificate for issuing RFC3161 timestamps
5 changes: 5 additions & 0 deletions .changeset/tricky-owls-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sigstore/mock": patch
---

Fix encoding for TSA-issued timestamps
12 changes: 8 additions & 4 deletions packages/mock-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,17 @@ function assembleTrustedRoot({
}): TrustedRoot {
return {
mediaType: 'application/vnd.dev.sigstore.trustedroot+json;version=0.1',
certificateAuthorities: [certificateAuthority(ca.rootCertificate, url)],
certificateAuthorities: [certificateAuthority([ca.rootCertificate], url)],
ctlogs: [transparencyLogInstance(ctlog.publicKey, url)],
tlogs: [transparencyLogInstance(tlog.publicKey, url)],
timestampAuthorities: [certificateAuthority(tsa.rootCertificate, url)],
timestampAuthorities: [
certificateAuthority([tsa.rootCertificate, tsa.intCertificate], url),
],
};
}

function certificateAuthority(
certificate: Buffer,
certificates: Buffer[],
url: string
): CertificateAuthority {
return {
Expand All @@ -151,7 +153,9 @@ function certificateAuthority(
},
uri: url,
certChain: {
certificates: [{ rawBytes: certificate }],
certificates: certificates.map((certificate) => ({
rawBytes: certificate,
})),
},
validFor: { start: new Date() },
};
Expand Down
18 changes: 18 additions & 0 deletions packages/mock/src/timestamp/tsa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const OID_SHA256_ALGO_ID = '2.16.840.1.101.3.4.2.1';
describe('TSA', () => {
const keyPair = generateKeyPair('prime256v1');
const clock = new Date();

describe('rootCertificate', () => {
it('returns the root certificate', async () => {
const tsa = await initializeTSA(keyPair, clock);
Expand All @@ -39,6 +40,23 @@ describe('TSA', () => {
});
});

describe('intCertificate', () => {
it('returns the intermediate certificate', async () => {
const tsa = await initializeTSA(keyPair, clock);
const der = tsa.intCertificate;

expect(der).toBeDefined();

const cert = pkijs.Certificate.fromBER(der);
expect(cert.subject.typesAndValues[0].value.valueBlock.value).toBe(
'tsa signing'
);
expect(cert.subject.typesAndValues[1].value.valueBlock.value).toBe(
'sigstore.mock'
);
});
});

describe('#timestamp', () => {
const request: TimestampRequest = {
hashAlgorithmOID: OID_SHA256_ALGO_ID,
Expand Down
134 changes: 101 additions & 33 deletions packages/mock/src/timestamp/tsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,29 @@ import * as asn1js from 'asn1js';
import type { KeyPairKeyObjectResult } from 'crypto';
import * as pkijs from 'pkijs';
import { DIGEST_SHA256, SIGNING_ALGORITHM_ECDSA_SHA384 } from '../constants';
import { ESSCertIDv2 } from '../util/ess-cert-id';
import { keyObjectToCryptoKey } from '../util/key';
import { createRootCertificate } from '../util/root-cert';
import {
createIntermediateCertificate,
createRootCertificate,
} from '../util/root-cert';
import { SigningCertificateV2 } from '../util/signing-cert';

const ROOT_NAME = 'CN=tsa,O=sigstore.mock';
const INT_NAME = 'CN=tsa signing,O=sigstore.mock';

const SIGNED_DATA_DIGEST_ALGORITHM = DIGEST_SHA256;

const ISSUER = 'CN=tsa,O=sigstore.mock';
const OID_TSTINFO_CONTENT_TYPE = '1.2.840.113549.1.9.16.1.4';
const OID_SIGNED_DATA_CONTENT_TYPE = '1.2.840.113549.1.7.2';
const OID_PKCS9_CONTENT_TYPE_KEY = '1.2.840.113549.1.9.3';
const OID_PKCS9_SIGNING_TIME_KEY = '1.2.840.113549.1.9.5';
const OKD_PKCS9_MESSAGE_DIGEST_KEY = '1.2.840.113549.1.9.4';
const OID_PKCS9_SIGNING_CERTIFICATE_V2_KEY = '1.2.840.113549.1.9.16.2.47';

export interface TSA {
rootCertificate: Buffer;
intCertificate: Buffer;
timestamp: (req: TimestampRequest) => Promise<Buffer>;
}

Expand All @@ -47,31 +61,44 @@ export async function initializeTSA(
privateKey: await keyObjectToCryptoKey(keyPair.privateKey),
publicKey: await keyObjectToCryptoKey(keyPair.publicKey),
};

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

const int = await createIntermediateCertificate(
INT_NAME,
ROOT_NAME,
cryptoKeyPair,
SIGNING_ALGORITHM_ECDSA_SHA384
);

return new TSAImpl({
rootCertificate: pkijs.Certificate.fromBER(root.cert.rawData),
keyPair: root.keyPair,
intCertificate: pkijs.Certificate.fromBER(int.cert.rawData),
keyPair: cryptoKeyPair,
clock,
});
}

interface TSAOptions {
rootCertificate: pkijs.Certificate;
intCertificate: pkijs.Certificate;
keyPair: CryptoKeyPair;
clock?: Date;
}

class TSAImpl implements TSA {
private rootCert: pkijs.Certificate;
private intCert: pkijs.Certificate;
private keyPair: CryptoKeyPair;
private getCurrentTime: () => Date;
private crypto: pkijs.ICryptoEngine;
constructor(options: TSAOptions) {
this.rootCert = options.rootCertificate;
this.intCert = options.intCertificate;
this.keyPair = options.keyPair;
this.getCurrentTime = () => options.clock || new Date();
this.crypto = new pkijs.CryptoEngine({
Expand All @@ -83,6 +110,10 @@ class TSAImpl implements TSA {
return Buffer.from(this.rootCert.toSchema().toBER(false));
}

public get intCertificate(): Buffer {
return Buffer.from(this.intCert.toSchema().toBER(false));
}

// Create a timestamp according to
// https://www.rfc-editor.org/rfc/rfc3161.html
public async timestamp(req: TimestampRequest): Promise<Buffer> {
Expand Down Expand Up @@ -117,11 +148,12 @@ class TSAImpl implements TSA {
}),
hashedMessage: new asn1js.OctetString({ valueHex: req.artifactHash }),
}),
serialNumber: new asn1js.Integer({ value: 1 }),
serialNumber: new asn1js.Integer({
valueHex: Buffer.from('DEADBEEF', 'hex'),
}),
genTime: this.getCurrentTime(),
accuracy: new pkijs.Accuracy({ seconds: 1 }),
nonce: new asn1js.Integer({ value: req.nonce }),
tsa: generalName('tsa', 'sigstore.mock'),
});
}

Expand All @@ -130,30 +162,84 @@ class TSAImpl implements TSA {
tstInfo: pkijs.TSTInfo,
includeCerts: boolean
): Promise<pkijs.SignedData> {
// The isConstructed flag makes the encoding of the
// EncapsulatedContentInfo look more like what the real TSA returns,
// however, it results in a reponse that is not parseable by openssl.
// I guess we should leave it at the default but let's revisit this
// later if we run into verification problems.
const encapContent = new pkijs.EncapsulatedContentInfo({
eContentType: OID_TSTINFO_CONTENT_TYPE,
eContent: new asn1js.OctetString({
valueHex: tstInfo.toSchema().toBER(false),
// The isConstructed flag makes the encoding of the
// EncapsulatedContentInfo look more like what the real TSA returns,
// however, it results in a reponse that is not parseable by openssl.
// idBlock: { isConstructed: true },
}),
});

const tstInfoDigest = await this.crypto.digest(
SIGNED_DATA_DIGEST_ALGORITHM,
tstInfo.toSchema().toBER(false)
);

const signerDigest = await this.crypto.digest(
DIGEST_SHA256,
this.intCert.toSchema().toBER(false)
);

// Create the ESSCertIDv2 structure containing information about the
// signing certificate which issued the timestamp
const certID = new ESSCertIDv2({
certHash: new asn1js.OctetString({ valueHex: signerDigest }),
issuerSerial: new pkijs.IssuerSerial({
issuer: new pkijs.GeneralNames({
names: [
new pkijs.GeneralName({ type: 4, value: this.intCert.issuer }),
],
}),
serialNumber: this.intCert.serialNumber,
}),
});

const signingCert = new SigningCertificateV2({ certs: [certID] });

// Create the signed attributes, including:
// - contentType
// - signingTime
// - messageDigest (digest of the tstInfo structure)
// - signingCertificateV2
const signedAttrs = new pkijs.SignedAndUnsignedAttributes({
type: 0,
attributes: [
new pkijs.Attribute({
type: OID_PKCS9_CONTENT_TYPE_KEY,
values: [
new asn1js.ObjectIdentifier({ value: OID_TSTINFO_CONTENT_TYPE }),
],
}),
new pkijs.Attribute({
type: OID_PKCS9_SIGNING_TIME_KEY,
values: [new asn1js.UTCTime({ valueDate: this.getCurrentTime() })],
}),
new pkijs.Attribute({
type: OKD_PKCS9_MESSAGE_DIGEST_KEY,
values: [new asn1js.OctetString({ valueHex: tstInfoDigest })],
}),
new pkijs.Attribute({
type: OID_PKCS9_SIGNING_CERTIFICATE_V2_KEY,
values: [new asn1js.Sequence({ value: [signingCert.toSchema()] })],
}),
],
});

/* istanbul ignore next */
const signedData = new pkijs.SignedData({
version: 1,
encapContentInfo: encapContent,
certificates: includeCerts ? [this.rootCert] : undefined,
certificates: includeCerts ? [this.intCert] : undefined,
signerInfos: [
new pkijs.SignerInfo({
version: 1,
signedAttrs: signedAttrs,
sid: new pkijs.IssuerAndSerialNumber({
issuer: this.rootCert.issuer,
serialNumber: this.rootCert.serialNumber,
issuer: this.intCert.issuer,
serialNumber: this.intCert.serialNumber,
}),
}),
],
Expand All @@ -163,29 +249,11 @@ class TSAImpl implements TSA {
await signedData.sign(
this.keyPair.privateKey,
0,
DIGEST_SHA256,
SIGNED_DATA_DIGEST_ALGORITHM,
undefined,
this.crypto
);

return signedData;
}
}

function generalName(commonName: string, orgName: string): pkijs.GeneralName {
return new pkijs.GeneralName({
type: 4,
value: new pkijs.RelativeDistinguishedNames({
typesAndValues: [
new pkijs.AttributeTypeAndValue({
type: '2.5.4.10',
value: new asn1js.PrintableString({ value: orgName }),
}),
new pkijs.AttributeTypeAndValue({
type: '2.5.4.3',
value: new asn1js.PrintableString({ value: commonName }),
}),
],
}),
});
}
34 changes: 34 additions & 0 deletions packages/mock/src/util/ess-cert-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as asn1js from 'asn1js';
import * as pkijs from 'pkijs';
import { ESSCertIDv2 } from './ess-cert-id';

describe('ESSCertIDv2', () => {
describe('constructor', () => {
describe('when no parameters are provided', () => {
it('should set the certHash and issuerSerial to empty buffers', () => {
const essCertIDv2 = new ESSCertIDv2();
expect(essCertIDv2).toBeDefined();
});
});

describe('when parameters are provided', () => {
it('should set the certHash and issuerSerial', () => {
const certHash = new asn1js.OctetString({
valueHex: new ArrayBuffer(0),
});
const issuerSerial = new pkijs.IssuerSerial({
issuer: new pkijs.GeneralNames({
names: [
new pkijs.GeneralName({ type: 4, value: new ArrayBuffer(0) }),
],
}),
serialNumber: new asn1js.Integer({ value: 888 }),
});
const essCertIDv2 = new ESSCertIDv2({ certHash, issuerSerial });

expect(essCertIDv2.certHash).toBe(certHash);
expect(essCertIDv2.issuerSerial).toBe(issuerSerial);
});
});
});
});
54 changes: 54 additions & 0 deletions packages/mock/src/util/ess-cert-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as asn1js from 'asn1js';
import * as pkijs from 'pkijs';
import * as pvutils from 'pvutils';

const CERT_HASH = 'certHash';
const ISSUER_SERIAL = 'issuerSerial';

interface IESSCertIDv2 {
certHash: asn1js.OctetString;
issuerSerial: pkijs.IssuerSerial;
}

type ESSCertIDv2Parameters = pkijs.PkiObjectParameters & Partial<IESSCertIDv2>;

// https://datatracker.ietf.org/doc/html/rfc5035#section-4
export class ESSCertIDv2 extends pkijs.PkiObject implements IESSCertIDv2 {
public static override CLASS_NAME = 'ESSCertIDv2';

public certHash!: asn1js.OctetString;
public issuerSerial!: pkijs.IssuerSerial;

constructor(parameters: ESSCertIDv2Parameters = {}) {
super();

this.certHash = pvutils.getParametersValue(
parameters,
CERT_HASH,
new asn1js.OctetString()
);
this.issuerSerial = pvutils.getParametersValue(
parameters,
ISSUER_SERIAL,
new pkijs.IssuerSerial()
);
}

public override toSchema(): asn1js.Sequence {
const result = new asn1js.Sequence({
value: [this.certHash, this.issuerSerial.toSchema()],
});

return result;
}

/* istanbul ignore next */
public override fromSchema(): void {
throw new Error('Not implemented');
}

/* istanbul ignore next */
public override toJSON(): IESSCertIDv2 {
throw new Error('Not implemented');
}
}
Loading

0 comments on commit 123389f

Please sign in to comment.