diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index abe4d05..f94bda9 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -12,17 +12,6 @@ export function getCaCertPath() { return path.join(certFolder, "ca-cert.pem"); } -/** - * @param {forge.pki.PublicKey} publicKey - * @returns {string} - */ -function createKeyIdentifier(publicKey) { - return forge.pki.getPublicKeyFingerprint(publicKey, { - encoding: "binary", - md: forge.md.sha1.create(), - }); -} - /** * @param {string} hostname * @returns {{privateKey: string, certificate: string}} @@ -62,19 +51,39 @@ export function generateCertForHost(hostname) { }, { /* - extKeyUsage serverAuth is required for TLS server authentication. - This is especially important for Python venv environments, which use their own - certificate validation logic and will reject certificates lacking the serverAuth EKU. - Adding serverAuth does not impact other usages + Extended Key Usage (EKU) serverAuth extension + + Needed for TLS server authentication. This extension indicates the certificate's + public key may be used for TLS WWW server authentication. + Python virtualenv environments (like pipx-installed Poetry) enforce this strictly + https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12 */ name: "extKeyUsage", serverAuth: true, }, { + /* + Subject Key Identifier (SKI) + + Needed for Python virtualenv SSL validation and certificate chain building. + This extension provides a means of identifying certificates containing a particular public key. + Python virtualenv environments require this for proper certificate chain validation. + System Python installations may be more lenient. + https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2 + */ name: "subjectKeyIdentifier", subjectKeyIdentifier: createKeyIdentifier(cert.publicKey), }, { + /* + Authority Key Identifier (AKI) + + Needed for Python virtualenv SSL validation and certificate path validation. + This extension identifies the public key corresponding to the private key used to sign + this certificate. It links this certificate to its issuing CA certificate. + Without this, Python virtualenv certificate validation might fail + https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1 + */ name: "authorityKeyIdentifier", keyIdentifier: authorityKeyIdentifier, }, @@ -142,7 +151,7 @@ function generateCa() { const attrs = [{ name: "commonName", value: "safe-chain proxy" }]; cert.setSubject(attrs); - cert.setIssuer(attrs); + cert.setIssuer(attrs); // Self-signed: issuer === subject const keyIdentifier = createKeyIdentifier(cert.publicKey); cert.setExtensions([ { @@ -156,10 +165,28 @@ function generateCa() { digitalSignature: true, keyEncipherment: true, }, + /* + Subject Key Identifier (SKI) + + Needed for Python virtualenv SSL validation and certificate chain building. + This extension provides a means of identifying certificates containing a particular public key. + Python virtualenv environments require this for proper certificate chain validation. + System Python installations may be more lenient. + https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.2 + */ { name: "subjectKeyIdentifier", subjectKeyIdentifier: keyIdentifier, }, + /* + Authority Key Identifier (AKI) + + Needed for Python virtualenv SSL validation and certificate path validation. + This extension identifies the public key corresponding to the private key used to sign + this certificate. It links this certificate to its issuing CA certificate. + Without this, Python virtualenv certificate validation might fail + https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.1 + */ { name: "authorityKeyIdentifier", keyIdentifier, @@ -172,3 +199,14 @@ function generateCa() { certificate: cert, }; } + +/** + * @param {forge.pki.PublicKey} publicKey + * @returns {string} + */ +function createKeyIdentifier(publicKey) { + return forge.pki.getPublicKeyFingerprint(publicKey, { + encoding: "binary", + md: forge.md.sha1.create(), + }); +} diff --git a/test/e2e/pip-ci.e2e.spec.js b/test/e2e/pip-ci.e2e.spec.js index 63bfd90..a99b8d0 100644 --- a/test/e2e/pip-ci.e2e.spec.js +++ b/test/e2e/pip-ci.e2e.spec.js @@ -12,6 +12,10 @@ describe("E2E: safe-chain setup-ci command for pip/pip3", () => { beforeEach(async () => { container = new DockerTestContainer(); await container.start(); + + // Clear pip cache before each test to ensure fresh downloads through proxy + const shell = await container.openShell("zsh"); + await shell.runCommand("pip3 cache purge"); }); afterEach(async () => { diff --git a/test/e2e/pip.e2e.spec.js b/test/e2e/pip.e2e.spec.js index 9a1adec..5d39d8c 100644 --- a/test/e2e/pip.e2e.spec.js +++ b/test/e2e/pip.e2e.spec.js @@ -16,6 +16,9 @@ describe("E2E: pip coverage", () => { const installationShell = await container.openShell("zsh"); await installationShell.runCommand("safe-chain setup --include-python"); + + // Clear pip cache before each test to ensure fresh downloads through proxy + await installationShell.runCommand("pip3 cache purge"); }); afterEach(async () => { @@ -118,9 +121,6 @@ describe("E2E: pip coverage", () => { it(`safe-chain blocks installation of malicious Python packages`, async () => { const shell = await container.openShell("zsh"); - // Clear pip cache to ensure network download through proxy - await shell.runCommand("pip3 cache purge"); - const result = await shell.runCommand( "pip3 install --break-system-packages safe-chain-pi-test" ); @@ -247,9 +247,6 @@ describe("E2E: pip coverage", () => { it(`pip3 successfully validates certificates for HTTPS downloads`, async () => { const shell = await container.openShell("zsh"); - // Clear cache to force network download through proxy - await shell.runCommand("pip3 cache purge"); - const result = await shell.runCommand( "pip3 install --break-system-packages certifi" );