diff --git a/.tool-versions b/.tool-versions index 08bec1cb..f45c708f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ python 3.11 -uv 0.10.0 +uv 0.13.4 # Keep in sync with `packageManager` in package.json — pnpm self-switches to # that version at runtime. pnpm 10.34.5 diff --git a/template/requirements.txt b/template/requirements.txt index 3e453aa1..da4ae1a3 100644 --- a/template/requirements.txt +++ b/template/requirements.txt @@ -42,3 +42,4 @@ urllib3==2.7.0 xarray==2025.4.0 xlrd==2.0.2 sympy==1.14.0 +certifi==2026.10.19 diff --git a/template/server/requirements.txt b/template/server/requirements.txt index b3e44b71..6d99e0d8 100644 --- a/template/server/requirements.txt +++ b/template/server/requirements.txt @@ -4,3 +4,4 @@ websockets==12.0 uvicorn[standard]==0.30.1 requests==2.33.0 pydantic==2.9.1 +certifi==2026.10.19 diff --git a/template/start-up.sh b/template/start-up.sh index 0ea0494d..3c6f11ed 100755 --- a/template/start-up.sh +++ b/template/start-up.sh @@ -1,5 +1,7 @@ #!/bin/bash +export NODE_OPTIONS="--use-openssl-ca${NODE_OPTIONS:+ $NODE_OPTIONS}" + install -d -m 0700 /run/e2b-jupyter function start_code_interpreter() { diff --git a/template/systemd/jupyter.service b/template/systemd/jupyter.service index 178124bd..a55040b1 100644 --- a/template/systemd/jupyter.service +++ b/template/systemd/jupyter.service @@ -8,6 +8,7 @@ StartLimitBurst=0 Type=simple RuntimeDirectory=e2b-jupyter RuntimeDirectoryMode=0700 +Environment=NODE_OPTIONS=--use-openssl-ca Environment=MATPLOTLIBRC=/root/.config/matplotlib/.matplotlibrc ExecStart=/usr/local/bin/jupyter server --IdentityProvider.token="" ExecStartPost=-/usr/bin/systemctl reset-failed code-interpreter diff --git a/template/template.py b/template/template.py index fd8dd937..c68541a5 100644 --- a/template/template.py +++ b/template/template.py @@ -17,6 +17,7 @@ def make_template( .set_envs( { "PIP_DEFAULT_TIMEOUT": "100", + "NODE_OPTIONS": "--use-openssl-ca", "PIP_DISABLE_PIP_VERSION_CHECK": "1", "PIP_NO_CACHE_DIR": "1", "JAVA_VERSION": "11", @@ -34,11 +35,18 @@ def make_template( "jq", "sudo", "fonts-noto-cjk", - "ca-certificates", + "ca-certificates=20250419+deb13u1", ] ) + .run_cmd( + "python -m pip install --use-deprecated=legacy-certs" + " --cert /etc/ssl/certs/ca-certificates.crt --upgrade pip==26.2.2" + ) .run_cmd("curl -fsSL https://deb.nodesource.com/setup_20.x | bash -") .apt_install("nodejs") + .run_cmd( + "echo 'export NODE_OPTIONS=\"--use-openssl-ca${NODE_OPTIONS:+ $NODE_OPTIONS}\"' > /etc/profile.d/node-ca.sh" + ) .copy("requirements.txt", "requirements.txt") .pip_install("--no-cache-dir -r requirements.txt") ) @@ -74,6 +82,7 @@ def make_template( [ "mkdir -p /usr/lib/jvm", "curl -fsSL https://download.java.net/java/ga/jdk${JAVA_VERSION}/openjdk-${JAVA_VERSION}_linux-x64_bin.tar.gz | tar -xz -C /usr/lib/jvm", + "/usr/lib/jvm/jdk-${JAVA_VERSION}/bin/keytool -delete -cacerts -storepass changeit -alias 'comodoaaaca [jdk]'", "update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-${JAVA_VERSION}/bin/java 1", "update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk-${JAVA_VERSION}/bin/javac 1", "wget https://github.com/SpencerPark/IJava/releases/download/v${IJAVA_VERSION}/ijava-${IJAVA_VERSION}.zip", @@ -88,6 +97,10 @@ def make_template( # Create server virtual environment .copy("server", ".server") .run_cmd("python -m venv .server/.venv") + .run_cmd( + ".server/.venv/bin/python -m pip install --use-deprecated=legacy-certs" + " --cert /etc/ssl/certs/ca-certificates.crt --upgrade pip==26.2.2" + ) # Copy and install server requirements .run_cmd( ".server/.venv/bin/pip install --no-cache-dir -r .server/requirements.txt" diff --git a/tests/test_certificates.py b/tests/test_certificates.py new file mode 100644 index 00000000..417f02bc --- /dev/null +++ b/tests/test_certificates.py @@ -0,0 +1,106 @@ +import json +import shlex + +import pytest +from e2b import Sandbox + +from harness import CodeInterpreter + +pytestmark = pytest.mark.skip_debug + +CHECK_CERTIFICATES = """ +import ssl +from importlib.metadata import version +from pathlib import Path + +import certifi +from pip._vendor import certifi as pip_certifi + +assert tuple(map(int, version('certifi').split('.'))) >= (2026, 10, 19) +assert tuple(map(int, version('pip').split('.'))) >= (26, 2, 2) + +blocked = {'emSign ECC Root CA - G3', 'AAA Certificate Services'} +for bundle in (None, certifi.where(), pip_certifi.where()): + context = ssl.create_default_context(cafile=bundle) + if bundle is None: + for path in Path('/etc/ssl/certs').iterdir(): + if path.is_file(): + try: + context.load_verify_locations(cafile=str(path)) + except ssl.SSLError: + pass + certificates = context.get_ca_certs() + assert certificates, f'Empty trust store: {bundle}' + names = { + value + for certificate in certificates + for rdn in certificate['subject'] + for key, value in rdn + if key == 'commonName' + } + assert not names & blocked, f'{bundle}: still trusts {names & blocked}' +print('Certificate stores verified') +""" + + +@pytest.mark.parametrize("python", ["python", "/root/.server/.venv/bin/python"]) +def test_certificate_stores(sandbox: Sandbox, python: str): + result = sandbox.commands.run( + f"{python} -c {shlex.quote(CHECK_CERTIFICATES)}", user="root" + ) + assert result.exit_code == 0 + assert "Certificate stores verified" in result.stdout + + +CHECK_NODE_HTTPS = """ +(async () => { +const https = require('node:https'); +const probe = (host) => new Promise((resolve, reject) => { + const request = https.get(`https://${host}/`, (response) => { + response.resume(); + resolve('connected'); + }); + request.setTimeout(15000, () => request.destroy(new Error('TLS probe timed out'))); + request.on('error', (error) => { + if (error.code === 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY' || + error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' || + error.code === 'SELF_SIGNED_CERT_IN_CHAIN') { + resolve('untrusted'); + } else { + reject(error); + } + }); +}); +const results = await Promise.all([ + probe('dv-g3b-emudhra.we-have-a-problem.com'), + probe('dv-g3b-aaa-emudhra.we-have-a-problem.com'), + probe('nodejs.org'), +]); +console.log(JSON.stringify(results)); +})() +""" + + +def test_node_certificate_stores(sandbox: Sandbox, client: CodeInterpreter): + command = CHECK_NODE_HTTPS + result = sandbox.commands.run(f"node -e {shlex.quote(command)}") + assert json.loads(result.stdout) == ["untrusted", "untrusted", "connected"] + + execution = client.run_code("await " + command, language="js") + assert execution.error is None + assert json.loads("".join(execution.stdout)) == [ + "untrusted", + "untrusted", + "connected", + ] + + +def test_java_certificate_store(sandbox: Sandbox): + result = sandbox.commands.run( + "/usr/lib/jvm/jdk-11/bin/keytool -J-Duser.language=en -list -v -cacerts -storepass changeit", + user="root", + ) + assert result.exit_code == 0 + assert "trustedCertEntry" in result.stdout + assert "CN=AAA Certificate Services," not in result.stdout + assert "CN=emSign ECC Root CA - G3," not in result.stdout