Skip to main content

ADR-028: RFC 8705 Certificate Binding Validation

Status​

Accepted

Date​

2026-02-01

Context​

STOA supports mTLS for API client authentication using RFC 8705 Certificate-Bound Access Tokens. JWT tokens contain a cnf.x5t#S256 claim with the client certificate's SHA-256 fingerprint. The MCP Gateway must validate that the certificate presented by the client (via the TLS-terminating proxy header) matches the fingerprint bound to the JWT.

The challenge: different systems in the chain use different fingerprint formats.

SystemFormatExample
RFC 8705 JWT (cnf.x5t#S256)base64urlhLmVMvjnmCm_I4d8_3nw5A
F5 / Nginx (default)hex lowercase84b99532f8e79829bf23877cff79f0e4
OpenSSL CLI outputhex with colons84:B9:95:32:F8:E7:98:29:BF:23:87:7C:FF:79:F0:E4

Additionally, the header name injected by the TLS-terminating proxy varies per deployment (X-SSL-Client-Cert-SHA256, X-Client-Cert-Fingerprint, SSL_CLIENT_FINGERPRINT, etc.).

  • ADR-027: X509 Header-Based Authentication β€” header contract between F5 and Keycloak
  • ADR-011: API Security Modes β€” mTLS for CORE internal APIs
  • CAB-868: Certificate Binding Metrics
  • CAB-1024: UAC-Driven Certificate Binding Policy

Decision​

1. Normalize to hex lowercase for all comparisons​

All three fingerprint formats are normalized to lowercase hex (no separators) before comparison. This avoids case-sensitivity issues and format ambiguity.

base64url  β†’  decode  β†’  hex lowercase
hex upper β†’ lower β†’ hex lowercase
hex:colons β†’ strip β†’ hex lowercase

2. Timing-safe comparison​

All fingerprint comparisons use secrets.compare_digest() to prevent timing-based side-channel attacks. This applies to both the MCP Gateway middleware and the shared fingerprint_utils module.

3. Auto-detect format when not specified​

detect_format(fingerprint) inspects the string pattern:

  • Contains : β†’ hex_colons
  • Matches ^[a-fA-F0-9]+$ β†’ hex
  • Otherwise β†’ base64url (with charset validation: ^[A-Za-z0-9\-_]+$)

4. UAC-driven configuration​

All certificate binding parameters are configurable per tenant via the UAC (Universal API Contract) schema:

UAC PathTypeDefaultDescription
security.authentication.mtls.cert_binding.enabledbooleantrueEnable cert binding validation
security.authentication.mtls.cert_binding.header_namestringX-SSL-Client-Cert-SHA256Header from TLS proxy
security.authentication.mtls.cert_binding.fingerprint_formatenumhexFormat: base64url, hex, hex_colons
security.authentication.mtls.cert_binding.jwt_claimstringcnf.x5t#S256JWT claim path
security.authentication.mtls.cert_binding.strict_modebooleantrueReject if cnf present but header absent

5. Centralized utilities​

All format conversion logic lives in control-plane-api/src/services/fingerprint_utils.py:

  • FingerprintFormat enum
  • detect_format(), normalize_to_hex(), hex_to_base64url()
  • fingerprints_match() β€” timing-safe cross-format comparison

The MCP Gateway duplicates normalization inline (cannot import from control-plane-api), but follows the same algorithm.

6. webMethods policy generation​

The UAC cert_binding config generates a webMethods requestProcessing policy action via mappers.py, passing header name, format, JWT claim, and strict mode as input mapping parameters.

Consequences​

Positive​

  • Works with any F5/LB configuration β€” header name and format are configurable, not hardcoded
  • No code changes per deployment β€” UAC schema drives all configuration
  • Secure against timing attacks β€” secrets.compare_digest throughout
  • Cross-format matching β€” same fingerprint in hex, base64url, or colons all match correctly
  • Feature flag β€” cert_binding_enabled allows progressive rollout

Negative​

  • Three format conversions β€” must handle base64url, hex, and hex_colons correctly
  • UAC schema complexity β€” adds a nested cert_binding object to security.authentication.mtls
  • Duplicated normalization β€” MCP Gateway and control-plane-api each have their own implementation (different runtimes)

Risks​

  • Base64url ambiguity β€” a hex string like abcdef1234 could be misdetected as hex when it's actually base64url. Mitigated by explicit format hints in UAC config.
  • Header spoofing β€” if the proxy header is not stripped from external requests, an attacker could inject a forged fingerprint. Mitigated by F5/Nginx configuration (trusted proxy only).