Skip to content

Commit f098fdd

Browse files
committed
CSR support in Fulcio mock
Signed-off-by: Brian DeHamer <[email protected]>
1 parent c2ca121 commit f098fdd

File tree

4 files changed

+190
-29
lines changed

4 files changed

+190
-29
lines changed

.changeset/tiny-comics-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sigstore/mock': minor
3+
---
4+
5+
Update Fulcio mock with support for CSRs

packages/mock/src/fulcio/__snapshots__/handler.test.ts.snap

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,89 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`fulcioHandler #fn when invoked returns a certificate chain 1`] = `
3+
exports[`fulcioHandler #fn when invoked w/ a CSR returns a certificate chain 1`] = `
4+
Extensions [
5+
"SEQUENCE :
6+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.1
7+
OCTET STRING : 687474703a2f2f666f6f2e636f6d",
8+
"SEQUENCE :
9+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.8
10+
OCTET STRING :
11+
UTF8String : 'http://foo.com'",
12+
"SEQUENCE :
13+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.2
14+
OCTET STRING : 776f726b666c6f775f6469737061746368",
15+
"SEQUENCE :
16+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.20
17+
OCTET STRING :
18+
UTF8String : 'workflow_dispatch'",
19+
"SEQUENCE :
20+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.9
21+
OCTET STRING :
22+
UTF8String : 'https://github.com/foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main'",
23+
"SEQUENCE :
24+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.10
25+
OCTET STRING :
26+
UTF8String : 'ba214227977e57973d87219493ad93eeda9c7d6c'",
27+
"SEQUENCE :
28+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.6
29+
OCTET STRING : 726566732f68656164732f6d61696e",
30+
"SEQUENCE :
31+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.14
32+
OCTET STRING :
33+
UTF8String : 'refs/heads/main'",
34+
"SEQUENCE :
35+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.5
36+
OCTET STRING : 666f6f2f6174746573742d64656d6f",
37+
"SEQUENCE :
38+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.12
39+
OCTET STRING :
40+
UTF8String : 'https://github.com/foo/attest-demo'",
41+
"SEQUENCE :
42+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.15
43+
OCTET STRING :
44+
UTF8String : '792829709'",
45+
"SEQUENCE :
46+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.16
47+
OCTET STRING :
48+
UTF8String : 'https://github.com/foo'",
49+
"SEQUENCE :
50+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.17
51+
OCTET STRING :
52+
UTF8String : '398027'",
53+
"SEQUENCE :
54+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.22
55+
OCTET STRING :
56+
UTF8String : 'public'",
57+
"SEQUENCE :
58+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.11
59+
OCTET STRING :
60+
UTF8String : 'github-hosted'",
61+
"SEQUENCE :
62+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.3
63+
OCTET STRING : 62613231343232373937376535373937336438373231393439336164393365656461396337643663",
64+
"SEQUENCE :
65+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.13
66+
OCTET STRING :
67+
UTF8String : 'ba214227977e57973d87219493ad93eeda9c7d6c'",
68+
"SEQUENCE :
69+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.4
70+
OCTET STRING : 4f494443",
71+
"SEQUENCE :
72+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.18
73+
OCTET STRING :
74+
UTF8String : 'https://github.com/foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main'",
75+
"SEQUENCE :
76+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.19
77+
OCTET STRING :
78+
UTF8String : 'ba214227977e57973d87219493ad93eeda9c7d6c'",
79+
"SEQUENCE :
80+
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.21
81+
OCTET STRING :
82+
UTF8String : 'https://github.com/foo/attest-demo/actions/runs/11997537386/attempts/3'",
83+
]
84+
`;
85+
86+
exports[`fulcioHandler #fn when invoked w/ a public key returns a certificate chain 1`] = `
487
Extensions [
588
"SEQUENCE :
689
OBJECT IDENTIFIER : 1.3.6.1.4.1.57264.1.1

packages/mock/src/fulcio/handler.test.ts

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import x509 from '@peculiar/x509';
18-
import { generateKeyPairSync } from 'crypto';
18+
import { generateKeyPairSync, subtle } from 'crypto';
1919
import { generateKeyPair } from '../util/key';
2020
import { CA, initializeCA } from './ca';
2121
import { fulcioHandler } from './handler';
@@ -32,39 +32,39 @@ describe('fulcioHandler', () => {
3232
});
3333

3434
describe('#fn', () => {
35+
const claims = {
36+
sub: 'http://github.com/foo/workflow.yml@refs/heads/main',
37+
iss: 'http://foo.com',
38+
event_name: 'workflow_dispatch',
39+
job_workflow_ref:
40+
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
41+
job_workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
42+
ref: 'refs/heads/main',
43+
repository: 'foo/attest-demo',
44+
repository_id: '792829709',
45+
repository_owner: 'foo',
46+
repository_owner_id: '398027',
47+
repository_visibility: 'public',
48+
run_attempt: '3',
49+
run_id: '11997537386',
50+
runner_environment: 'github-hosted',
51+
sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
52+
workflow: 'OIDC',
53+
workflow_ref:
54+
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
55+
workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
56+
};
57+
const jwt = jwtify(claims);
58+
3559
it('returns a function', async () => {
3660
const ca = await initializeCA(keyPair);
3761
const handler = fulcioHandler(ca);
3862
expect(handler.fn).toBeInstanceOf(Function);
3963
});
4064

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

44-
const claims = {
45-
sub: 'http://github.com/foo/workflow.yml@refs/heads/main',
46-
iss: 'http://foo.com',
47-
event_name: 'workflow_dispatch',
48-
job_workflow_ref:
49-
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
50-
job_workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
51-
ref: 'refs/heads/main',
52-
repository: 'foo/attest-demo',
53-
repository_id: '792829709',
54-
repository_owner: 'foo',
55-
repository_owner_id: '398027',
56-
repository_visibility: 'public',
57-
run_attempt: '3',
58-
run_id: '11997537386',
59-
runner_environment: 'github-hosted',
60-
sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
61-
workflow: 'OIDC',
62-
workflow_ref:
63-
'foo/attest-demo/.github/workflows/oidc.yml@refs/heads/main',
64-
workflow_sha: 'ba214227977e57973d87219493ad93eeda9c7d6c',
65-
};
66-
const jwt = jwtify(claims);
67-
6868
const certRequest = {
6969
credentials: {
7070
oidcIdentityToken: jwt,
@@ -100,9 +100,14 @@ describe('fulcioHandler', () => {
100100
certs.signedCertificateEmbeddedSct.chain.certificates
101101
).toHaveLength(2);
102102

103-
const { extensions } = new x509.X509Certificate(
103+
const { extensions, publicKey } = new x509.X509Certificate(
104104
certs.signedCertificateEmbeddedSct.chain.certificates[0]
105105
);
106+
107+
// Ensure public key matches input
108+
expect(publicKey.toString('pem')).toEqual(
109+
certRequest.publicKeyRequest.publicKey.content.trimEnd()
110+
);
106111
expect(
107112
extensions
108113
.filter((e) => e.type.startsWith('1.3.6.1.4.1.57264'))
@@ -125,6 +130,66 @@ describe('fulcioHandler', () => {
125130
});
126131
});
127132
});
133+
134+
describe('when invoked w/ a CSR', () => {
135+
it('returns a certificate chain', async () => {
136+
const kp = await subtle.generateKey(
137+
{ name: 'ecdsa', namedCurve: 'P-256' },
138+
true,
139+
['sign', 'verify']
140+
);
141+
142+
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
143+
signingAlgorithm: {
144+
name: 'ECDSA',
145+
hash: 'SHA-256',
146+
},
147+
keys: kp,
148+
});
149+
150+
const certRequest = {
151+
credentials: {
152+
oidcIdentityToken: jwt,
153+
},
154+
certificateSigningRequest: csr.toString('pem'),
155+
};
156+
157+
const ca = await initializeCA(keyPair);
158+
const { fn } = fulcioHandler(ca);
159+
160+
// Make a request
161+
const resp = await fn(JSON.stringify(certRequest));
162+
expect(resp.statusCode).toBe(201);
163+
164+
// Check the response
165+
const certs = JSON.parse(resp.response.toString());
166+
expect(certs).toBeDefined();
167+
expect(certs.signedCertificateEmbeddedSct).toBeDefined();
168+
expect(certs.signedCertificateEmbeddedSct.chain).toBeDefined();
169+
expect(
170+
certs.signedCertificateEmbeddedSct.chain.certificates
171+
).toBeDefined();
172+
expect(
173+
certs.signedCertificateEmbeddedSct.chain.certificates
174+
).toHaveLength(2);
175+
176+
const { extensions, publicKey } = new x509.X509Certificate(
177+
certs.signedCertificateEmbeddedSct.chain.certificates[0]
178+
);
179+
180+
// Ensure public key matches the CSR
181+
const expectedKey = await crypto.subtle.exportKey('spki', kp.publicKey);
182+
expect(publicKey.toString('base64')).toEqual(
183+
Buffer.from(expectedKey).toString('base64')
184+
);
185+
186+
expect(
187+
extensions
188+
.filter((e) => e.type.startsWith('1.3.6.1.4.1.57264'))
189+
.map((e) => e.toString('asn'))
190+
).toMatchSnapshot();
191+
});
192+
});
128193
});
129194
});
130195

packages/mock/src/fulcio/handler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import x509 from '@peculiar/x509';
1718
import assert from 'assert';
1819
import { generateKeyPairSync } from 'crypto';
1920
import * as jose from 'jose';
@@ -101,7 +102,9 @@ function parseBody(
101102
): { subject: string; publicKey: string; claims: Record<string, string> } {
102103
const json = JSON.parse(body.toString());
103104
const oidc = json.credentials.oidcIdentityToken;
104-
const pem = json.publicKeyRequest.publicKey.content;
105+
const pem = json.publicKeyRequest
106+
? json.publicKeyRequest.publicKey.content
107+
: extractCSRKey(json.certificateSigningRequest);
105108

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

272+
function extractCSRKey(pem: string): string {
273+
const csr = new x509.Pkcs10CertificateRequest(pem);
274+
return csr.publicKey.toString('pem');
275+
}
276+
269277
// PEM string to DER-encoded byte buffer conversion
270278
function fromPEM(pem: string): Buffer {
271279
return Buffer.from(

0 commit comments

Comments
 (0)