Attesto.MTLS (Attesto v1.15.0)

Copy Markdown View Source

RFC 8705 - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens.

A protected resource that supports mTLS-bound access tokens MUST verify that the access token's confirmation claim (RFC 7800 cnf.x5t#S256) matches the SHA-256 thumbprint of the client certificate presented in the same TLS connection. This module computes that thumbprint and recognises the binding shape.

Thumbprint definition

Per RFC 8705 §3.1 the x5t#S256 value is

base64url(SHA-256(DER-encoded certificate)), no padding

which is the canonical shape validated by Attesto.Thumbprint.

Why we round-trip through :public_key.pkix_decode_cert/2

compute_thumbprint/1 only digests its input after confirming that the bytes parse as an X.509 certificate. A caller that fed in a random binary would otherwise produce a "thumbprint" that no real client certificate could ever match - silently turning the binding into a permanent reject, or (if the binary came from an unauthenticated source) into an attacker-controlled match. Fail closed at the source.

This module is framework-agnostic: no Plug, no database, no application config. It is a pure function of the certificate bytes. A resource server composes Attesto.Token.verify/3 with compute_thumbprint/1 applied to the DER bytes its TLS layer surfaces (e.g. :ssl.peercert/1).

Where the binding may be issued

Whether the listener is even allowed to issue mTLS-bound tokens (the TLS layer is directly terminated and the peer certificate is genuinely the client's, rather than a reverse-proxy socket) is a deployment fact the host application owns. Attesto does not read it from config; the caller decides whether to pass an mTLS thumbprint to Attesto.Token.mint/2 at all.

Summary

Functions

Authenticate an OAuth client certificate according to RFC 8705 §2.

Parse the RFC 8705 client identity values from a DER certificate.

Compute the RFC 8705 §3.1 x5t#S256 thumbprint of an X.509 client certificate from its DER encoding.

Returns true iff the given access-token claims map advertises an mTLS binding via the RFC 8705 cnf.x5t#S256 confirmation claim. Tolerates any non-empty string value (full shape validation happens in Attesto.Token.verify/3).

The expected length, in characters, of a well-formed x5t#S256 thumbprint.

Returns true iff value is a syntactically-valid x5t#S256 thumbprint: the canonical base64url-no-pad encoding of a 32-byte SHA-256 digest. Delegates to Attesto.Thumbprint.valid?/1.

Types

certificate_identities()

@type certificate_identities() :: %{
  subject_dn: String.t(),
  san_dns: [String.t()],
  san_uri: [String.t()],
  san_ip: [String.t()],
  san_email: [String.t()]
}

client_auth_method()

@type client_auth_method() :: :tls_client_auth | :self_signed_tls_client_auth

thumbprint()

@type thumbprint() :: String.t()

Functions

authenticate_client(der, method, client_metadata)

@spec authenticate_client(binary(), client_auth_method() | String.t(), map()) ::
  :ok
  | {:error,
     :invalid_certificate | :invalid_client_metadata | :certificate_mismatch}

Authenticate an OAuth client certificate according to RFC 8705 §2.

For tls_client_auth, client_metadata must contain exactly one of the five RFC 8705 §2.1.2 subject metadata values. Attesto parses and compares that identity; the caller remains responsible for proving possession during the TLS handshake and validating the PKI chain, validity period, and revocation.

For self_signed_tls_client_auth, client_metadata supplies a resolved jwks/"jwks" value. The presented DER certificate must exactly match the leaf certificate in an x5c member registered for the client. A jwks_uri is deliberately not fetched here; network resolution remains host-owned.

certificate_identities(der)

@spec certificate_identities(binary()) ::
  {:ok, certificate_identities()} | {:error, :invalid_certificate}

Parse the RFC 8705 client identity values from a DER certificate.

This is a syntax operation only. A successful result says nothing about the certificate's chain, validity, revocation status, or whether the peer proved possession of its private key.

compute_thumbprint(der)

@spec compute_thumbprint(binary()) ::
  {:ok, thumbprint()} | {:error, :invalid_certificate}

Compute the RFC 8705 §3.1 x5t#S256 thumbprint of an X.509 client certificate from its DER encoding.

Returns {:ok, thumbprint} if the bytes parse as a certificate; {:error, :invalid_certificate} otherwise. The certificate is NOT validated against any trust store, expiry, or revocation status - that is the TLS terminator's responsibility. This function only ensures the bytes ARE a certificate (so we never emit a thumbprint for arbitrary attacker-controlled bytes) and computes the digest.

mtls_bound?(arg1)

@spec mtls_bound?(map()) :: boolean()

Returns true iff the given access-token claims map advertises an mTLS binding via the RFC 8705 cnf.x5t#S256 confirmation claim. Tolerates any non-empty string value (full shape validation happens in Attesto.Token.verify/3).

thumbprint_length()

@spec thumbprint_length() :: pos_integer()

The expected length, in characters, of a well-formed x5t#S256 thumbprint.

thumbprint_shape?(value)

@spec thumbprint_shape?(term()) :: boolean()

Returns true iff value is a syntactically-valid x5t#S256 thumbprint: the canonical base64url-no-pad encoding of a 32-byte SHA-256 digest. Delegates to Attesto.Thumbprint.valid?/1.