Skip to content

Commit 19e9f9d

Browse files
bdehamersegiddins
andcommitted
CSR support in Fulcio mock
Co-authored-by: Samuel Giddins <[email protected]> Signed-off-by: Brian DeHamer <[email protected]>
1 parent c2ca121 commit 19e9f9d

File tree

4 files changed

+193
-28
lines changed

4 files changed

+193
-28
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: 95 additions & 26 deletions
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 { Crypto } from '@peculiar/webcrypto';
1718
import x509 from '@peculiar/x509';
1819
import { generateKeyPairSync } from 'crypto';
1920
import { generateKeyPair } from '../util/key';
@@ -32,39 +33,39 @@ describe('fulcioHandler', () => {
3233
});
3334

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

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

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-
6869
const certRequest = {
6970
credentials: {
7071
oidcIdentityToken: jwt,
@@ -100,9 +101,14 @@ describe('fulcioHandler', () => {
100101
certs.signedCertificateEmbeddedSct.chain.certificates
101102
).toHaveLength(2);
102103

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

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)