Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dmPython_trace.log

# Testing
.pytest_cache/
ssl-modern/

# Local technical notes
TECHNICAL_REPORT.md
Expand Down
17 changes: 12 additions & 5 deletions docs/test-results/2026-09-27-ssl-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions scripts/prepare_dm_modern_ssl_ci.sh
Original file line number Diff line number Diff line change
@@ -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"
61 changes: 61 additions & 0 deletions tests/ssl_modern/test_modern_server_certificate.py
Original file line number Diff line number Diff line change
@@ -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)})
Loading