Skip to content

Commit

Permalink
fulcio mock support for all GH claims (#1319)
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 8279f49 commit c2ca121
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-tigers-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sigstore/mock': patch
---

Update Fulcio mock server to support all GH OIDC claims
84 changes: 84 additions & 0 deletions packages/mock/src/fulcio/__snapshots__/handler.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`fulcioHandler #fn when invoked 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'",
]
`;
2 changes: 1 addition & 1 deletion packages/mock/src/fulcio/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface CertificateRequestOptions {
extensions?: ExtensionValue[];
}

interface ExtensionValue {
export interface ExtensionValue {
oid: string;
value: string;
legacy?: boolean;
Expand Down
28 changes: 28 additions & 0 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 x509 from '@peculiar/x509';
import { generateKeyPairSync } from 'crypto';
import { generateKeyPair } from '../util/key';
import { CA, initializeCA } from './ca';
Expand Down Expand Up @@ -43,6 +44,24 @@ describe('fulcioHandler', () => {
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);

Expand Down Expand Up @@ -80,6 +99,15 @@ describe('fulcioHandler', () => {
expect(
certs.signedCertificateEmbeddedSct.chain.certificates
).toHaveLength(2);

const { extensions } = new x509.X509Certificate(
certs.signedCertificateEmbeddedSct.chain.certificates[0]
);
expect(
extensions
.filter((e) => e.type.startsWith('1.3.6.1.4.1.57264'))
.map((e) => e.toString('asn'))
).toMatchSnapshot();
});

describe('when the CA raises an error', () => {
Expand Down
171 changes: 159 additions & 12 deletions packages/mock/src/fulcio/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,33 @@ import assert from 'assert';
import { generateKeyPairSync } from 'crypto';
import * as jose from 'jose';
import type { Handler, HandlerFn, HandlerFnResult } from '../shared.types';
import type { CA } from './ca';
import type { CA, ExtensionValue } from './ca';

const CREATE_SIGNING_CERT_PATH = '/api/v2/signingCert';
const DEFAULT_SUBJECT = 'NO-SUBJECT';
const DEFAULT_ISSUER = 'https://fake.oidcissuer.com';

const ISSUER_EXT_OID_V1 = '1.3.6.1.4.1.57264.1.1';
const GH_WORKFLOW_TRIGGER_EXT_OID = '1.3.6.1.4.1.57264.1.2';
const GH_WORKFLOW_SHA_EXT_OID = '1.3.6.1.4.1.57264.1.3';
const GH_WORKFLOW_NAME_EXT_OID = '1.3.6.1.4.1.57264.1.4';
const GH_WORKFLOW_REPO_EXT_OID = '1.3.6.1.4.1.57264.1.5';
const GH_WORKFLOW_REF_EXT_OID = '1.3.6.1.4.1.57264.1.6';
const ISSUER_EXT_OID_V2 = '1.3.6.1.4.1.57264.1.8';
const BUILD_SIGNER_URI_EXT_OID = '1.3.6.1.4.1.57264.1.9';
const BUILD_SIGNER_DIGEST_EXT_OID = '1.3.6.1.4.1.57264.1.10';
const RUNNER_ENVIRONMENT_EXT_OID = '1.3.6.1.4.1.57264.1.11';
const SOURCE_REPO_URI_EXT_OID = '1.3.6.1.4.1.57264.1.12';
const SOURCE_REPO_DIGEST_EXT_OID = '1.3.6.1.4.1.57264.1.13';
const SOURCE_REPO_REF_EXT_OID = '1.3.6.1.4.1.57264.1.14';
const SOURCE_REPO_ID_EXT_OID = '1.3.6.1.4.1.57264.1.15';
const SOURCE_REPO_OWNER_URI_EXT_OID = '1.3.6.1.4.1.57264.1.16';
const SOURCE_REPO_OWNER_ID_EXT_OID = '1.3.6.1.4.1.57264.1.17';
const BUILD_CONFIG_URI_EXT_OID = '1.3.6.1.4.1.57264.1.18';
const BUILD_CONFIG_DIGEST_EXT_OID = '1.3.6.1.4.1.57264.1.19';
const BUILD_TRIGGER_EXT_OID = '1.3.6.1.4.1.57264.1.20';
const RUN_INVOCATION_URI_EXT_OID = '1.3.6.1.4.1.57264.1.21';
const SOURCE_REPO_VISIBILITY_EXT_OID = '1.3.6.1.4.1.57264.1.22';

interface FulcioHandlerOptions {
strict?: boolean;
Expand All @@ -52,18 +71,17 @@ function createSigningCertHandler(
return async (body: string): Promise<HandlerFnResult> => {
try {
// Extract relevant fields from the request
const { subject, issuer, publicKey } = strict
const { subject, publicKey, claims } = strict
? parseBody(body, subjectClaim)
: stubBody();

const extensions = extensionFromClaims(claims);

// Request certificate from CA
const cert = await ca.issueCertificate({
publicKey: fromPEM(publicKey),
subjectAltName: subject,
extensions: [
{ oid: ISSUER_EXT_OID_V1, value: issuer, legacy: true },
{ oid: ISSUER_EXT_OID_V2, value: issuer },
],
extensions: extensions,
});

// Format the response
Expand All @@ -80,31 +98,34 @@ function createSigningCertHandler(
function parseBody(
body: string,
subjectClaim: string
): { subject: string; issuer: string; publicKey: string } {
): { 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;

// Decode the JWT
/* eslint-disable @typescript-eslint/no-explicit-any */
const claims = jose.decodeJwt(oidc) as any;
const claims = jose.decodeJwt(oidc) as Record<string, string>;

/* istanbul ignore next */
return {
subject: claims[subjectClaim] || DEFAULT_SUBJECT,
issuer: claims['iss'] || DEFAULT_ISSUER,
publicKey: pem,
claims: { iss: DEFAULT_ISSUER, ...claims },
};
}

function stubBody(): { subject: string; issuer: string; publicKey: string } {
function stubBody(): {
subject: string;
publicKey: string;
claims: Record<string, string>;
} {
const { publicKey } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
});
return {
subject: DEFAULT_SUBJECT,
issuer: DEFAULT_ISSUER,
publicKey: publicKey.export({ format: 'pem', type: 'spki' }).toString(),
claims: { iss: DEFAULT_ISSUER },
};
}

Expand All @@ -119,6 +140,132 @@ function buildResponse(leaf: Buffer, root: Buffer): string {
return JSON.stringify(body);
}

function extensionFromClaims(claims: Record<string, string>): ExtensionValue[] {
const extensions: ExtensionValue[] = [];
const baseURL = 'https://github.com';

for (const [key, value] of Object.entries(claims)) {
switch (key) {
case 'iss':
extensions.push({
oid: ISSUER_EXT_OID_V1,
value: value,
legacy: true,
});
extensions.push({ oid: ISSUER_EXT_OID_V2, value: value });
break;
case 'event_name':
extensions.push({
oid: GH_WORKFLOW_TRIGGER_EXT_OID,
value: value,
legacy: true,
});
extensions.push({ oid: BUILD_TRIGGER_EXT_OID, value: value });
break;
case 'sha':
extensions.push({
oid: GH_WORKFLOW_SHA_EXT_OID,
value: value,
legacy: true,
});
extensions.push({ oid: SOURCE_REPO_DIGEST_EXT_OID, value: value });
break;
case 'workflow':
extensions.push({
oid: GH_WORKFLOW_NAME_EXT_OID,
value: value,
legacy: true,
});
break;
case 'repository':
extensions.push({
oid: GH_WORKFLOW_REPO_EXT_OID,
value: value,
legacy: true,
});
extensions.push({
oid: SOURCE_REPO_URI_EXT_OID,
value: `${baseURL}/${value}`,
});
break;
case 'ref':
extensions.push({
oid: GH_WORKFLOW_REF_EXT_OID,
value: value,
legacy: true,
});
extensions.push({
oid: SOURCE_REPO_REF_EXT_OID,
value: value,
});
break;
case 'job_workflow_ref':
extensions.push({
oid: BUILD_SIGNER_URI_EXT_OID,
value: `${baseURL}/${value}`,
});
break;
case 'job_workflow_sha':
extensions.push({
oid: BUILD_SIGNER_DIGEST_EXT_OID,
value: value,
});
break;
case 'runner_environment':
extensions.push({
oid: RUNNER_ENVIRONMENT_EXT_OID,
value: value,
});
break;
case 'repository_id':
extensions.push({
oid: SOURCE_REPO_ID_EXT_OID,
value: value,
});
break;
case 'repository_owner':
extensions.push({
oid: SOURCE_REPO_OWNER_URI_EXT_OID,
value: `${baseURL}/${value}`,
});
break;
case 'repository_owner_id':
extensions.push({
oid: SOURCE_REPO_OWNER_ID_EXT_OID,
value: value,
});
break;
case 'workflow_ref':
extensions.push({
oid: BUILD_CONFIG_URI_EXT_OID,
value: `${baseURL}/${value}`,
});
break;
case 'workflow_sha':
extensions.push({
oid: BUILD_CONFIG_DIGEST_EXT_OID,
value: value,
});
break;
case 'repository_visibility':
extensions.push({
oid: SOURCE_REPO_VISIBILITY_EXT_OID,
value: value,
});
break;
}
}

if (claims['repository'] && claims['run_id'] && claims['run_attempt']) {
extensions.push({
oid: RUN_INVOCATION_URI_EXT_OID,
value: `${baseURL}/${claims['repository']}/actions/runs/${claims['run_id']}/attempts/${claims['run_attempt']}`,
});
}

return extensions;
}

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

0 comments on commit c2ca121

Please sign in to comment.