Skip to content

Commit

Permalink
use fulcio v2 api (#359)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <bdehamer@github.com>
  • Loading branch information
bdehamer committed Mar 20, 2023
1 parent 5edbc7d commit 8f9f994
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 126 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-ties-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sigstore': patch
---

Use the Fulcio v2 API for requesting signing certificates
13 changes: 9 additions & 4 deletions src/__tests__/ca/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ import * as crypto from 'crypto';
import { toCertificateRequest } from '../../ca/format';

describe('toCertificateRequest', () => {
const identityToken = 'a.b.c';
const key = crypto.generateKeyPairSync('ec', {
namedCurve: 'P-256',
}).publicKey;
const challenge = Buffer.from('challenge');

it('returns a CertificateRequest', () => {
const cr = toCertificateRequest(key, challenge);
const cr = toCertificateRequest(identityToken, key, challenge);

expect(cr.signedEmailAddress).toEqual(challenge.toString('base64'));
expect(cr.publicKey.content).toEqual(
key.export({ type: 'spki', format: 'der' }).toString('base64')
expect(cr.credentials.oidcIdentityToken).toEqual(identityToken);
expect(cr.publicKeyRequest.publicKey.algorithm).toEqual('ECDSA');
expect(cr.publicKeyRequest.publicKey.content).toEqual(
key.export({ type: 'spki', format: 'pem' }).toString('ascii')
);
expect(cr.publicKeyRequest.proofOfPossession).toEqual(
challenge.toString('base64')
);
});
});
28 changes: 18 additions & 10 deletions src/__tests__/ca/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('CAClient', () => {

const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n`;
const rootCertificate = `-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----\n`;
const certChain = [leafCertificate, rootCertificate].join('');
const certChain = [leafCertificate, rootCertificate];

// Request data
const identityToken = 'a.b.c';
Expand All @@ -46,24 +46,32 @@ tbn02XdfIl+ZhQqUZv88dgDB86bfKyoOokA7fagAEOulkquhKKoOxdOySQ==
const challenge = Buffer.from('challenge');

const certRequest = {
publicKey: {
content: publicKey
.export({ type: 'spki', format: 'der' })
.toString('base64'),
credentials: {
oidcIdentityToken: identityToken,
},
publicKeyRequest: {
publicKey: {
algorithm: 'ECDSA',
content: publicKey
.export({ type: 'spki', format: 'pem' })
.toString('ascii'),
},
proofOfPossession: challenge.toString('base64'),
},
signedEmailAddress: challenge.toString('base64'),
};

describe('when Fulcio returns a valid response', () => {
beforeEach(() => {
// Mock Fulcio request
nock(baseURL)
.matchHeader('Accept', 'application/pem-certificate-chain')
.matchHeader('Content-Type', 'application/json')
.matchHeader('Authorization', `Bearer ${identityToken}`)
.matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+'))
.post('/api/v1/signingCert', certRequest)
.reply(201, certChain);
.post('/api/v2/signingCert', certRequest)
.reply(201, {
signedCertificateEmbeddedSct: {
chain: { certificates: certChain },
},
});
});

it('returns the certificate chain', async () => {
Expand Down
49 changes: 31 additions & 18 deletions src/__tests__/client/fulcio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ limitations under the License.
*/
import nock from 'nock';

import { Fulcio } from '../../client/fulcio';
import {
Fulcio,
SigningCertificateRequest,
SigningCertificateResponse,
} from '../../client/fulcio';

describe('Fulcio', () => {
const baseURL = 'http://localhost:8000';
Expand All @@ -28,32 +32,41 @@ describe('Fulcio', () => {
describe('#createSigningCertificate', () => {
const identityToken = `a.b.c`;
const certRequest = {
publicKey: {
content:
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==',
credentials: {
oidcIdentityToken: identityToken,
},
signedEmailAddress: 'MEUCIEntw6QwoyDHb52HUIUVDnqFeGBI4oaCBMCoOtcbVKQ=',
};
publicKeyRequest: {
publicKey: {
algorithm: 'ECDSA',
content:
'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==',
},
proofOfPossession: 'MEUCIEntw6QwoyDHb52HUIUVDnqFeGBI4oaCBMCoOtcbVKQ=',
},
} satisfies SigningCertificateRequest;

describe('when the certificate request is valid', () => {
const certificate = `-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----`;
const certificateResponse: SigningCertificateResponse = {
signedCertificateEmbeddedSct: {
chain: {
certificates: [
`-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----`,
],
},
},
};

beforeEach(() => {
nock(baseURL)
.matchHeader('Accept', 'application/pem-certificate-chain')
.matchHeader('Content-Type', 'application/json')
.matchHeader('Authorization', `Bearer ${identityToken}`)
.matchHeader('User-Agent', new RegExp('sigstore-js\\/\\d+.\\d+.\\d+'))
.post('/api/v1/signingCert', certRequest)
.reply(200, certificate);
.post('/api/v2/signingCert', certRequest)
.reply(200, certificateResponse);
});

it('returns the signing certificate', async () => {
const result = await subject.createSigningCertificate(
identityToken,
certRequest
);
expect(result).toBe(certificate);
const result = await subject.createSigningCertificate(certRequest);
expect(result).toEqual(certificateResponse);
});
});

Expand All @@ -64,12 +77,12 @@ describe('Fulcio', () => {
};

beforeEach(() => {
nock(baseURL).post('/api/v1/signingCert').reply(400, responseBody);
nock(baseURL).post('/api/v2/signingCert').reply(400, responseBody);
});

it('returns an error', async () => {
await expect(
subject.createSigningCertificate(identityToken, certRequest)
subject.createSigningCertificate(certRequest)
).rejects.toThrow('HTTP Error: 400 Bad Request');
});
});
Expand Down
47 changes: 33 additions & 14 deletions src/__tests__/sign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,34 @@ describe('Signer', () => {
// Fulcio output
const leafCertificate = `-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----`;
const rootCertificate = `-----BEGIN CERTIFICATE-----\nxyz\n-----END CERTIFICATE-----`;
const certificate = [leafCertificate, rootCertificate].join('\n');

beforeEach(() => {
// Mock Fulcio request
nock(fulcioBaseURL)
.matchHeader('Accept', 'application/pem-certificate-chain')
.matchHeader('Content-Type', 'application/json')
.matchHeader('Authorization', `Bearer ${jwt}`)
.post('/api/v1/signingCert', {
publicKey: { content: /.+/i },
signedEmailAddress: /.+/i,
.post('/api/v2/signingCert', {
credentials: {
oidcIdentityToken: jwt,
},
publicKeyRequest: {
publicKey: {
algorithm: 'ECDSA',
content: /.+/i,
},
proofOfPossession: /.+/i,
},
})
.reply(200, certificate);
.reply(200, {
signedCertificateEmbeddedSct: {
chain: { certificates: [leafCertificate, rootCertificate] },
},
});
});

describe('when Rekor returns successfully', () => {
// Rekor output
const signature = 'ABC123';
const b64Cert = Buffer.from(certificate).toString('base64');
const b64Cert = Buffer.from(leafCertificate).toString('base64');
const uuid =
'69e5a0c1663ee4452674a5c9d5050d866c2ee31e2faaf79913aea7cc27293cf6';

Expand Down Expand Up @@ -270,14 +279,24 @@ describe('Signer', () => {
beforeEach(() => {
// Mock Fulcio request
nock(fulcioBaseURL)
.matchHeader('Accept', 'application/pem-certificate-chain')
.matchHeader('Content-Type', 'application/json')
.matchHeader('Authorization', `Bearer ${jwt}`)
.post('/api/v1/signingCert', {
publicKey: { content: /.+/i },
signedEmailAddress: /.+/i,
.post('/api/v2/signingCert', {
credentials: {
oidcIdentityToken: jwt,
},
publicKeyRequest: {
publicKey: {
algorithm: 'ECDSA',
content: /.+/i,
},
proofOfPossession: /.+/i,
},
})
.reply(200, certificate);
.reply(200, {
signedCertificateEmbeddedSct: {
chain: { certificates: [certificate] },
},
});
});

describe('when Rekor returns successfully', () => {
Expand Down
34 changes: 3 additions & 31 deletions src/__tests__/util/pem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { fromDER, split, toDER } from '../../util/pem';
import { fromDER, toDER } from '../../util/pem';

describe('pem', () => {
const cert1 =
'-----BEGIN CERTIFICATE-----\nABCD\n-----END CERTIFICATE-----\n';
const cert2 =
'-----BEGIN CERTIFICATE-----\nDEFG\n-----END CERTIFICATE-----\n';

describe('split', () => {
describe('when certificate is empty', () => {
it('should return empty array', () => {
expect(split('')).toEqual([]);
});
});

describe('when given a certificate with a single certificate', () => {
it('returns an array with a single certificate', () => {
expect(split(cert1)).toEqual([cert1]);
});
});

describe('when given a certificate with multiple certificates', () => {
it('returns an array with all the certificates', () => {
const certs = split([cert1, cert2].join('\n'));

expect(certs).toHaveLength(2);
expect(certs[0]).toEqual(cert1);
expect(certs[1]).toEqual(cert2);
});
});
});

describe('toDER', () => {
describe('when the object is a certificate', () => {
const pem = cert1;
const pem =
'-----BEGIN CERTIFICATE-----\nABCD\n-----END CERTIFICATE-----\n';
it('returns a DER-encoded certificate', () => {
const der = toDER(pem);
expect(der).toEqual(Buffer.from('ABCD', 'base64'));
Expand Down
21 changes: 14 additions & 7 deletions src/ca/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { KeyObject } from 'crypto';
import { CertificateRequest } from '../client/fulcio';
import { SigningCertificateRequest } from '../client/fulcio';

export function toCertificateRequest(
identityToken: string,
publicKey: KeyObject,
challenge: Buffer
): CertificateRequest {
): SigningCertificateRequest {
return {
publicKey: {
content: publicKey
.export({ type: 'spki', format: 'der' })
.toString('base64'),
credentials: {
oidcIdentityToken: identityToken,
},
publicKeyRequest: {
publicKey: {
algorithm: 'ECDSA',
content: publicKey
.export({ format: 'pem', type: 'spki' })
.toString('ascii'),
},
proofOfPossession: challenge.toString('base64'),
},
signedEmailAddress: challenge.toString('base64'),
};
}
10 changes: 3 additions & 7 deletions src/ca/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.
import { KeyObject } from 'crypto';
import { Fulcio } from '../client';
import { InternalError } from '../error';
import { pem } from '../util';
import { toCertificateRequest } from './format';

export interface CA {
Expand All @@ -43,15 +42,12 @@ export class CAClient implements CA {
publicKey: KeyObject,
challenge: Buffer
): Promise<string[]> {
const request = toCertificateRequest(publicKey, challenge);
const request = toCertificateRequest(identityToken, publicKey, challenge);

try {
const certificate = await this.fulcio.createSigningCertificate(
identityToken,
request
);
const certificate = await this.fulcio.createSigningCertificate(request);

return pem.split(certificate);
return certificate.signedCertificateEmbeddedSct.chain.certificates;
} catch (err) {
throw new InternalError('error creating signing certificate', err);
}
Expand Down

0 comments on commit 8f9f994

Please sign in to comment.