Skip to content

Commit

Permalink
CSR support in Fulcio mock (#1321)
Browse files Browse the repository at this point in the history
Signed-off-by: Brian DeHamer <bdehamer@github.com>
Co-authored-by: Samuel Giddins <segiddins@segiddins.me>
  • Loading branch information
bdehamer and segiddins authored Nov 26, 2024
1 parent c2ca121 commit 8ec213e
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-comics-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/mock': minor
---

Update Fulcio mock with support for CSRs
85 changes: 84 additions & 1 deletion packages/mock/src/fulcio/__snapshots__/handler.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`fulcioHandler #fn when invoked returns a certificate chain 1`] = `
exports[`fulcioHandler #fn when invoked w/ a CSR returns a certificate chain 1`] = `
Extensions [
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.1
OCTET STRING : 687474703a2f2f666f6f2e636f6d",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.8
OCTET STRING :
UTF8String : 'http://foo.com'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.2
OCTET STRING : 776f726b666c6f775f6469737061746368",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.20
OCTET STRING :
UTF8String : 'workflow_dispatch'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.9
OCTET STRING :
UTF8String : 'https://github.com/foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.10
OCTET STRING :
UTF8String : 'ba214227977e57973d87219493ad93eeda9c7d6c'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.6
OCTET STRING : 726566732f68656164732f6d61696e",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.14
OCTET STRING :
UTF8String : 'refs/heads/main'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.5
OCTET STRING : 666f6f2f6174746573742d64656d6f",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.12
OCTET STRING :
UTF8String : 'https://github.com/foo/attest-demo'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.15
OCTET STRING :
UTF8String : '792829709'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.16
OCTET STRING :
UTF8String : 'https://github.com/foo'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.17
OCTET STRING :
UTF8String : '398027'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.22
OCTET STRING :
UTF8String : 'public'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.11
OCTET STRING :
UTF8String : 'github-hosted'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.3
OCTET STRING : 62613231343232373937376535373937336438373231393439336164393365656461396337643663",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.13
OCTET STRING :
UTF8String : 'ba214227977e57973d87219493ad93eeda9c7d6c'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.4
OCTET STRING : 4f494443",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.18
OCTET STRING :
UTF8String : 'https://github.com/foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.19
OCTET STRING :
UTF8String : 'ba214227977e57973d87219493ad93eeda9c7d6c'",
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.21
OCTET STRING :
UTF8String : 'https://github.com/foo/attest-demo/actions/runs/11997537386/attempts/3'",
]
`;

exports[`fulcioHandler #fn when invoked w/ a public key returns a certificate chain 1`] = `
Extensions [
"SEQUENCE :
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.1
Expand Down
121 changes: 95 additions & 26 deletions packages/mock/src/fulcio/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { Crypto } from '@peculiar/webcrypto';
import x509 from '@peculiar/x509';
import { generateKeyPairSync } from 'crypto';
import { generateKeyPair } from '../util/key';
Expand All @@ -32,39 +33,39 @@ describe('fulcioHandler', () => {
});

describe('#fn', () => {
const claims = {
sub: 'http://github.com/foo/workflow.yml@refs/heads/main',
iss: 'http://foo.com',
event_name: 'workflow_dispatch',
job_workflow_ref:
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
job_workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
ref: 'refs/heads/main',
repository: 'foo/attest-demo',
repository_id: '792829709',
repository_owner: 'foo',
repository_owner_id: '398027',
repository_visibility: 'public',
run_attempt: '3',
run_id: '11997537386',
runner_environment: 'github-hosted',
sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
workflow: 'OIDC',
workflow_ref:
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
};
const jwt = jwtify(claims);

it('returns a function', async () => {
const ca = await initializeCA(keyPair);
const handler = fulcioHandler(ca);
expect(handler.fn).toBeInstanceOf(Function);
});

describe('when invoked', () => {
describe('when invoked w/ a public key', () => {
const { publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });

const claims = {
sub: 'http://github.com/foo/workflow.yml@refs/heads/main',
iss: 'http://foo.com',
event_name: 'workflow_dispatch',
job_workflow_ref:
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
job_workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
ref: 'refs/heads/main',
repository: 'foo/attest-demo',
repository_id: '792829709',
repository_owner: 'foo',
repository_owner_id: '398027',
repository_visibility: 'public',
run_attempt: '3',
run_id: '11997537386',
runner_environment: 'github-hosted',
sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
workflow: 'OIDC',
workflow_ref:
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
};
const jwt = jwtify(claims);

const certRequest = {
credentials: {
oidcIdentityToken: jwt,
Expand Down Expand Up @@ -100,9 +101,14 @@ describe('fulcioHandler', () => {
certs.signedCertificateEmbeddedSct.chain.certificates
).toHaveLength(2);

const { extensions } = new x509.X509Certificate(
const { extensions, publicKey } = new x509.X509Certificate(
certs.signedCertificateEmbeddedSct.chain.certificates[0]
);

// Ensure public key matches input
expect(publicKey.toString('pem')).toEqual(
certRequest.publicKeyRequest.publicKey.content.trimEnd()
);
expect(
extensions
.filter((e) => e.type.startsWith('1.3.6.1.4.1.57264'))
Expand All @@ -125,6 +131,69 @@ describe('fulcioHandler', () => {
});
});
});

describe('when invoked w/ a CSR', () => {
it('returns a certificate chain', async () => {
const crypto = new Crypto();
const kp = await crypto.subtle.generateKey(
{ name: 'ecdsa', namedCurve: 'P-256' },
true,
['sign', 'verify']
);
const csr = await x509.Pkcs10CertificateRequestGenerator.create(
{
signingAlgorithm: {
name: 'ECDSA',
hash: 'SHA-256',
},
keys: kp,
},
crypto
);

const certRequest = {
credentials: {
oidcIdentityToken: jwt,
},
certificateSigningRequest: csr.toString('pem'),
};

const ca = await initializeCA(keyPair);
const { fn } = fulcioHandler(ca);

// Make a request
const resp = await fn(JSON.stringify(certRequest));
expect(resp.statusCode).toBe(201);

// Check the response
const certs = JSON.parse(resp.response.toString());
expect(certs).toBeDefined();
expect(certs.signedCertificateEmbeddedSct).toBeDefined();
expect(certs.signedCertificateEmbeddedSct.chain).toBeDefined();
expect(
certs.signedCertificateEmbeddedSct.chain.certificates
).toBeDefined();
expect(
certs.signedCertificateEmbeddedSct.chain.certificates
).toHaveLength(2);

const { extensions, publicKey } = new x509.X509Certificate(
certs.signedCertificateEmbeddedSct.chain.certificates[0]
);

// Ensure public key matches the CSR
const expectedKey = await crypto.subtle.exportKey('spki', kp.publicKey);
expect(publicKey.toString('base64')).toEqual(
Buffer.from(expectedKey).toString('base64')
);

expect(
extensions
.filter((e) => e.type.startsWith('1.3.6.1.4.1.57264'))
.map((e) => e.toString('asn'))
).toMatchSnapshot();
});
});
});
});

Expand Down
10 changes: 9 additions & 1 deletion packages/mock/src/fulcio/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import x509 from '@peculiar/x509';
import assert from 'assert';
import { generateKeyPairSync } from 'crypto';
import * as jose from 'jose';
Expand Down Expand Up @@ -101,7 +102,9 @@ function parseBody(
): { subject: string; publicKey: string; claims: Record<string, string> } {
const json = JSON.parse(body.toString());
const oidc = json.credentials.oidcIdentityToken;
const pem = json.publicKeyRequest.publicKey.content;
const pem = json.publicKeyRequest
? json.publicKeyRequest.publicKey.content
: extractCSRKey(json.certificateSigningRequest);

// Decode the JWT
const claims = jose.decodeJwt(oidc) as Record<string, string>;
Expand Down Expand Up @@ -266,6 +269,11 @@ function extensionFromClaims(claims: Record<string, string>): ExtensionValue[] {
return extensions;
}

function extractCSRKey(pem: string): string {
const csr = new x509.Pkcs10CertificateRequest(pem);
return csr.publicKey.toString('pem');
}

// PEM string to DER-encoded byte buffer conversion
function fromPEM(pem: string): Buffer {
return Buffer.from(
Expand Down

0 comments on commit 8ec213e

Please sign in to comment.