Skip to content

Repository files navigation

UDS Client Certificate Auth — TLS Certificate Bridge

Lightweight Docker service that acts as a bridge between a TLS client-certificate protected endpoint and the UDS authentication system.

When a user accesses this service with a client certificate, the certificate data is encrypted and forwarded to the UDS server for authentication.

How it works

Browser ──TLS + client cert──▶ nginx (:443) ──proxy_pass──▶ aiohttp (:8080)
│ │
│ encrypts cert
│ returns auto-submit form
▼ │
X-Client-Cert header POST to UDS callback ▼
UDS auth_callback
  1. UDS generates a signed link pointing to this service. The path contains a JSON payload {"url": "...", "ticket": "..."} signed with HMAC-SHA256, where ticket is a unique, single-use identifier that prevents replay attacks.
  2. The browser performs a TLS handshake with its client certificate.
  3. Nginx extracts the certificate and forwards it via headers to the Python app.
  4. The app verifies the HMAC signature, encrypts the certificate together with the ticket (AES-256-CBC + HMAC-SHA256 Encrypt-then-MAC), and returns an auto-submitting HTML form that POSTs the encrypted payload to UDS.
  5. UDS decrypts the payload, verifies the signature, checks the ticket has not been used before (anti-replay), and authenticates the user.

Configuration

The only required configuration is a shared HMAC key, stored in config.yaml:

hmac_key: "your-64-char-hex-string"

Generate one with:

docker run --rm \
-v $(pwd)/config:/app/config \
--entrypoint /usr/local/bin/uv \
client-cert-auth run python scripts/generate_config.py

Endpoint

PathDescription
GET /cert_auth/<signed_url>Receives the client certificate, encrypts it, returns an auto-submitting form that POSTs to the UDS callback URL embedded in the path.
Other pathsEmpty 200 response (no information disclosed).

The <signed_url> component encodes a JSON document with the target URL and a single-use ticket, signed with HMAC-SHA256:

payload = {"url": "https://uds.example.com/.../callback", "ticket": "uuid..."}
data_b64 = base64url(json(payload))
signed_url = data_b64 . "." . hmac_hex(shared_key, data_b64)

The ticket field is a unique identifier (e.g. a UUID) generated by UDS for each authentication request. It is included in the encrypted payload so that UDS can verify it hasn't been used before, providing replay attack protection.

Data sent to UDS

The auto-submitting form POSTs a single field:

FieldContent
payloadAES-256-CBC encrypted + HMAC-SHA256 authenticated JSON (Encrypt-then-MAC), base64-encoded. Contains: cert, host, remote_ip, forwarded_for, ticket.

The encrypted payload decodes to:

{
"cert": "<client certificate PEM or \"EMPTY\">",
"host": "client-cert.example.com",
"remote_ip": "192.168.1.100",
"forwarded_for": "10.0.0.1, 172.16.0.2",
"ticket": "<single-use ticket from signed URL>"
}

Example of the HTML form returned by the service:

<!DOCTYPE html><html><head><metacharset="utf-8"><title>Client Certificate Authentication</title></head><body><p>Redirecting...</p><formmethod="POST" action="https://uds.example.com/uds/page/auth/callback/client_cert/"><inputtype="hidden" name="payload" value="AaDB3kVfn38..."></form><script>document.getElementById('f').submit();</script></body></html>

Build

./build.sh

Run

docker run -d --name client-cert-auth -p 443:443 \
--log-opt max-size=10m --log-opt max-file=3 \
-v $(pwd)/config/config.yaml:/app/config/config.yaml:ro \
-v $(pwd)/certs:/etc/certs:ro \
client-cert-auth

Optional mounts

All of these are auto-generated on startup if not provided:

MountPurposeAuto-generated?
/etc/certs/server.pemServer TLS certificateYes (self-signed)
/etc/certs/key.pemServer private keyYes
/etc/certs/dhparam.pemDH parametersYes (2048-bit)
/etc/nginx/snippets/ssl-params.confCustom SSL configurationFalls back to built-in default

Environment variables (set by entrypoint)

VariableDefaultDescription
CLIENT_CERT_AUTH_LISTEN_HOST127.0.0.1Internal listen address for the Python app
CLIENT_CERT_AUTH_LISTEN_PORT8080Internal listen port
CLIENT_CERT_AUTH_CONFIGconfig/config.yamlPath to configuration file

Testing

Run the test suite with:

uv run pytest tests/ -v

Type checking

This project uses pyright for static type checking:

uv run pyright

Test coverage

ModuleWhat is tested
crypto_utilsAES-256-CBC encrypt/decrypt roundtrip, HMAC integrity verification, tamper detection (IV, ciphertext, MAC), wrong-key rejection, long payloads, deterministic IV
handlerSigned URL encoding/decoding roundtrip, HMAC verification on path params, tamper detection, auto-submit form generation, payload content verification, EMPTY sentinel, catch-all handler
configYAML loading, env var overrides, missing file, missing key, frozen dataclass

Viewing logs

docker logs -f client-cert-auth

License

BSD 3-Clause License. See LICENSE.

About

UDS Smartcard auth web companion

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages