Skip to content

RequestServer: Support TLS client certificate authentication #8343

@kwinsch

Description

@kwinsch

Summary

Ladybird has IPC infrastructure for client certificate authentication (certificate_requested / set_certificate in RequestServer.ipc and RequestClient.ipc), with callbacks wired through LibRequests::Request and ResourceLoader. However, the server-side implementation is non-functional: ConnectionFromClient::set_certificate() unconditionally calls TODO(), and the browser-side callback in ResourceLoader.cpp returns an empty CertificateAndKey.

It seems, that this infrastructure was originally built for the internal LibTLS implementation and has not been adapted for the cURL backend. I would like to make it functional and am opening this issue to discuss the design before writing any code.

Problem: timing mismatch between LibTLS and cURL

The existing IPC flow was designed for LibTLS, which could pause a TLS handshake mid-flight:

  1. LibTLS encounters the server's CertificateRequest message and pauses.
  2. RequestServer sends certificate_requested(request_id) to the browser.
  3. The browser responds with set_certificate(request_id, cert, key).
  4. LibTLS resumes the handshake with the certificate.

cURL does not support pausing the TLS handshake. CURLOPT_SSLCERT and CURLOPT_SSLKEY must be configured on the easy handle before it is added to the multi handle. In the current code, Request::handle_fetch_state() sets all cURL options and calls curl_multi_add_handle() synchronously. By the time a set_certificate IPC response could arrive from the browser, the handshake is already underway or complete.

Proposed approach

Add a RequestClientCertificate state to the Request state machine, placed between RetrieveCookie and Fetch. This follows the same async IPC round-trip pattern that cookie retrieval already uses:

Init -> DNSLookup -> RetrieveCookie -> RequestClientCertificate -> Fetch

The flow would be:

  1. handle_request_client_certificate_state() sends certificate_requested(request_id) to the browser via the primary connection.
  2. The browser invokes its on_certificate_requested callback, checks for a matching certificate, and responds with set_certificate().
  3. ConnectionFromClient::set_certificate() stores the certificate and key on the Request object and transitions to Fetch.
  4. handle_fetch_state() applies CURLOPT_SSLCERT_BLOB and CURLOPT_SSLKEY_BLOB (available since cURL 7.71.0; Ladybird ships 8.18.0) if a certificate was provided.

This reuses the existing IPC messages without protocol changes.

Performance

This adds one IPC round-trip per network request. Possible mitigations:

  • Only enter the state for HTTPS URLs.
  • If the browser has no certificates configured at all, respond immediately without blocking.
  • Alternatively, a pre-registration model (browser registers certificates per host via a separate IPC) could avoid per-request queries entirely.

Scope

I am thinking of this in two parts:

  1. cURL wiring: the new state, the set_certificate() implementation, CURLOPT_SSLCERT_BLOB/CURLOPT_SSLKEY_BLOB in handle_fetch_state(). The browser side would return an empty certificate (no certificate store yet). This makes the existing infrastructure functional end-to-end.

  2. Browser-side certificate management: replace the empty lambda in ResourceLoader.cpp with an actual certificate lookup. Certificate import, selection UI, and persistent storage would be separate work.

Questions for maintainers

  • Is the RequestClientCertificate state approach acceptable, or is a different design preferred (for example, pre-registration or CURLOPT_SSL_CTX_FUNCTION with a backend-specific callback)?
  • Are there concerns about the additional IPC round-trip per HTTPS request?
  • Should the WebSocket path (WebSocketImplCurl) be included in the first change or handled separately?

Relevant code

Location Current state
Services/RequestServer/ConnectionFromClient.cpp:325 set_certificate() calls TODO()
Services/RequestServer/ConnectionFromClient.cpp:425 WebSocket set_certificate is a no-op
Services/RequestServer/Request.cpp:499 handle_fetch_state() — cURL TLS setup, no SSLCERT/SSLKEY
Libraries/LibWeb/Loader/ResourceLoader.cpp:480 on_certificate_requested returns empty
Services/RequestServer/RequestServer.ipc set_certificate(request_id, certificate, key) => (bool success)
Services/RequestServer/RequestClient.ipc certificate_requested(request_id) =|

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions