Safeguard
Application Security•

ASVS 5.0 V12.1.3: Validate mTLS Client Certificates Before Trust

ASVS 5.0 V12.1.3 (Level 2): verify an mTLS client certificate chains to a trusted CA before using its identity for authentication or authorization.

Safeguard Security Team
3 min read

ASVS 5.0 V12.1.3 says that when you use mutual TLS, you must confirm the client certificate is trusted before you read any identity from it and use that identity to authenticate or authorize. It is a Level 2 requirement.

The common failure is a server that asks for a certificate but does not verify it, then reads the common name from the subject and believes it. Anyone can mint a certificate that says CN=billing-service.

Why it matters

A certificate is only a signed claim. The identity inside means something only if the signature chains to a CA you chose to trust, the certificate is within its validity dates and it is allowed for client authentication. Skip the chain check and mTLS is theater: the handshake succeeds and the identity is attacker supplied.

How to implement it

Make the TLS layer enforce verification, then map identity from the verified chain only.

Go server:

pool := x509.NewCertPool()
pem, err := os.ReadFile("/etc/pki/internal-clients-ca.pem")
if err != nil { log.Fatal(err) }
if !pool.AppendCertsFromPEM(pem) { log.Fatal("no CA certs loaded") }

srv := &http.Server{
    Addr: ":8443",
    TLSConfig: &tls.Config{
        MinVersion: tls.VersionTLS12,
        ClientAuth: tls.RequireAndVerifyClientCert,
        ClientCAs:  pool,
    },
}

RequireAndVerifyClientCert is the setting that matters. RequestClientCert and VerifyClientCertIfGiven both let unverified or missing certificates through to your handler.

nginx:

ssl_client_certificate /etc/pki/internal-clients-ca.pem;
ssl_verify_client on;
ssl_verify_depth 2;

When nginx fronts the app, pass identity upstream only from $ssl_client_s_dn after ssl_verify_client on, and strip any client supplied copy of that header. Prefer a SAN URI or DNS name over the subject common name for the identity, and check an allowlist of identities per route.

How to test it

  1. Connect with no certificate. The handshake or the first request must fail.
  2. Connect with a self signed certificate that has a valid looking subject. It must fail.
  3. Connect with a certificate from a different CA. It must fail.
  4. Connect with an expired certificate. It must fail.
curl --cert rogue.crt --key rogue.key https://svc.internal:8443/

Add these as integration tests so a refactor to the TLS config cannot loosen them.

Common mistakes

  • Using ssl_verify_client optional and never checking $ssl_client_verify in the app.
  • Trusting a header like X-Client-DN that the client can send itself.
  • Loading the whole system trust store as the client CA pool, so any public CA certificate is accepted.
  • Authorizing on the subject common name alone.

Quick check

Is asking for a client certificate the same as verifying it?

No. The server must also validate the chain, dates and usage. Use the mode that requires and verifies.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.