From 515e4556ce8850447aaecd26be6d0cd125f2f83b Mon Sep 17 00:00:00 2001 From: Atlas Date: Thu, 4 Jun 2026 10:24:38 +0200 Subject: Add support for HAProxy certificate fingerprint in PROXY protocol. --- src/modules/m_haproxy.cpp | 101 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/src/modules/m_haproxy.cpp b/src/modules/m_haproxy.cpp index 101204328..1dea83f28 100644 --- a/src/modules/m_haproxy.cpp +++ b/src/modules/m_haproxy.cpp @@ -58,6 +58,9 @@ enum // The identifier for a TLS TLV entry. PP2_TYPE_SSL = 0x20, + // The identifier for a TLS certificate fingerprint TLV entry. + PP2_TYPE_CERTFP = 0xE0, + // The minimum length of a PP2_TYPE_SSL TLV entry. PP2_TYPE_SSL_LENGTH = 5, @@ -150,6 +153,12 @@ private: // The API for interacting with user TLS internals. UserCertificateAPI& sslapi; + // The fingerprint forwarded by the proxy for the client certificate. + std::string certificate_fingerprint; + + // The fake certificate we attach for this connection, if any. + reference certificate; + // The current state of the PROXY parser. HAProxyState state = HPS_WAITING_FOR_HEADER; @@ -172,17 +181,58 @@ private: } // What type of TLV are we parsing? - switch (recvq[start_index]) + switch (static_cast(recvq[start_index])) { case PP2_TYPE_SSL: if (!ReadProxyTLVSSL(sock, start_index + PP2_TLV_LENGTH, length)) return 0; break; + + case PP2_TYPE_CERTFP: + if (!ReadProxyTLVCertFP(sock, start_index + PP2_TLV_LENGTH, length)) + return 0; + break; } return PP2_TLV_LENGTH + length; } + bool ReadProxyTLVCertFP(StreamSocket* sock, size_t start_index, uint16_t buffer_length) + { + // If the socket is not a user socket we don't have to do + // anything with this TLV's information. + if (sock->type != StreamSocket::SS_USER) + return true; + + // The fingerprint must be a non-empty hex-encoded digest of a plausible size. + // A hex string must have an even length and at most 128 chars (64-byte digest). + if (buffer_length == 0 || buffer_length % 2 != 0 || buffer_length > 128) + { + ServerInstance->Logs.Debug(MODNAME, "Ignoring PP2_TYPE_CERTFP TLV with unexpected length {}", buffer_length); + return true; + } + + std::string& recvq = GetRecvQ(); + certificate_fingerprint.assign(&recvq[start_index], buffer_length); + + // Verify every character is a valid hexadecimal digit. + if (!std::all_of(certificate_fingerprint.begin(), certificate_fingerprint.end(), [](unsigned char c) { return std::isxdigit(c); })) + { + ServerInstance->Logs.Debug(MODNAME, "Ignoring PP2_TYPE_CERTFP TLV with non-hex content"); + return true; + } + + ServerInstance->Logs.Debug(MODNAME, "Received certificate fingerprint from HAProxy: {}", certificate_fingerprint); + + // If the SSL TLV was already processed, patch the existing certificate + // with the fingerprint now that we have it. + if (certificate) + { + SetCertificateFingerprint(certificate); + } + return true; + } + bool ReadProxyTLVSSL(StreamSocket* sock, size_t start_index, uint16_t buffer_length) { // A TLS TLV must at least consist of client info (uint8_t) and verification info (uint32_t). @@ -207,19 +257,32 @@ private: if ((recvq[start_index] & PP2_CLIENT_SSL) == 0) return true; - // Create a fake ssl_cert for the user. Ideally we should use the user's - // TLS client certificate here but as of 2018-10-16 this is not forwarded - // by HAProxy. - auto* cert = new ssl_cert(); - cert->error = "HAProxy does not forward client TLS certificates"; - cert->invalid = true; - cert->revoked = true; - cert->trusted = false; - cert->unknownsigner = true; - - // Extract the user for this socket and set their certificate. - LocalUser* luser = static_cast(sock)->user; - sslapi->SetCertificate(luser, cert); + // Create a fake ssl_cert for the user. If the proxy already forwarded a + // certificate fingerprint via PP2_TYPE_CERTFP we use it; otherwise we only + // know the client was connected over TLS. + if (!certificate) + { + auto* cert = new ssl_cert(); + cert->dn = "(unknown)"; + cert->issuer = "(unknown)"; + + if (!certificate_fingerprint.empty()) + { + SetCertificateFingerprint(cert); + } + else + { + cert->error = "HAProxy does not forward client TLS certificates"; + cert->invalid = true; + cert->revoked = true; + cert->trusted = false; + cert->unknownsigner = true; + } + + LocalUser* luser = static_cast(sock)->user; + sslapi->SetCertificate(luser, cert); + certificate = cert; + } return true; } @@ -376,6 +439,16 @@ private: return ReadProxyAddress(sock, destrecvq); } + void SetCertificateFingerprint(ssl_cert* cert) + { + cert->fingerprints.push_back(std::move(certificate_fingerprint)); + cert->invalid = false; + cert->trusted = true; + cert->unknownsigner = false; + cert->revoked = false; + cert->error.clear(); + } + public: HAProxyHook(const std::shared_ptr& Prov, StreamSocket* sock, UserCertificateAPI& api) : IOHookMiddle(Prov) -- cgit v1.3.1-10-gc9f91