Skip to content

TofuVerifier performs no verification: accepts any certificate and any handshake signature, defeating TLS authentication for all renderer connections #364

Description

@forkwright

Finding

The custom ServerCertVerifier used for every renderer-to-server QUIC connection performs no verification of any kind. TofuVerifier::verify_server_cert unconditionally returns ServerCertVerified::assertion(), and both verify_tls12_signature and verify_tls13_signature unconditionally return HandshakeSignatureValid::assertion() without evaluating the signature over the TLS handshake transcript. This defeats both halves of TLS peer authentication at once:

  • The handshake-signature stubs mean the peer is never required to prove possession of the private key matching the presented certificate.
  • The verify_server_cert stub means the certificate's identity is never checked against any stored value, so the server certificate fingerprint that the protocol exchanges for exactly this purpose (PairingChallenge.cert_fingerprint) is never consulted by the TLS layer.

The TOFU model legitimately skips CA-chain validation, but it must still (a) verify the handshake signature to bind the session to the private-key holder and (b) pin the certificate fingerprint after first contact. TofuVerifier does neither.

Evidence

crates/syndesis/src/tls/mod.rs:163-165 — the verifier is documented as accepting any certificate and is a unit struct holding no pinned fingerprint:

/// TOFU certificate verifier that accepts any certificate on first contact.
#[derive(Debug)]
struct TofuVerifier;

crates/syndesis/src/tls/mod.rs:176 — server certificate accepted unconditionally; _end_entity and the OCSP/name inputs are all ignored:

        Ok(rustls::client::danger::ServerCertVerified::assertion())

crates/syndesis/src/tls/mod.rs:185 — TLS 1.2 handshake signature accepted without checking _message, _cert, or _dss:

        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())

crates/syndesis/src/tls/mod.rs:194 — same for TLS 1.3 (the live path: QUIC mandates TLS 1.3):

        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())

crates/syndesis/src/tls/mod.rs:208 — this stub is wired in as the certificate verifier for every client connection:

        .with_custom_certificate_verifier(Arc::new(TofuVerifier))

crates/syndesis/src/protocol/session_frame.rs:35-40 — the protocol already transports the value the verifier should pin, but nothing feeds it to TofuVerifier:

pub struct PairingChallenge {
    /// Human-readable server name.
    pub server_name: String,
    /// Hex-encoded SHA-256 fingerprint of the server's TLS certificate.
    pub cert_fingerprint: String,
}

Why this matters

Under the counter-surveillance threat model the renderer link must withstand an active on-path adversary. As written, two independent active-MitM paths both succeed:

  1. An attacker holding only the server's public certificate (no private key) can present it during the QUIC handshake with their own ephemeral key; the signature stubs accept it.
  2. An attacker holding their own certificate and key (a different fingerprint) can present it on any connection, including a post-pairing reconnect; the verify_server_cert stub accepts it because the pinned fingerprint is never checked.

Either way the adversary terminates the renderer's TLS session. The PairingComplete frame and every SessionInit frame carry the API key inside that session, so the attacker captures the key and can then authenticate to the real server as a legitimate renderer. The server-side TOFU check (fingerprint stored at pairing, verified on authenticate) cannot help, because the channel is already compromised at the TLS layer before any application frame is sent.

Desired correction

Replace the no-op TofuVerifier with a verifier that performs real authentication while still skipping only the CA-chain check:

  • Hold an optional pinned SHA-256 fingerprint, supplied at construction (sourced from PairingChallenge.cert_fingerprint).
  • In verify_server_cert, compute the SHA-256 fingerprint of end_entity; when a pin is present, return Err(rustls::Error::General(...)) on mismatch; on first contact (no pin), accept and surface the observed fingerprint to the caller for storage.
  • Implement verify_tls12_signature / verify_tls13_signature by delegating to the provider's signature-verification algorithms (e.g. via rustls::crypto::ring::default_provider().signature_verification_algorithms) instead of asserting validity.

Done when: a certificate presented with a private key that does not match it is rejected during the handshake; a client configured with pinned fingerprint A rejects a server presenting a certificate with fingerprint B while accepting the original certificate; and tests cover both the wrong-key and wrong-fingerprint rejection cases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions