From 3cb4042f836af6272a794d0bd57414741c30b50c Mon Sep 17 00:00:00 2001 From: he_sk Date: Sun, 27 Sep 2026 03:48:19 +0800 Subject: [PATCH] test: gate modern SSL certificate verification against DM8 --- .github/workflows/integration-tests.yml | 20 +++++- .gitignore | 1 + .../test-results/2026-09-27-ssl-connection.md | 17 ++++-- scripts/prepare_dm_modern_ssl_ci.sh | 47 ++++++++++++++ .../test_modern_server_certificate.py | 61 +++++++++++++++++++ 5 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 scripts/prepare_dm_modern_ssl_ci.sh create mode 100644 tests/ssl_modern/test_modern_server_certificate.py diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b613140..f3d47a4 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -159,12 +159,30 @@ jobs: TZ: Asia/Shanghai run: python -m pytest -q tests/ssl --junitxml=pytest-dm-ssl.xml + - name: Replace bundled SSL certificate with a SAN certificate + run: bash scripts/prepare_dm_modern_ssl_ci.sh + + - name: Run modern server certificate regression + timeout-minutes: 10 + env: + TZ: Asia/Shanghai + run: | + export DM_SSL_MODERN_HOST="$DM_SSL_TEST_HOST" + export DM_SSL_MODERN_PORT="$DM_SSL_TEST_PORT" + export DM_SSL_MODERN_USER="$DM_SSL_TEST_USER" + export DM_SSL_MODERN_PASSWORD="$DM_SSL_TEST_PASSWORD" + export DM_SSL_MODERN_PATH="$PWD/ssl-modern/client" + export DM_SSL_MODERN_BAD_CA="$PWD/ssl-certs/ca-cert.pem" + python -m pytest -q tests/ssl_modern --junitxml=pytest-dm-ssl-modern.xml + - name: Upload SSL regression evidence if: always() uses: actions/upload-artifact@v4 with: name: real-dm-ssl-arm-py3.10 - path: pytest-dm-ssl.xml + path: | + pytest-dm-ssl.xml + pytest-dm-ssl-modern.xml if-no-files-found: ignore - name: Show SSL database logs on failure diff --git a/.gitignore b/.gitignore index 5baa152..8d1fc4a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ dmPython_trace.log # Testing .pytest_cache/ +ssl-modern/ # Local technical notes TECHNICAL_REPORT.md diff --git a/docs/test-results/2026-09-27-ssl-connection.md b/docs/test-results/2026-09-27-ssl-connection.md index 716def0..d394ea1 100644 --- a/docs/test-results/2026-09-27-ssl-connection.md +++ b/docs/test-results/2026-09-27-ssl-connection.md @@ -14,12 +14,19 @@ cases; 184 passed, 4 skipped, 6 deselected there. GitHub CI supplies the required credential for both database encodings. -## Remaining scope +## Modern server certificate + +An isolated ARM DM8 instance used a short-lived test CA, a server certificate +with `127.0.0.1` in its IP SAN, and a matching SYSDBA client certificate. +Without a `server-cert.pem` pin in the client directory, the ARM macOS Python +3.10 extension connected and queried successfully. Connecting as `localhost` +was rejected by hostname verification, and replacing the trusted CA with an +unrelated CA was rejected as an unknown authority. All three real-database +checks in `tests/ssl_modern` passed locally. The GitHub ARM SSL job now runs +the same checks after replacing the bundled certificates in its isolated +instance. -The local test uses DM8's bundled legacy certificate without SAN, so it -exercises exact certificate pinning. CA and hostname verification for modern -SAN certificates is implemented but needs a server with a modern certificate -for an end-to-end regression. +## Remaining scope The ARM macOS Python 3.10 extension also connected to the SSL-enabled DM8 instance with the bundled RSA client key re-encrypted in traditional PEM diff --git a/scripts/prepare_dm_modern_ssl_ci.sh b/scripts/prepare_dm_modern_ssl_ci.sh new file mode 100644 index 0000000..5325e9e --- /dev/null +++ b/scripts/prepare_dm_modern_ssl_ci.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="${DM_SSL_MODERN_ROOT:-$PWD/ssl-modern}" +container="${DM_SSL_TEST_CONTAINER:-dmpython-ci-dm8-ssl}" +user="${DM_SSL_TEST_USER:-SYSDBA}" +umask 077 +mkdir -p "$root/server" "$root/client" + +openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 2 \ + -keyout "$root/ca-key.pem" -out "$root/ca-cert.pem" \ + -subj '/CN=dmPython CI Test CA' \ + -addext 'basicConstraints=critical,CA:TRUE' \ + -addext 'keyUsage=critical,keyCertSign,cRLSign' >/dev/null 2>&1 +openssl req -newkey rsa:2048 -nodes -sha256 \ + -keyout "$root/server/server-key.pem" -out "$root/server.csr" \ + -subj '/CN=server' >/dev/null 2>&1 +cat > "$root/server.ext" <<'EOF' +subjectAltName=IP:127.0.0.1 +extendedKeyUsage=serverAuth +keyUsage=digitalSignature,keyEncipherment +EOF +openssl x509 -req -in "$root/server.csr" \ + -CA "$root/ca-cert.pem" -CAkey "$root/ca-key.pem" -CAcreateserial \ + -out "$root/server/server-cert.pem" -days 2 -sha256 \ + -extfile "$root/server.ext" >/dev/null 2>&1 + +openssl req -newkey rsa:2048 -nodes -sha256 \ + -keyout "$root/client/client-key.pem" -out "$root/client.csr" \ + -subj "/CN=$user" >/dev/null 2>&1 +cat > "$root/client.ext" <<'EOF' +extendedKeyUsage=clientAuth +keyUsage=digitalSignature,keyEncipherment +EOF +openssl x509 -req -in "$root/client.csr" \ + -CA "$root/ca-cert.pem" -CAkey "$root/ca-key.pem" -CAcreateserial \ + -out "$root/client/client-cert.pem" -days 2 -sha256 \ + -extfile "$root/client.ext" >/dev/null 2>&1 + +cp "$root/ca-cert.pem" "$root/server/ca-cert.pem" +cp "$root/ca-cert.pem" "$root/client/ca-cert.pem" +docker cp "$root/server/." "$container:/opt/dmdbms/bin/server_ssl/" +docker cp "$root/client/." "$container:/opt/dmdbms/bin/client_ssl/$user/" +docker exec "$container" chown -R dmdba:dinstall \ + /opt/dmdbms/bin/server_ssl "/opt/dmdbms/bin/client_ssl/$user" +docker restart "$container" >/dev/null +echo "DM8 restarted with short-lived SAN certificate" diff --git a/tests/ssl_modern/test_modern_server_certificate.py b/tests/ssl_modern/test_modern_server_certificate.py new file mode 100644 index 0000000..19204d4 --- /dev/null +++ b/tests/ssl_modern/test_modern_server_certificate.py @@ -0,0 +1,61 @@ +"""Real DM8 regression for CA and SAN verification after cert replacement.""" + +import os +import shutil +import time +from pathlib import Path + +import dmPython +import pytest + + +@pytest.fixture(scope="module") +def modern_params(): + names = ( + "DM_SSL_MODERN_HOST", + "DM_SSL_MODERN_PORT", + "DM_SSL_MODERN_USER", + "DM_SSL_MODERN_PASSWORD", + "DM_SSL_MODERN_PATH", + "DM_SSL_MODERN_BAD_CA", + ) + missing = [name for name in names if not os.getenv(name)] + if missing: + pytest.fail(f"modern SSL environment is incomplete: {', '.join(missing)}") + params = { + "server": os.environ["DM_SSL_MODERN_HOST"], + "port": int(os.environ["DM_SSL_MODERN_PORT"]), + "user": os.environ["DM_SSL_MODERN_USER"], + "password": os.environ["DM_SSL_MODERN_PASSWORD"], + "ssl_path": os.environ["DM_SSL_MODERN_PATH"], + } + assert not (Path(params["ssl_path"]) / "server-cert.pem").exists() + deadline = time.monotonic() + 90 + while True: + try: + with dmPython.connect(**params): + return params + except dmPython.Error: + if time.monotonic() >= deadline: + pytest.fail("DM8 did not accept the modern certificate within 90 seconds") + time.sleep(2) + + +def test_trusted_ip_san_connects(modern_params): + with dmPython.connect(**modern_params) as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1") + assert cur.fetchone() == (1,) + + +def test_wrong_hostname_is_rejected(modern_params): + with pytest.raises(dmPython.Error, match=r"x509:.*localhost"): + dmPython.connect(**{**modern_params, "server": "localhost"}) + + +def test_untrusted_ca_is_rejected(modern_params, tmp_path): + cert_dir = tmp_path / "untrusted-ca" + shutil.copytree(modern_params["ssl_path"], cert_dir) + shutil.copyfile(os.environ["DM_SSL_MODERN_BAD_CA"], cert_dir / "ca-cert.pem") + with pytest.raises(dmPython.Error, match="unknown authority"): + dmPython.connect(**{**modern_params, "ssl_path": str(cert_dir)})