From 297a264fe0371a3d513108c31fe5eb631aa6213f Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 3 Dec 2025 15:40:02 -0800 Subject: [PATCH] Adapt per comments --- .../safe-chain/src/registryProxy/certUtils.js | 44 +++---------------- .../interceptors/pipInterceptor.js | 23 +++++----- .../interceptors/pipInterceptor.spec.js | 10 +++++ 3 files changed, 27 insertions(+), 50 deletions(-) diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index 6e75954..4206b28 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -115,41 +115,20 @@ function loadCa() { const keyPath = path.join(certFolder, "ca-key.pem"); const certPath = path.join(certFolder, "ca-cert.pem"); - let existingPrivateKey = null; - if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { const privateKeyPem = fs.readFileSync(keyPath, "utf8"); const certPem = fs.readFileSync(certPath, "utf8"); const privateKey = forge.pki.privateKeyFromPem(privateKeyPem); const certificate = forge.pki.certificateFromPem(certPem); - existingPrivateKey = privateKey; - // Don't return a cert that is valid for less than 1 hour - // Some extensions were added in a later phase, ensure it has them or regenerate const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); - /** @type {any} */ - const basicConstraints = certificate.getExtension("basicConstraints"); - const hasCriticalBasicConstraints = Boolean( - basicConstraints && basicConstraints.critical - ); - const hasSubjectKeyIdentifier = Boolean( - certificate.getExtension("subjectKeyIdentifier") - ); - const hasAuthorityKeyIdentifier = Boolean( - certificate.getExtension("authorityKeyIdentifier") - ); - if ( - certificate.validity.notAfter > oneHourFromNow && - hasCriticalBasicConstraints && - hasSubjectKeyIdentifier && - hasAuthorityKeyIdentifier - ) { + if (certificate.validity.notAfter > oneHourFromNow) { return { privateKey, certificate }; } } - const { privateKey, certificate } = generateCa(existingPrivateKey || undefined); + const { privateKey, certificate } = generateCa(); fs.mkdirSync(certFolder, { recursive: true }); fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey)); fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate)); @@ -157,21 +136,8 @@ function loadCa() { return { privateKey, certificate }; } -/** - * Reconstruct the public key from the existing private key so renewed/self-signed CA certificates keep the same key material, - * preserving SKI/AKI continuity - * @param {forge.pki.PrivateKey} [existingPrivateKey] - */ -function generateCa(existingPrivateKey) { - const keys = existingPrivateKey - ? { - privateKey: existingPrivateKey, - publicKey: forge.pki.setRsaPublicKey( - /** @type {any} */(existingPrivateKey).n, - /** @type {any} */(existingPrivateKey).e - ) - } - : forge.pki.rsa.generateKeyPair(2048); +function generateCa() { + const keys = forge.pki.rsa.generateKeyPair(2048); const cert = forge.pki.createCertificate(); cert.publicKey = keys.publicKey; @@ -205,7 +171,7 @@ function generateCa(existingPrivateKey) { keyIdentifier, }, ]); - cert.sign(/** @type {any} */(keys.privateKey), forge.md.sha256.create()); + cert.sign(keys.privateKey, forge.md.sha256.create()); return { privateKey: keys.privateKey, diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js index 8976bf5..9a122a6 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.js @@ -8,9 +8,6 @@ const knownPipRegistries = [ "pythonhosted.org", ]; -// Pattern for sdist extensions -const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i; - /** * @param {string} url * @returns {import("./interceptorBuilder.js").Interceptor | undefined} @@ -40,8 +37,9 @@ function buildPipInterceptor(registry) { // Per python, packages that differ only by hyphen vs underscore are considered the same. const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName; - const isMalicious = await isMalwarePackage(packageName, version) - || await isMalwarePackage(hyphenName, version); + const isMalicious = + await isMalwarePackage(packageName, version) + || await isMalwarePackage(hyphenName, version); if (isMalicious) { reqContext.blockMalware(packageName, version); @@ -83,17 +81,20 @@ function parsePipPackageFromUrl(url, registry) { // Example sdist: https://files.pythonhosted.org/packages/xx/yy/requests-2.28.1.tar.gz // Wheel (.whl) and Poetry's preflight metadata (.whl.metadata) - if (filename.endsWith(".whl") || filename.endsWith(".whl.metadata")) { - const base = filename.endsWith(".whl") - ? filename.slice(0, -4) - : filename.slice(0, -".whl.metadata".length); + // Examples: + // foo_bar-2.0.0-py3-none-any.whl + // foo_bar-2.0.0-py3-none-any.whl.metadata + const wheelExtRe = /\.whl(?:\.metadata)?$/; + const wheelExtMatch = filename.match(wheelExtRe); + if (wheelExtMatch) { + const base = filename.replace(wheelExtRe, ""); const firstDash = base.indexOf("-"); if (firstDash > 0) { const dist = base.slice(0, firstDash); // may contain underscores const rest = base.slice(firstDash + 1); // version + the rest of tags const secondDash = rest.indexOf("-"); const rawVersion = secondDash >= 0 ? rest.slice(0, secondDash) : rest; - packageName = dist; // preserve underscores + packageName = dist; version = rawVersion; // Reject "latest" as it's a placeholder, not a real version // When version is "latest", this signals the URL doesn't contain actual version info @@ -106,6 +107,7 @@ function parsePipPackageFromUrl(url, registry) { } // Source dist (sdist) and potential metadata sidecars (e.g., .tar.gz.metadata) + const sdistExtWithMetadataRe = /\.(tar\.gz|zip|tar\.bz2|tar\.xz)(\.metadata)?$/i; const sdistExtMatch = filename.match(sdistExtWithMetadataRe); if (sdistExtMatch) { const base = filename.replace(sdistExtWithMetadataRe, ""); @@ -122,7 +124,6 @@ function parsePipPackageFromUrl(url, registry) { return { packageName, version }; } } - // Unknown file type or invalid return { packageName: undefined, version: undefined }; } diff --git a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js index eb99f08..482a800 100644 --- a/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js +++ b/packages/safe-chain/src/registryProxy/interceptors/pipInterceptor.spec.js @@ -34,6 +34,11 @@ describe("pipInterceptor", async () => { url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0-py3-none-any.whl", expected: { packageName: "foo-bar", version: "2.0.0" }, }, + { + // Poetry preflight metadata alongside wheel (.whl.metadata) + url: "https://files.pythonhosted.org/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl.metadata", + expected: { packageName: "foo-bar", version: "2.0.0" }, + }, { url: "https://files.pythonhosted.org/packages/xx/yy/foo_bar-2.0.0-py3-none-any.whl", expected: { packageName: "foo-bar", version: "2.0.0" }, @@ -46,6 +51,11 @@ describe("pipInterceptor", async () => { url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0b1.tar.gz", expected: { packageName: "foo-bar", version: "2.0.0b1" }, }, + { + // sdist with metadata sidecar (.tar.gz.metadata) + url: "https://files.pythonhosted.org/packages/xx/yy/foo_bar-2.0.0.tar.gz.metadata", + expected: { packageName: "foo-bar", version: "2.0.0" }, + }, { url: "https://pypi.org/packages/source/f/foo_bar/foo_bar-2.0.0rc1.tar.gz", expected: { packageName: "foo-bar", version: "2.0.0rc1" },