Attesto.MTLS (Attesto v0.5.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

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

thumbprint()

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

Functions

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.