Skip to content

VirtualCable/client-cert-web-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

Path Description
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 paths Empty 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:

Field Content
payload AES-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><meta charset="utf-8"><title>Client Certificate Authentication</title></head>
<body>
    <p>Redirecting...</p>
    <form method="POST" action="https://uds.example.com/uds/page/auth/callback/client_cert/">
        <input type="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:

Mount Purpose Auto-generated?
/etc/certs/server.pem Server TLS certificate Yes (self-signed)
/etc/certs/key.pem Server private key Yes
/etc/certs/dhparam.pem DH parameters Yes (2048-bit)
/etc/nginx/snippets/ssl-params.conf Custom SSL configuration Falls back to built-in default

Environment variables (set by entrypoint)

Variable Default Description
CLIENT_CERT_AUTH_LISTEN_HOST 127.0.0.1 Internal listen address for the Python app
CLIENT_CERT_AUTH_LISTEN_PORT 8080 Internal listen port
CLIENT_CERT_AUTH_CONFIG config/config.yaml Path 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

Module What is tested
crypto_utils AES-256-CBC encrypt/decrypt roundtrip, HMAC integrity verification, tamper detection (IV, ciphertext, MAC), wrong-key rejection, long payloads, deterministic IV
handler Signed URL encoding/decoding roundtrip, HMAC verification on path params, tamper detection, auto-submit form generation, payload content verification, EMPTY sentinel, catch-all handler
config YAML 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

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors