From 8d4b20c5604738f1c4427fc50cac92909d3f61bf Mon Sep 17 00:00:00 2001 From: awilliams1-cb <148368153+awilliams1-cb@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:11:40 -0400 Subject: [PATCH 1/3] fix: validate external and internal cert linking --- __tests__/task-origin-validate.test.ts | 172 +++++++++++++++++++++++++ src/lib/task-origin-validate.ts | 34 ++++- 2 files changed, 203 insertions(+), 3 deletions(-) diff --git a/__tests__/task-origin-validate.test.ts b/__tests__/task-origin-validate.test.ts index dde6ba7..55b7c3b 100644 --- a/__tests__/task-origin-validate.test.ts +++ b/__tests__/task-origin-validate.test.ts @@ -3,6 +3,7 @@ import { fileURLToPath } from 'url'; import os from 'os'; import fs from 'fs/promises'; import crypto from 'crypto'; +import forge from 'node-forge'; import * as tar from 'tar'; import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { @@ -249,4 +250,175 @@ describe('buildAndValidateSignature', () => { ).rejects.toThrow(); }); }); + + describe('self-minted certificate chain', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'forged-sig-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true }); + }); + + // Mints a completely self-signed chain (attacker's own root -> intermediate -> + // leaf) that never touches the real Coinbase hierarchy. The subject/issuer names + // mirror the genuine chain so that, absent the pins, it would build a valid path. + function mintSelfSignedChain(sanURI: string): { + leaf: string; + intermediate: string; + root: string; + } { + const notBefore = new Date('2020-01-01T00:00:00Z'); + const notAfter = new Date('2050-01-01T00:00:00Z'); + + const derBase64 = (cert: forge.pki.Certificate): string => + forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes()); + + const makeCert = ( + subjectCN: string, + issuerCN: string, + subjectKey: forge.pki.rsa.PublicKey, + signingKey: forge.pki.rsa.PrivateKey, + serial: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extensions: any[] + ): forge.pki.Certificate => { + const cert = forge.pki.createCertificate(); + cert.publicKey = subjectKey; + cert.serialNumber = serial; + cert.validity.notBefore = notBefore; + cert.validity.notAfter = notAfter; + cert.setSubject([{ name: 'commonName', value: subjectCN }]); + cert.setIssuer([{ name: 'commonName', value: issuerCN }]); + cert.setExtensions(extensions); + cert.sign(signingKey, forge.md.sha256.create()); + return cert; + }; + + const rootKeys = forge.pki.rsa.generateKeyPair(2048); + const intKeys = forge.pki.rsa.generateKeyPair(2048); + const leafKeys = forge.pki.rsa.generateKeyPair(2048); + + // Self-signed root impersonating the real CB-ROOT-CORE. + const root = makeCert('CB-ROOT-CORE', 'CB-ROOT-CORE', rootKeys.publicKey, rootKeys.privateKey, '01', [ + { name: 'basicConstraints', cA: true, critical: true }, + { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, + ]); + // Intermediate impersonating the runtime intermediate, signed by the fake root. + const intermediate = makeCert( + 'corporate.device.cbhq.net', + 'CB-ROOT-CORE', + intKeys.publicKey, + rootKeys.privateKey, + '02', + [ + { name: 'basicConstraints', cA: true, critical: true }, + { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, + ] + ); + // Leaf carrying a forged facilitator identity, signed by the fake intermediate. + const leaf = makeCert( + 'forged', + 'corporate.device.cbhq.net', + leafKeys.publicKey, + intKeys.privateKey, + '03', + [ + { name: 'basicConstraints', cA: false }, + { name: 'keyUsage', digitalSignature: true, critical: true }, + { name: 'subjectAltName', altNames: [{ type: 6, value: sanURI }] }, + ] + ); + + return { leaf: derBase64(leaf), intermediate: derBase64(intermediate), root: derBase64(root) }; + } + + it('rejects a bundle whose certificate chain is fully self-signed', async () => { + const chain = mintSelfSignedChain('ldap:///base-facilitators'); + + // Start from a genuine bundle and keep its real signature + TSA timestamp so + // verification reaches the certificate-chain stage; only the cert chain is + // swapped for the self-minted one, laid out as [leaf, runtime intermediate, + // static intermediate, root] to match the injection indices. + const bundle = JSON.parse( + await fs.readFile(path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), 'utf8') + ); + bundle.verificationMaterial.x509CertificateChain.certificates = [ + { rawBytes: chain.leaf }, + { rawBytes: chain.intermediate }, + { rawBytes: chain.intermediate }, + { rawBytes: chain.root }, + ]; + + const forgedSignatureFile = path.join(tempDir, 'base-facilitator-signature.json'); + await fs.writeFile(forgedSignatureFile, JSON.stringify(bundle)); + + // The pins refuse to inject the self-minted root/intermediate as trust anchors, + // so no trusted certificate path can be built for the forged leaf. + await expect( + buildAndValidateSignature({ + taskFolderPath: VALID_TASK_FOLDER, + signatureFile: forgedSignatureFile, + commonName: 'base-facilitators', + role: 'baseFacilitator' as TaskOriginRole, + }) + ).rejects.toThrow(/certificate chain/i); + }, 60000); + }); + + describe('malformed certificate chain', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'malformed-sig-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true }); + }); + + // The pinning logic assumes the bundle carries the full chain + // [leaf, runtime intermediate, static intermediate, root]. A bundle missing the + // runtime intermediate ([1]) or root ([3]) must be rejected outright rather than + // silently falling through to a base-only chain. + async function writeBundleWithChain(certificateCount: number): Promise { + const bundle = JSON.parse( + await fs.readFile(path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), 'utf8') + ); + bundle.verificationMaterial.x509CertificateChain.certificates = + bundle.verificationMaterial.x509CertificateChain.certificates.slice(0, certificateCount); + + const signatureFile = path.join(tempDir, 'base-facilitator-signature.json'); + await fs.writeFile(signatureFile, JSON.stringify(bundle)); + return signatureFile; + } + + it('rejects a bundle missing the root certificate', async () => { + // Keep [leaf, runtime, static] but drop the root ([3]). + const signatureFile = await writeBundleWithChain(3); + await expect( + buildAndValidateSignature({ + taskFolderPath: VALID_TASK_FOLDER, + signatureFile, + commonName: 'base-facilitators', + role: 'baseFacilitator' as TaskOriginRole, + }) + ).rejects.toThrow('certificate chain must contain a runtime intermediate and root'); + }); + + it('rejects a bundle with only a leaf certificate', async () => { + // Only [leaf] present, so both the runtime intermediate ([1]) and root ([3]) are missing. + const signatureFile = await writeBundleWithChain(1); + await expect( + buildAndValidateSignature({ + taskFolderPath: VALID_TASK_FOLDER, + signatureFile, + commonName: 'base-facilitators', + role: 'baseFacilitator' as TaskOriginRole, + }) + ).rejects.toThrow('certificate chain must contain a runtime intermediate and root'); + }); + }); }); diff --git a/src/lib/task-origin-validate.ts b/src/lib/task-origin-validate.ts index 811ba62..94a3420 100644 --- a/src/lib/task-origin-validate.ts +++ b/src/lib/task-origin-validate.ts @@ -1,6 +1,7 @@ import * as tar from 'tar'; import path from 'path'; import fs from 'fs/promises'; +import { X509Certificate } from 'crypto'; import { Verifier, toTrustMaterial, toSignedEntity } from '@sigstore/verify'; import { bundleFromJSON } from '@sigstore/bundle'; import trustedRoot from './config/trusted-root.json'; @@ -135,6 +136,16 @@ export async function buildAndValidateSignature(options: TaskOriginVerifyOptions const runtimeIntermediate = bundleCerts[1] ? { rawBytes: bundleCerts[1].rawBytes } : null; const rootCert = bundleCerts[3] ? { rawBytes: bundleCerts[3].rawBytes } : null; + // The pinning below assumes the bundle carries the full chain + // [leaf, runtime intermediate, static intermediate, root]. If the runtime + // intermediate or root is missing, that assumption is broken and we cannot build a + // chain that anchors to a configured CA, so reject rather than silently continuing. + if (!runtimeIntermediate || !rootCert) { + throw new Error( + 'Invalid signature bundle: certificate chain must contain a runtime intermediate and root' + ); + } + // Prepare trust material with: // 1. Date strings converted to Date objects (required for filtering) // 2. Runtime intermediate CA injected into certificate chain @@ -150,10 +161,27 @@ export async function buildAndValidateSignature(options: TaskOriginVerifyOptions rawBytes: Buffer.from(cert.rawBytes, 'base64'), })); - // Build the certificate chain: base certs + runtime intermediate + root + // Build the certificate chain: base certs + runtime intermediate + root. + // The runtime intermediate and root come from an external source, so they are + // only injected after being pinned to the statically-configured intermediate. + const staticInter = new X509Certificate(baseCerts[0].rawBytes); const certChain = [...baseCerts]; - if (runtimeIntermediate) certChain.push(runtimeIntermediate); - if (rootCert) certChain.push(rootCert); + + // Pin: the root must actually have signed our pinned static intermediate, so + // only the genuine root is trusted. A cert that is not part of the chain (e.g. + // a self-minted root) cannot have produced that signature and is rejected. + // A failed pin falls through to a base-only chain so other configured CAs can + // still be tried; verification rejects only if no CA yields a trusted path. + const root = new X509Certificate(new Uint8Array(rootCert.rawBytes)); + if (root.ca && staticInter.verify(root.publicKey)) { + // Pin: the runtime intermediate must be issued by our pinned static + // intermediate; this also rejects a self-signed intermediate injected here. + const runtime = new X509Certificate(new Uint8Array(runtimeIntermediate.rawBytes)); + if (runtime.ca && runtime.verify(staticInter.publicKey)) { + certChain.push(runtimeIntermediate); + } + certChain.push(rootCert); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any const normalized: any = { From 02a0a8aab675b042d43a02ee33d3e62d230082e8 Mon Sep 17 00:00:00 2001 From: awilliams1-cb <148368153+awilliams1-cb@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:54:07 -0400 Subject: [PATCH 2/3] Formatting --- __tests__/task-origin-validate.test.ts | 31 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/__tests__/task-origin-validate.test.ts b/__tests__/task-origin-validate.test.ts index 55b7c3b..4904b27 100644 --- a/__tests__/task-origin-validate.test.ts +++ b/__tests__/task-origin-validate.test.ts @@ -302,10 +302,17 @@ describe('buildAndValidateSignature', () => { const leafKeys = forge.pki.rsa.generateKeyPair(2048); // Self-signed root impersonating the real CB-ROOT-CORE. - const root = makeCert('CB-ROOT-CORE', 'CB-ROOT-CORE', rootKeys.publicKey, rootKeys.privateKey, '01', [ - { name: 'basicConstraints', cA: true, critical: true }, - { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, - ]); + const root = makeCert( + 'CB-ROOT-CORE', + 'CB-ROOT-CORE', + rootKeys.publicKey, + rootKeys.privateKey, + '01', + [ + { name: 'basicConstraints', cA: true, critical: true }, + { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, + ] + ); // Intermediate impersonating the runtime intermediate, signed by the fake root. const intermediate = makeCert( 'corporate.device.cbhq.net', @@ -332,7 +339,11 @@ describe('buildAndValidateSignature', () => { ] ); - return { leaf: derBase64(leaf), intermediate: derBase64(intermediate), root: derBase64(root) }; + return { + leaf: derBase64(leaf), + intermediate: derBase64(intermediate), + root: derBase64(root), + }; } it('rejects a bundle whose certificate chain is fully self-signed', async () => { @@ -343,7 +354,10 @@ describe('buildAndValidateSignature', () => { // swapped for the self-minted one, laid out as [leaf, runtime intermediate, // static intermediate, root] to match the injection indices. const bundle = JSON.parse( - await fs.readFile(path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), 'utf8') + await fs.readFile( + path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), + 'utf8' + ) ); bundle.verificationMaterial.x509CertificateChain.certificates = [ { rawBytes: chain.leaf }, @@ -385,7 +399,10 @@ describe('buildAndValidateSignature', () => { // silently falling through to a base-only chain. async function writeBundleWithChain(certificateCount: number): Promise { const bundle = JSON.parse( - await fs.readFile(path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), 'utf8') + await fs.readFile( + path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), + 'utf8' + ) ); bundle.verificationMaterial.x509CertificateChain.certificates = bundle.verificationMaterial.x509CertificateChain.certificates.slice(0, certificateCount); From f73689fff81ca11d5ea3603635838a57836ff4e9 Mon Sep 17 00:00:00 2001 From: awilliams1-cb <148368153+awilliams1-cb@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:33:26 -0400 Subject: [PATCH 3/3] PR changes --- .../signatures/pinning/leaf-only.json | 26 +++ .../signatures/pinning/missing-root.json | 32 ++++ .../signatures/pinning/self-signed-chain.json | 35 ++++ __tests__/task-origin-validate.test.ts | 161 +----------------- src/lib/task-origin-validate.ts | 18 +- 5 files changed, 103 insertions(+), 169 deletions(-) create mode 100644 __tests__/fixtures/signatures/pinning/leaf-only.json create mode 100644 __tests__/fixtures/signatures/pinning/missing-root.json create mode 100644 __tests__/fixtures/signatures/pinning/self-signed-chain.json diff --git a/__tests__/fixtures/signatures/pinning/leaf-only.json b/__tests__/fixtures/signatures/pinning/leaf-only.json new file mode 100644 index 0000000..6cbafb2 --- /dev/null +++ b/__tests__/fixtures/signatures/pinning/leaf-only.json @@ -0,0 +1,26 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIIEIzCCAgugAwIBAgIUOp6v9e+JrE5OXdi9j1cyJ5kq8J4wDQYJKoZIhvcNAQENBQAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMREwDwYDVQQKEwhDb2luYmFzZTERMA8GA1UECxMIU2VjdXJpdHkxIjAgBgNVBAMTGWNvcnBvcmF0ZS5kZXZpY2UuY2JocS5uZXQwHhcNMjYwMTIzMjMwNDE0WhcNMjYwMTI0MjMwNDE0WjBZMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xETAPBgNVBAoTCENvaW5iYXNlMRIwEAYDVQQLEwljb3Jwb3JhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ6uQBengAJg0GjKT6wWD1gVRY+hIcQC12KSUD+vii3RvxxXVn/wZVviNrFOHfVm6RMFm/OtwuzuLQ18y7i9zIhtaFylQ4/6aXpZ9Nf6yrUdHd91UFq1D973Kz7niWqA/+jbjBsMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBQyvkU1R2wkcPgcZOqxRSxnjCP8ezAkBgNVHREEHTAbhhlsZGFwOi8vL2Jhc2UtZmFjaWxpdGF0b3JzMA0GCSqGSIb3DQEBDQUAA4ICAQBVXpfTFpCttCeaKu1elrVFGzjXgKh/b5de2cW0X+b498ZZVrSWKzb0ZL7zfP9ZNXXFV64sy7ijo9zNMBmdErdl8HwDw3GYyfWedzrvwwvgHrDveRghd3bmwd08vaU6jpUwzhrq30a1SLao3TgkaHpvnZvEpdtKv7cRwadz0bUUTHKjdQTosL7MCS2jupCQ1HSqxWvZ490tbTiTAFyUQUJvlDcv0AsUMAUdUD6TWgPrDWZ4AS8W00vsAlZCrobv3UVHImGsJgVQmDHQ83gMYLGGFgPSQ3iON0koFz0KqJtAbYv3RU3A3faQ90T2KI9A3e1AwETjQwbvVN722NyIEPjzdOtbcpUMrCaZkuWs3sB9tJO3PWLnAqKFSyd6CYOwxHDb/aZyxN5BPtBBz9BxdnM42T7KjABUQ1pINMHHoXZYGjQvH+9C3kygOrgvo04iFKgZwhS+rKphZVllUpkzxv7yfG5QqOVUM0o45LcGVgYJfX/istXT3VCff/I2xgSw+McAzTIlYK539C8oNpbXefym0XXbPlXmOVe3oKC0UwcxdNthIS0ptinnKl0jJ8JKDJRphbk6hZb1f+ak1v3w8Ftj8/CoBGEF5o51kD7TlelgaPWXZMedJfrk8XUoqfZLYa7CmOzif0XHKHzTz5pKcJQ75/Hod+SzCqyVghvV8hjJfg==" + } + ] + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICyTADAgEAMIICwAYJKoZIhvcNAQcCoIICsTCCAq0CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQguXTXv6cm2iEjF2nneUhNQ9Um8zCPcxKYvQJnjnJljaYCFHj03SNBghQNLyvPlUlgz7iOhb6/GA8yMDI2MDEyMzIzMDQxNVowAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdswggHXAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwMTIzMjMwNDE1WjAvBgkqhkiG9w0BCQQxIgQgOiTt85EwxNC4CVLW0S8vLJSgw7W4BiUIsFg4UcEaBMcwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGcwZQIwYkJP6elkSXKrIbF+QXk7ipJQBrwi9NJstYfdvsBGI0iVRRJH1IjQmx13iiVd23EcAjEAuV+0VTZW4WO16ZWW+j+Z0rlBOcLVKb29ALdLylgMo0r8SHWKRGPNMFZUT8IfDHYo" + } + ] + } + }, + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_384", + "digest": "0BfGnSK3SvSkB04GcrW/My7qDhoWCV52qR1VNCGZFWjfIFA+H/jETdHvdTzKOIh5" + }, + "signature": "MGYCMQDrKHTGPwG2Fp7fd1fTQjxZKn+/cBEcfvhOoGns80CoPG6xIH1MlMqLuWE62Tr8P58CMQCq9mETXWy77dkr02jove5BW0k2DF6lFgeKJoTmKWtj7p8ug6qTiDfszo/UluVu0RA=" + } +} diff --git a/__tests__/fixtures/signatures/pinning/missing-root.json b/__tests__/fixtures/signatures/pinning/missing-root.json new file mode 100644 index 0000000..7dfe9e4 --- /dev/null +++ b/__tests__/fixtures/signatures/pinning/missing-root.json @@ -0,0 +1,32 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIIEIzCCAgugAwIBAgIUOp6v9e+JrE5OXdi9j1cyJ5kq8J4wDQYJKoZIhvcNAQENBQAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMREwDwYDVQQKEwhDb2luYmFzZTERMA8GA1UECxMIU2VjdXJpdHkxIjAgBgNVBAMTGWNvcnBvcmF0ZS5kZXZpY2UuY2JocS5uZXQwHhcNMjYwMTIzMjMwNDE0WhcNMjYwMTI0MjMwNDE0WjBZMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xETAPBgNVBAoTCENvaW5iYXNlMRIwEAYDVQQLEwljb3Jwb3JhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ6uQBengAJg0GjKT6wWD1gVRY+hIcQC12KSUD+vii3RvxxXVn/wZVviNrFOHfVm6RMFm/OtwuzuLQ18y7i9zIhtaFylQ4/6aXpZ9Nf6yrUdHd91UFq1D973Kz7niWqA/+jbjBsMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBQyvkU1R2wkcPgcZOqxRSxnjCP8ezAkBgNVHREEHTAbhhlsZGFwOi8vL2Jhc2UtZmFjaWxpdGF0b3JzMA0GCSqGSIb3DQEBDQUAA4ICAQBVXpfTFpCttCeaKu1elrVFGzjXgKh/b5de2cW0X+b498ZZVrSWKzb0ZL7zfP9ZNXXFV64sy7ijo9zNMBmdErdl8HwDw3GYyfWedzrvwwvgHrDveRghd3bmwd08vaU6jpUwzhrq30a1SLao3TgkaHpvnZvEpdtKv7cRwadz0bUUTHKjdQTosL7MCS2jupCQ1HSqxWvZ490tbTiTAFyUQUJvlDcv0AsUMAUdUD6TWgPrDWZ4AS8W00vsAlZCrobv3UVHImGsJgVQmDHQ83gMYLGGFgPSQ3iON0koFz0KqJtAbYv3RU3A3faQ90T2KI9A3e1AwETjQwbvVN722NyIEPjzdOtbcpUMrCaZkuWs3sB9tJO3PWLnAqKFSyd6CYOwxHDb/aZyxN5BPtBBz9BxdnM42T7KjABUQ1pINMHHoXZYGjQvH+9C3kygOrgvo04iFKgZwhS+rKphZVllUpkzxv7yfG5QqOVUM0o45LcGVgYJfX/istXT3VCff/I2xgSw+McAzTIlYK539C8oNpbXefym0XXbPlXmOVe3oKC0UwcxdNthIS0ptinnKl0jJ8JKDJRphbk6hZb1f+ak1v3w8Ftj8/CoBGEF5o51kD7TlelgaPWXZMedJfrk8XUoqfZLYa7CmOzif0XHKHzTz5pKcJQ75/Hod+SzCqyVghvV8hjJfg==" + }, + { + "rawBytes": "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" + }, + { + "rawBytes": "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" + } + ] + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICyTADAgEAMIICwAYJKoZIhvcNAQcCoIICsTCCAq0CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQguXTXv6cm2iEjF2nneUhNQ9Um8zCPcxKYvQJnjnJljaYCFHj03SNBghQNLyvPlUlgz7iOhb6/GA8yMDI2MDEyMzIzMDQxNVowAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdswggHXAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwMTIzMjMwNDE1WjAvBgkqhkiG9w0BCQQxIgQgOiTt85EwxNC4CVLW0S8vLJSgw7W4BiUIsFg4UcEaBMcwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGcwZQIwYkJP6elkSXKrIbF+QXk7ipJQBrwi9NJstYfdvsBGI0iVRRJH1IjQmx13iiVd23EcAjEAuV+0VTZW4WO16ZWW+j+Z0rlBOcLVKb29ALdLylgMo0r8SHWKRGPNMFZUT8IfDHYo" + } + ] + } + }, + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_384", + "digest": "0BfGnSK3SvSkB04GcrW/My7qDhoWCV52qR1VNCGZFWjfIFA+H/jETdHvdTzKOIh5" + }, + "signature": "MGYCMQDrKHTGPwG2Fp7fd1fTQjxZKn+/cBEcfvhOoGns80CoPG6xIH1MlMqLuWE62Tr8P58CMQCq9mETXWy77dkr02jove5BW0k2DF6lFgeKJoTmKWtj7p8ug6qTiDfszo/UluVu0RA=" + } +} diff --git a/__tests__/fixtures/signatures/pinning/self-signed-chain.json b/__tests__/fixtures/signatures/pinning/self-signed-chain.json new file mode 100644 index 0000000..325e0fd --- /dev/null +++ b/__tests__/fixtures/signatures/pinning/self-signed-chain.json @@ -0,0 +1,35 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.2", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIIC9TCCAd2gAwIBAgIBAzANBgkqhkiG9w0BAQsFADAkMSIwIAYDVQQDExljb3Jwb3JhdGUuZGV2aWNlLmNiaHEubmV0MCAXDTIwMDEwMTAwMDAwMFoYDzIwNTAwMTAxMDAwMDAwWjARMQ8wDQYDVQQDEwZmb3JnZWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDF7en1otICjtDRcIZjXZB8QLaLi2gwpeEdBrwgrC6N+voWsyzh7z6r8NE40CSQbwxMLxHTKkpDDm8WYgvJShNee7FPaRUw997fb5apty9vGsddOsap3iAK++RGiuxO6ludhjK1+/ZRiYlajMdg/YnC1j95J/AHG06rY/9b3dvVqtrR3kXAhf9w7qj2zEQwGLhYd+o0b56d5JkJQIPKoaXu1eS4Pf2ydkdxHh4Nes0GmD9ClEyGnBEfDYqojsnDxW3Gsv42EDjJuNdTTQ3XeAU2SthTi5OQ8/SNQV1WXXb5+3Skcmj2ox1hcHB/b4mNYqqeAq/sBHqu5hs5EZLxnrKDAgMBAAGjQzBBMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMCQGA1UdEQQdMBuGGWxkYXA6Ly8vYmFzZS1mYWNpbGl0YXRvcnMwDQYJKoZIhvcNAQELBQADggEBAC4/lj9MCIo/686Z8abht61kSarRECiU3Lq4wqHcGsCDL+KAQJHPrQrAVrOykiAlUih1dWgJ+LAhzHMXABvLPXKzeCgypa8cUdixnyM5sTjO6yFKgNUQzRnaEHEZdAFtdjPs4YqQ2Sz13Lj3eoneguH5XoX/dKd/T5Oa16yQTcG0/WSvLNqNvRXTM2Sbh0YYno24FD4ecdKfroaEdlSShG8+l27WfYlAj1NL2/4U0z54Myppvn2oPh+ARrGAjr/cyzncts+1yDIK3VIAuZtnKt8A76/6ITzKP6BZzKEz9Eh6b1W6ebLaF6aBVv4Xvu2s2mjHt9yko0ax08XpclLN0gY=" + }, + { + "rawBytes": "MIIC2zCCAcOgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDEwxDQi1ST09ULUNPUkUwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMCQxIjAgBgNVBAMTGWNvcnBvcmF0ZS5kZXZpY2UuY2JocS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAxpnrKtP6F3xr5BbcQ0wYcuFvdbQ3BA31g72pkEiHtlHSqu3sQKAzgX4yAmwcjohdMoCQVCUlznWxG8MjHu52Kgc5Upie/LggoPEi7eqkptNnSL56aecU8PCXvJEZkxFa+bbeMdSh6Dga+0t5LdCRx9XDd5ulmvDHIIfaaymLamsFRu2GIlFwJyJ612navqN/ip1pAay5SbFgDZQ7cujAu3Hk4sUAwYYO01T1L2Wt4Uic1sLHgcGTDgivbexZu5G2iraT7Uxr1DQJ3L6CzH7hdkc+lLMfUmgeIJsaCpgjM0Na0snV4BuFhZ3iIFlB7rsUc6uuyDtXiuzYU5/DDA2JAgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQB/k9qjFNKkPkraukz+5CtQVce4B2zNYjLfoTh023Jb0hc4UZ9R4dW+qAYq8lf/jSDxrfAXuBKyB3k7yleMgIkeCfjAzrwhwtA85x2KyRdr4s8UYk11ZMu578CF2CBIzTyIz9nk9BkCEt75B7XbDzN0gi6wN9k2JmjZJzbLyySYPKuA0it5cwV5iur8A5jXQx/VFr0PVCwDe82wRneWGvn2mV/JRBhDn5eNDdaSHJ0NW2lbDaL+Nl+6PMD1sFp4p0/GpupGTJ5asuy3EfUkYMMouUC79Sy4fN4A2I4MRmnsFMsabccWQMlAnu3oyBHtgeNwTHgHNmN7lRDqyh3szQPv" + }, + { + "rawBytes": "MIIC2zCCAcOgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDEwxDQi1ST09ULUNPUkUwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMCQxIjAgBgNVBAMTGWNvcnBvcmF0ZS5kZXZpY2UuY2JocS5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAxpnrKtP6F3xr5BbcQ0wYcuFvdbQ3BA31g72pkEiHtlHSqu3sQKAzgX4yAmwcjohdMoCQVCUlznWxG8MjHu52Kgc5Upie/LggoPEi7eqkptNnSL56aecU8PCXvJEZkxFa+bbeMdSh6Dga+0t5LdCRx9XDd5ulmvDHIIfaaymLamsFRu2GIlFwJyJ612navqN/ip1pAay5SbFgDZQ7cujAu3Hk4sUAwYYO01T1L2Wt4Uic1sLHgcGTDgivbexZu5G2iraT7Uxr1DQJ3L6CzH7hdkc+lLMfUmgeIJsaCpgjM0Na0snV4BuFhZ3iIFlB7rsUc6uuyDtXiuzYU5/DDA2JAgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQB/k9qjFNKkPkraukz+5CtQVce4B2zNYjLfoTh023Jb0hc4UZ9R4dW+qAYq8lf/jSDxrfAXuBKyB3k7yleMgIkeCfjAzrwhwtA85x2KyRdr4s8UYk11ZMu578CF2CBIzTyIz9nk9BkCEt75B7XbDzN0gi6wN9k2JmjZJzbLyySYPKuA0it5cwV5iur8A5jXQx/VFr0PVCwDe82wRneWGvn2mV/JRBhDn5eNDdaSHJ0NW2lbDaL+Nl+6PMD1sFp4p0/GpupGTJ5asuy3EfUkYMMouUC79Sy4fN4A2I4MRmnsFMsabccWQMlAnu3oyBHtgeNwTHgHNmN7lRDqyh3szQPv" + }, + { + "rawBytes": "MIICzjCCAbagAwIBAgIBATANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDEwxDQi1ST09ULUNPUkUwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMBcxFTATBgNVBAMTDENCLVJPT1QtQ09SRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI8PpIjTe3OqCW6r6GvJnDggkxffDSFYGgScVPZCh0XKR1Au5YRiMqvoJykQ4AfZMmcV7mIBkiRyNO/9hNRR6F3Uo4/Krgr6EPMXsyAxOdmX5BvdsxfA324N35rlHuP1+sPdzz1mA+SrEFD1CIdEKZsYyA7LYnbwxrYRcOjUGCbvIlIAYpmqU28u3nPFe7GyR2f7wuqW1Tu25MCbG1KBnfUFgLh4tWOOsP6tneBflUY7eoa1BnRuuSl1oQ+pm+G5l3xwzV9cylZ1gvLtY1KOV93gtMpjpsaDQE68t21CW6+dpfbfoxXqE0HvM2L07Ss5quFzVoaSzu0HYoz6PqmX0LsCAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBADfStzgs7zaPgzE1amytWTA5U+vPw3BxLYrkuvVtKSTye20QngkuKHuDA/POwRUXQTUXr6sLtZreRg2HJOm4JF8sB0qZJB1UkGo1+fSFFSat6BWaNDGENxgSIAzjTgOK3lucpVUthsPMAGyWtDe4TCCOS/1mcSn7Nv4iBwHVoeD9f1IKxrcxwj4kbTAg9LmoeJpNnkF/j1czHWX92FZNHUIBPEXca9OfpzWLTcz9+bTisnRgi1dYXpF88hIGRPDZlzn3i4yddsP31cVC+inID6VQ+6B7yrKzDAuFQg+x2k0jUYZjaCnOhLUBYm9qrMPaKbEL0HcfKU0K/Qq9NcqsvDQ=" + } + ] + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICyTADAgEAMIICwAYJKoZIhvcNAQcCoIICsTCCAq0CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQguXTXv6cm2iEjF2nneUhNQ9Um8zCPcxKYvQJnjnJljaYCFHj03SNBghQNLyvPlUlgz7iOhb6/GA8yMDI2MDEyMzIzMDQxNVowAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdswggHXAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwMTIzMjMwNDE1WjAvBgkqhkiG9w0BCQQxIgQgOiTt85EwxNC4CVLW0S8vLJSgw7W4BiUIsFg4UcEaBMcwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGcwZQIwYkJP6elkSXKrIbF+QXk7ipJQBrwi9NJstYfdvsBGI0iVRRJH1IjQmx13iiVd23EcAjEAuV+0VTZW4WO16ZWW+j+Z0rlBOcLVKb29ALdLylgMo0r8SHWKRGPNMFZUT8IfDHYo" + } + ] + } + }, + "messageSignature": { + "messageDigest": { + "algorithm": "SHA2_384", + "digest": "0BfGnSK3SvSkB04GcrW/My7qDhoWCV52qR1VNCGZFWjfIFA+H/jETdHvdTzKOIh5" + }, + "signature": "MGYCMQDrKHTGPwG2Fp7fd1fTQjxZKn+/cBEcfvhOoGns80CoPG6xIH1MlMqLuWE62Tr8P58CMQCq9mETXWy77dkr02jove5BW0k2DF6lFgeKJoTmKWtj7p8ug6qTiDfszo/UluVu0RA=" + } +} diff --git a/__tests__/task-origin-validate.test.ts b/__tests__/task-origin-validate.test.ts index 4904b27..e15d8b8 100644 --- a/__tests__/task-origin-validate.test.ts +++ b/__tests__/task-origin-validate.test.ts @@ -3,7 +3,6 @@ import { fileURLToPath } from 'url'; import os from 'os'; import fs from 'fs/promises'; import crypto from 'crypto'; -import forge from 'node-forge'; import * as tar from 'tar'; import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { @@ -20,6 +19,7 @@ const FIXTURES_DIR = path.resolve(__dirname, 'fixtures'); const VALID_TASK_FOLDER = path.join(FIXTURES_DIR, 'valid-task', 'config', 'chain1'); const MODIFIED_TASK_FOLDER = path.join(FIXTURES_DIR, 'modified-task', 'config', 'chain1'); const VALID_SIGNATURES_DIR = path.join(FIXTURES_DIR, 'signatures/valid'); +const PINNING_SIGNATURES_DIR = path.join(FIXTURES_DIR, 'signatures/pinning'); // Task creator email const TASK_CREATOR_EMAIL = 'alexis.williams.1@coinbase.com'; @@ -251,174 +251,26 @@ describe('buildAndValidateSignature', () => { }); }); - describe('self-minted certificate chain', () => { - let tempDir: string; - - beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'forged-sig-')); - }); - - afterEach(async () => { - await fs.rm(tempDir, { recursive: true }); - }); - - // Mints a completely self-signed chain (attacker's own root -> intermediate -> - // leaf) that never touches the real Coinbase hierarchy. The subject/issuer names - // mirror the genuine chain so that, absent the pins, it would build a valid path. - function mintSelfSignedChain(sanURI: string): { - leaf: string; - intermediate: string; - root: string; - } { - const notBefore = new Date('2020-01-01T00:00:00Z'); - const notAfter = new Date('2050-01-01T00:00:00Z'); - - const derBase64 = (cert: forge.pki.Certificate): string => - forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes()); - - const makeCert = ( - subjectCN: string, - issuerCN: string, - subjectKey: forge.pki.rsa.PublicKey, - signingKey: forge.pki.rsa.PrivateKey, - serial: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extensions: any[] - ): forge.pki.Certificate => { - const cert = forge.pki.createCertificate(); - cert.publicKey = subjectKey; - cert.serialNumber = serial; - cert.validity.notBefore = notBefore; - cert.validity.notAfter = notAfter; - cert.setSubject([{ name: 'commonName', value: subjectCN }]); - cert.setIssuer([{ name: 'commonName', value: issuerCN }]); - cert.setExtensions(extensions); - cert.sign(signingKey, forge.md.sha256.create()); - return cert; - }; - - const rootKeys = forge.pki.rsa.generateKeyPair(2048); - const intKeys = forge.pki.rsa.generateKeyPair(2048); - const leafKeys = forge.pki.rsa.generateKeyPair(2048); - - // Self-signed root impersonating the real CB-ROOT-CORE. - const root = makeCert( - 'CB-ROOT-CORE', - 'CB-ROOT-CORE', - rootKeys.publicKey, - rootKeys.privateKey, - '01', - [ - { name: 'basicConstraints', cA: true, critical: true }, - { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, - ] - ); - // Intermediate impersonating the runtime intermediate, signed by the fake root. - const intermediate = makeCert( - 'corporate.device.cbhq.net', - 'CB-ROOT-CORE', - intKeys.publicKey, - rootKeys.privateKey, - '02', - [ - { name: 'basicConstraints', cA: true, critical: true }, - { name: 'keyUsage', keyCertSign: true, cRLSign: true, critical: true }, - ] - ); - // Leaf carrying a forged facilitator identity, signed by the fake intermediate. - const leaf = makeCert( - 'forged', - 'corporate.device.cbhq.net', - leafKeys.publicKey, - intKeys.privateKey, - '03', - [ - { name: 'basicConstraints', cA: false }, - { name: 'keyUsage', digitalSignature: true, critical: true }, - { name: 'subjectAltName', altNames: [{ type: 6, value: sanURI }] }, - ] - ); - - return { - leaf: derBase64(leaf), - intermediate: derBase64(intermediate), - root: derBase64(root), - }; - } - + describe('certificate chain pinning', () => { it('rejects a bundle whose certificate chain is fully self-signed', async () => { - const chain = mintSelfSignedChain('ldap:///base-facilitators'); - - // Start from a genuine bundle and keep its real signature + TSA timestamp so - // verification reaches the certificate-chain stage; only the cert chain is - // swapped for the self-minted one, laid out as [leaf, runtime intermediate, - // static intermediate, root] to match the injection indices. - const bundle = JSON.parse( - await fs.readFile( - path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), - 'utf8' - ) - ); - bundle.verificationMaterial.x509CertificateChain.certificates = [ - { rawBytes: chain.leaf }, - { rawBytes: chain.intermediate }, - { rawBytes: chain.intermediate }, - { rawBytes: chain.root }, - ]; - - const forgedSignatureFile = path.join(tempDir, 'base-facilitator-signature.json'); - await fs.writeFile(forgedSignatureFile, JSON.stringify(bundle)); - // The pins refuse to inject the self-minted root/intermediate as trust anchors, // so no trusted certificate path can be built for the forged leaf. await expect( buildAndValidateSignature({ taskFolderPath: VALID_TASK_FOLDER, - signatureFile: forgedSignatureFile, + signatureFile: path.join(PINNING_SIGNATURES_DIR, 'self-signed-chain.json'), commonName: 'base-facilitators', role: 'baseFacilitator' as TaskOriginRole, }) ).rejects.toThrow(/certificate chain/i); - }, 60000); - }); - - describe('malformed certificate chain', () => { - let tempDir: string; - - beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'malformed-sig-')); }); - afterEach(async () => { - await fs.rm(tempDir, { recursive: true }); - }); - - // The pinning logic assumes the bundle carries the full chain - // [leaf, runtime intermediate, static intermediate, root]. A bundle missing the - // runtime intermediate ([1]) or root ([3]) must be rejected outright rather than - // silently falling through to a base-only chain. - async function writeBundleWithChain(certificateCount: number): Promise { - const bundle = JSON.parse( - await fs.readFile( - path.join(VALID_SIGNATURES_DIR, 'base-facilitator-signature.json'), - 'utf8' - ) - ); - bundle.verificationMaterial.x509CertificateChain.certificates = - bundle.verificationMaterial.x509CertificateChain.certificates.slice(0, certificateCount); - - const signatureFile = path.join(tempDir, 'base-facilitator-signature.json'); - await fs.writeFile(signatureFile, JSON.stringify(bundle)); - return signatureFile; - } - it('rejects a bundle missing the root certificate', async () => { - // Keep [leaf, runtime, static] but drop the root ([3]). - const signatureFile = await writeBundleWithChain(3); + // [leaf, runtime, static] with the root ([3]) dropped. await expect( buildAndValidateSignature({ taskFolderPath: VALID_TASK_FOLDER, - signatureFile, + signatureFile: path.join(PINNING_SIGNATURES_DIR, 'missing-root.json'), commonName: 'base-facilitators', role: 'baseFacilitator' as TaskOriginRole, }) @@ -427,11 +279,10 @@ describe('buildAndValidateSignature', () => { it('rejects a bundle with only a leaf certificate', async () => { // Only [leaf] present, so both the runtime intermediate ([1]) and root ([3]) are missing. - const signatureFile = await writeBundleWithChain(1); await expect( buildAndValidateSignature({ taskFolderPath: VALID_TASK_FOLDER, - signatureFile, + signatureFile: path.join(PINNING_SIGNATURES_DIR, 'leaf-only.json'), commonName: 'base-facilitators', role: 'baseFacilitator' as TaskOriginRole, }) diff --git a/src/lib/task-origin-validate.ts b/src/lib/task-origin-validate.ts index 94a3420..7103e77 100644 --- a/src/lib/task-origin-validate.ts +++ b/src/lib/task-origin-validate.ts @@ -136,10 +136,8 @@ export async function buildAndValidateSignature(options: TaskOriginVerifyOptions const runtimeIntermediate = bundleCerts[1] ? { rawBytes: bundleCerts[1].rawBytes } : null; const rootCert = bundleCerts[3] ? { rawBytes: bundleCerts[3].rawBytes } : null; - // The pinning below assumes the bundle carries the full chain - // [leaf, runtime intermediate, static intermediate, root]. If the runtime - // intermediate or root is missing, that assumption is broken and we cannot build a - // chain that anchors to a configured CA, so reject rather than silently continuing. + // The pinning below assumes the bundle carries the full cert chain. Fails early if this + // assumption is not met. if (!runtimeIntermediate || !rootCert) { throw new Error( 'Invalid signature bundle: certificate chain must contain a runtime intermediate and root' @@ -162,25 +160,17 @@ export async function buildAndValidateSignature(options: TaskOriginVerifyOptions })); // Build the certificate chain: base certs + runtime intermediate + root. - // The runtime intermediate and root come from an external source, so they are - // only injected after being pinned to the statically-configured intermediate. const staticInter = new X509Certificate(baseCerts[0].rawBytes); const certChain = [...baseCerts]; - // Pin: the root must actually have signed our pinned static intermediate, so - // only the genuine root is trusted. A cert that is not part of the chain (e.g. - // a self-minted root) cannot have produced that signature and is rejected. - // A failed pin falls through to a base-only chain so other configured CAs can - // still be tried; verification rejects only if no CA yields a trusted path. + // Only inject bundle CAs if they verify against the pinned static intermediate. const root = new X509Certificate(new Uint8Array(rootCert.rawBytes)); if (root.ca && staticInter.verify(root.publicKey)) { - // Pin: the runtime intermediate must be issued by our pinned static - // intermediate; this also rejects a self-signed intermediate injected here. const runtime = new X509Certificate(new Uint8Array(runtimeIntermediate.rawBytes)); if (runtime.ca && runtime.verify(staticInter.publicKey)) { certChain.push(runtimeIntermediate); + certChain.push(rootCert); } - certChain.push(rootCert); } // eslint-disable-next-line @typescript-eslint/no-explicit-any