all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Shannon Sterz <s.sterz@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox 3/3] http: tls: add integration tests for openssl verify callbacks
Date: Thu, 25 Jun 2026 13:22:36 +0200	[thread overview]
Message-ID: <20260625112236.188257-3-s.sterz@proxmox.com> (raw)
In-Reply-To: <20260625112236.188257-1-s.sterz@proxmox.com>

these integration tests intend to clearly encode how TLS verification
is handled. they work by spawning a TLS server via `openssl s_server`
and connecting to it. to avoid having the tests fail due to there not
being an open port, a unix socket is used for the connection.

encoded in these tests is the following behaviour for the callback:

1. if a single certificate is used by the server (for example, a
   self-signed certificate):
    a. if the fingerprint matches the certificate -> valid connection
    b. if no fingerprint was provided -> fall back to OpenSSL's checks
2. if a certificate chain was provided by the server:
    a. if the fingerprint matches any certificate in the chain ->
       valid connection
    b. if no fingerprint was provided -> fall back to OpenSSL's checks

tests for the new and old behavior differ in one key way: the old
behavior accepted connections that did not provide a certificate
(chain) matching the fingerprint but were valid according to OpenSSL.
this broke the implicit assumption that providing a fingerprint acted
like "pinning" a certificate. the new behavior does not accept such
connections.

note that 2.a.) technically specifies new, or at least previously
undefined behavior. previously a fingerprint was only checked against
a leaf certificate of a chain. however, pinning an (intermediate) ca
instead of the specific leaf certificate represents a valid use-case.
adding this ability to both, the new and old behavior makes sense as
it only allows more flexibility and, thus, would not break any
existing setups.

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
---
 Cargo.toml                                    |   1 +
 proxmox-http/Cargo.toml                       |   2 +
 proxmox-http/tests/certs/cert-chain.pem       |  46 ++
 .../tests/certs/intermediate-cert.pem         |  23 +
 proxmox-http/tests/certs/intermediate-csr.pem |  17 +
 proxmox-http/tests/certs/intermediate-key.pem |  28 ++
 proxmox-http/tests/certs/leaf-cert.pem        |  24 +
 proxmox-http/tests/certs/leaf-csr.pem         |  17 +
 proxmox-http/tests/certs/leaf-key.pem         |  28 ++
 proxmox-http/tests/certs/root-cert.pem        |  23 +
 proxmox-http/tests/certs/root-key.pem         |  28 ++
 proxmox-http/tests/certs/self-signed-cert.pem |  23 +
 proxmox-http/tests/certs/self-signed-key.pem  |  28 ++
 proxmox-http/tests/common/mod.rs              | 412 ++++++++++++++++++
 proxmox-http/tests/openssl_verify_cb_new.rs   |  57 +++
 proxmox-http/tests/openssl_verify_cb_old.rs   |  49 +++
 16 files changed, 806 insertions(+)
 create mode 100644 proxmox-http/tests/certs/cert-chain.pem
 create mode 100644 proxmox-http/tests/certs/intermediate-cert.pem
 create mode 100644 proxmox-http/tests/certs/intermediate-csr.pem
 create mode 100644 proxmox-http/tests/certs/intermediate-key.pem
 create mode 100644 proxmox-http/tests/certs/leaf-cert.pem
 create mode 100644 proxmox-http/tests/certs/leaf-csr.pem
 create mode 100644 proxmox-http/tests/certs/leaf-key.pem
 create mode 100644 proxmox-http/tests/certs/root-cert.pem
 create mode 100644 proxmox-http/tests/certs/root-key.pem
 create mode 100644 proxmox-http/tests/certs/self-signed-cert.pem
 create mode 100644 proxmox-http/tests/certs/self-signed-key.pem
 create mode 100644 proxmox-http/tests/common/mod.rs
 create mode 100644 proxmox-http/tests/openssl_verify_cb_new.rs
 create mode 100644 proxmox-http/tests/openssl_verify_cb_old.rs

diff --git a/Cargo.toml b/Cargo.toml
index bef718ec..a47baa0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -137,6 +137,7 @@ serde_plain = "1.0"
 syn = { version = "2", features = [ "full", "visit-mut" ] }
 sync_wrapper = "1"
 tar = "0.4"
+tempfile = "3.15"
 termcolor = "1.1.2"
 thiserror = "2"
 tokio = "1.6"
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index aadb6a42..bff65e9c 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -44,6 +44,8 @@ proxmox-compression = { workspace = true, optional = true }
 [dev-dependencies]
 tokio = { workspace = true, features = [ "macros" ] }
 flate2 = { workspace = true }
+proxmox-sys = { workspace = true }
+tempfile = { workspace = true }
 
 [features]
 default = []
diff --git a/proxmox-http/tests/certs/cert-chain.pem b/proxmox-http/tests/certs/cert-chain.pem
new file mode 100644
index 00000000..a88ae376
--- /dev/null
+++ b/proxmox-http/tests/certs/cert-chain.pem
@@ -0,0 +1,46 @@
+-----BEGIN CERTIFICATE-----
+MIID0zCCArugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCQVQx
+DzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmllbm5hMRAwDgYDVQQKDAdQcm94
+bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQDDB50bHMtY2VydC12YWxpZGF0
+aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTM0MjAxWhgPMzAyNTEwMjUxMzQyMDFa
+MHMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExEDAOBgNVBAoMB1Byb3ht
+b3gxEDAOBgNVBAsMB1Byb3htb3gxLzAtBgNVBAMMJnRscy1jZXJ0LXZhbGlkYXRp
+b24taW50ZXJtZWRpYXRlLnRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2/UMNpaQ7WsTmUFvXO/FyGcKSaAjcLvfj15ezjcJxlDr1S580Z3T7kph
+boR5K8iH8nJa0Ujv7ty2c0HgpKZIduYzZ5+hv1K7czbpQRqoY5956O06VCUuIYC5
+Oa2byEV10PBojckd/opFW0rhCR1+VTtRLXinQ64RVdcfnHVbWpV8QBks96ZeNHAX
+mv5FlYICHfgCQm8FAljN1X3dQXkroHErmXDw6BXGrfYBnXlI4p+e5cj9pBJHqRJ0
+XGsnoAv1xmEmgsSm6S+wRZ/+dtY6rs3N//qz6J9ZuyAccjKpY5yS1Xa6/f+7IJFd
+W7MGh+DZ6kWJ1MxAbTKcIUo4P4GOUQIDAQABo2YwZDAdBgNVHQ4EFgQUa+cbP/Az
+gyufv3AtFJdQ7tY6iGswHwYDVR0jBBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCsw
+EgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL
+BQADggEBAFq9zeICWKkM7P5D2e0f1tUt4krV45D7avZqkNe7tlglm9g66UlDHXH9
+bvVPYkPG3mHSn0QtEykTgzCB5l1EgL3+OL32SoOBOQtrS0ePlmVhe05ydgqf7oFf
+EECvghk+Z96cSRtgPwrWp8U93C3Zggaf56QI7KqZg9QpYCjyAG+pGrhoPTmyMv8H
+PLxgGw7fJyviwHLrHCUxZridQ6w9fOT02aqsIoock9df9YBJuDKJILDI3oXaeKds
+y09qJ/2kwJPlHrtyvqOdWrrJz/x3trpOO1cpH96VttYW0usHOsWdgqvc9tR7Axwf
+TYthAFu4PV97914MfSvvPPEyP+C6NIA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID6zCCAtOgAwIBAgIUcHTmdgi8DylmBhrTr4NBKpnLBk4wDQYJKoZIhvcNAQEL
+BQAwfDELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmll
+bm5hMRAwDgYDVQQKDAdQcm94bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQD
+DB50bHMtY2VydC12YWxpZGF0aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTMzODIw
+WhgPMzAyNTEwMjUxMzM4MjBaMHwxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVu
+bmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwH
+UHJveG1veDEnMCUGA1UEAwwedGxzLWNlcnQtdmFsaWRhdGlvbi1yb290LnRlc3Rz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoIyemjaxlCB1jDMgBooZ
+/Bhhprhbz6OxoAGFpI8T7ppzZFKoFOWIHEJcYrvqAQn0RqZe8EIUXbquIYjBZPnA
+bwxrZHMgcC5J/f0pRD+N68uCUU2EPaoHqk+v/YMM+zTViftWIFpU17HfSmEkPR4l
+9OljcYendp1nMRsRB3dxTPmoK/D1E87LVrz7GlsuoFDJE3CfBQ6eNUMSdNbicDF0
+xwzyzQsCGuCboHa1Q+fFU1Se3Lts1S+X7TdX2XySvFziq3wkAKaMZ46C4wM25+Z7
+Is+kBDxIxaPq13XdpneG5Iis2hT2ruWSbopuD57Zuh83/HQSSNlaaPFivib2O9kA
+1QIDAQABo2MwYTAdBgNVHQ4EFgQUsI4iO4yr1STBmK9TWO0+bgf9fCswHwYDVR0j
+BBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCswDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAEM83xjtDyctuwYTCuwM7KRt
+StuAmtnw/snfBJZgCKNPi0fOM1rzwM1g/h4LrH7go9VpxQ2VtT+9/20MBhlqWdAN
+sI1IpUMArsmzlKaBZUZDrS3An9iRztsmnftLfkXyku6nUcb8TPDmE5r1arnDngsX
++EcINC1DgOTy4Sv9vWv6apJQtNg+/Xqs+Ax+4iIXDJde28SX7p8vTdkBKLhHnGLJ
+zrEI2DzGqy8+sPTKSYGw3oNH3QUwf3FJnZKJGifmiehzdHkVKF3XesBddQjWOM/y
+E4yYJqlwpDhykDEz5d6sD6F/5mw3LOqk2J2jfDbPN5IEagYEDMzAqx10Wi7U+94=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/intermediate-cert.pem b/proxmox-http/tests/certs/intermediate-cert.pem
new file mode 100644
index 00000000..4a56c76c
--- /dev/null
+++ b/proxmox-http/tests/certs/intermediate-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID0zCCArugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCQVQx
+DzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmllbm5hMRAwDgYDVQQKDAdQcm94
+bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQDDB50bHMtY2VydC12YWxpZGF0
+aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTM0MjAxWhgPMzAyNTEwMjUxMzQyMDFa
+MHMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExEDAOBgNVBAoMB1Byb3ht
+b3gxEDAOBgNVBAsMB1Byb3htb3gxLzAtBgNVBAMMJnRscy1jZXJ0LXZhbGlkYXRp
+b24taW50ZXJtZWRpYXRlLnRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2/UMNpaQ7WsTmUFvXO/FyGcKSaAjcLvfj15ezjcJxlDr1S580Z3T7kph
+boR5K8iH8nJa0Ujv7ty2c0HgpKZIduYzZ5+hv1K7czbpQRqoY5956O06VCUuIYC5
+Oa2byEV10PBojckd/opFW0rhCR1+VTtRLXinQ64RVdcfnHVbWpV8QBks96ZeNHAX
+mv5FlYICHfgCQm8FAljN1X3dQXkroHErmXDw6BXGrfYBnXlI4p+e5cj9pBJHqRJ0
+XGsnoAv1xmEmgsSm6S+wRZ/+dtY6rs3N//qz6J9ZuyAccjKpY5yS1Xa6/f+7IJFd
+W7MGh+DZ6kWJ1MxAbTKcIUo4P4GOUQIDAQABo2YwZDAdBgNVHQ4EFgQUa+cbP/Az
+gyufv3AtFJdQ7tY6iGswHwYDVR0jBBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCsw
+EgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL
+BQADggEBAFq9zeICWKkM7P5D2e0f1tUt4krV45D7avZqkNe7tlglm9g66UlDHXH9
+bvVPYkPG3mHSn0QtEykTgzCB5l1EgL3+OL32SoOBOQtrS0ePlmVhe05ydgqf7oFf
+EECvghk+Z96cSRtgPwrWp8U93C3Zggaf56QI7KqZg9QpYCjyAG+pGrhoPTmyMv8H
+PLxgGw7fJyviwHLrHCUxZridQ6w9fOT02aqsIoock9df9YBJuDKJILDI3oXaeKds
+y09qJ/2kwJPlHrtyvqOdWrrJz/x3trpOO1cpH96VttYW0usHOsWdgqvc9tR7Axwf
+TYthAFu4PV97914MfSvvPPEyP+C6NIA=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/intermediate-csr.pem b/proxmox-http/tests/certs/intermediate-csr.pem
new file mode 100644
index 00000000..824248d8
--- /dev/null
+++ b/proxmox-http/tests/certs/intermediate-csr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICyjCCAbICAQAwgYQxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExDzAN
+BgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwHUHJveG1v
+eDEvMC0GA1UEAwwmdGxzLWNlcnQtdmFsaWRhdGlvbi1pbnRlcm1lZGlhdGUudGVz
+dHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb9Qw2lpDtaxOZQW9c
+78XIZwpJoCNwu9+PXl7ONwnGUOvVLnzRndPuSmFuhHkryIfyclrRSO/u3LZzQeCk
+pkh25jNnn6G/UrtzNulBGqhjn3no7TpUJS4hgLk5rZvIRXXQ8GiNyR3+ikVbSuEJ
+HX5VO1EteKdDrhFV1x+cdVtalXxAGSz3pl40cBea/kWVggId+AJCbwUCWM3Vfd1B
+eSugcSuZcPDoFcat9gGdeUjin57lyP2kEkepEnRcayegC/XGYSaCxKbpL7BFn/52
+1jquzc3/+rPon1m7IBxyMqljnJLVdrr9/7sgkV1bswaH4NnqRYnUzEBtMpwhSjg/
+gY5RAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAsI2k85A0oMQRAqeZEG7mV48y
+65FQ/WzdnIe1/SsmGYnrJ4ZWZF69+hRCTaVrEJEIlbD1lT+teKYlE1x4aMR8zaWo
+CymBFfDAjRUyR4s38BliXslekivC7o8IUcyi7prjOvMHtK3p+1f+wPCyz6jDSD9V
+9LuHi7kdZBfCUIxBtPtGrGdouf+s6LkTv64DGyldsturDl3CnRvUaRDt95qc6gUW
+YhMLv/bzxet75htvk4H2VhpZn/ZKhJRNebetDFWH0LWB5IouLOBdDsDkWRMaV0x9
+HYbG3jZ8NgSHmqAxqxSq/dt6XSJ9mpVl9vTaAPxq05v337j54FC34oz0q0pK9Q==
+-----END CERTIFICATE REQUEST-----
diff --git a/proxmox-http/tests/certs/intermediate-key.pem b/proxmox-http/tests/certs/intermediate-key.pem
new file mode 100644
index 00000000..beb4bad6
--- /dev/null
+++ b/proxmox-http/tests/certs/intermediate-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDb9Qw2lpDtaxOZ
+QW9c78XIZwpJoCNwu9+PXl7ONwnGUOvVLnzRndPuSmFuhHkryIfyclrRSO/u3LZz
+QeCkpkh25jNnn6G/UrtzNulBGqhjn3no7TpUJS4hgLk5rZvIRXXQ8GiNyR3+ikVb
+SuEJHX5VO1EteKdDrhFV1x+cdVtalXxAGSz3pl40cBea/kWVggId+AJCbwUCWM3V
+fd1BeSugcSuZcPDoFcat9gGdeUjin57lyP2kEkepEnRcayegC/XGYSaCxKbpL7BF
+n/521jquzc3/+rPon1m7IBxyMqljnJLVdrr9/7sgkV1bswaH4NnqRYnUzEBtMpwh
+Sjg/gY5RAgMBAAECggEABQOjEaOBDkiAm9/IACBfK+Bddaw24p0FzajjFGRgzqqN
+lcCHi+fDKw17Bx/x+zOJFdfRhi/ZeGKDrkD0NAyuXjeFOHmFIG1sZIX970QCTrMV
+/l9aEwz97jmW/1+by4b51peEaqcJDgJs7lXYp3KKrLq7cQPtHDfdoU1UJSbvvDLd
+Xl7gZ4WYla4EAncFsA1K5jBuANG66mO8Gmumz+eLmslUB2RVLxPrVx0PybSXxQII
+rnzd6V24DV6+VkN0lBVm1k98Is4hbCEmMyZGweWX9wjdCzMPgqqPHbto7gY8UkPM
+Iz+BwbTI59pM9/m0BLtPxUd7Sccy076L0PtC3XYJeQKBgQDwLISbPRjJaGnJMpU9
+8M5+6n0D8E+BlRCS6Qkav6X/gy6rdNAl5hQjDaSZk72D7a+DGJUBsa9F9GgIw8pW
+Btkd+4/ukqLP3aoSKTKTRtt7tFZMPi+5CJJSGUDIDbmS0XrxxNMudu7kO8ODmPE3
+w5uHSYztw+I6yGMNWY5GmCdjOwKBgQDqc37YmadECvq1IwHZiXWprDowe/qL+VYy
+VFZj/zGABRxSGOoIc/AW8MjbDh+WEyErIkVkxj3AZ8N85sziIJH0+K823DWtiILR
+zuUJ1GrVfTzkGjFTrrCJd6Ev0fBKO7/jtpGOYD9XpCtEoxz30RDNjq+aIv57B53w
+3+Gq7tKj4wKBgQCjKtt8S+nHC3SzB/Z0emEPwGbmgiDBvG/iHwfccE9qY8kVGus+
+lC0iE2a8H68lLhmLSuwQlpKpR/5V1g5km4pt4DZMsrqB1epxJCQEAqOiS0ZFzgnF
+/5jIxfdI8moc4MxR7JI8gviRfji58vIOHIpRQxrHfcj4fqMssqcCNuSreQKBgQCt
+EC5tQxcGkjg4p6vA4cg6REj76ziqRJaNNlZDIGhwwNUEASIYtURgGsOZd9Z3GI3e
+YkDpP7Drq2zRcSmCLlqvgzcLfwgcne07ZMcLN4LZLsZY9sC8rfHgt68DNqxyj6J5
+PBY8C+4WCrhpxSIoCGqn4hDb7cL+HERJP2o8nGhe0wKBgDddzovqa7xEJAJAKFU9
+YiZGhSq7sB/1MXEAzmbQLFUmuMpQFEVv4hjXry1d3n51Y2M8AbOLRttZD5myN/QT
+BFoKmfQzOY5IydfLy4XGmsB/dVovd2Sq3JUuqeA9rtPvkGZcLppiipGc8mkSxpI2
+RGcpj1GoYLQKcFQraBdFWxhG
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/certs/leaf-cert.pem b/proxmox-http/tests/certs/leaf-cert.pem
new file mode 100644
index 00000000..a6260999
--- /dev/null
+++ b/proxmox-http/tests/certs/leaf-cert.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID9DCCAtygAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwczELMAkGA1UEBhMCQVQx
+DzANBgNVBAgMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwHUHJv
+eG1veDEvMC0GA1UEAwwmdGxzLWNlcnQtdmFsaWRhdGlvbi1pbnRlcm1lZGlhdGUu
+dGVzdHMwIBcNMjYwNjI0MTM1MTA1WhgPMzAyNTEwMjUxMzUxMDVaMHwxCzAJBgNV
+BAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UE
+CgwHUHJveG1veDEQMA4GA1UECwwHUHJveG1veDEnMCUGA1UEAwwedGxzLWNlcnQt
+dmFsaWRhdGlvbi1sZWFmLnRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAkHL4lBzjkvGm0iNRwLijTKqLIYL5kjr2rfepjtFV0f/s+WDjSUcbgryv
+VrjoAazt4G4huxFsWA1Yc/wq90ut5FQCn3zID/XUTjloTOE7S7jyc+a+agbZNMEj
+R2udNqNPIMAUDnCss5PDq/S+15lnPdkK8YWk4vl2hT4GiU3P8xv6WZQPWlJA0gRe
+mA2/sCdzxux0YP5yOUPJ9ZnYTgJyXdZ1WigsjEwzKgjWBzmRMDp81T02Pyan8oUq
+frCzC/jIoeyC7JemZCzVaEhkbRRmh9eS3O9trIjjeXGVdi04JUDdeecFWXVRO9G2
+xRc71L4OxHuHUoJNyYbIkn6j5IiaewIDAQABo4GGMIGDMAkGA1UdEwQCMAAwEQYJ
+YIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEF
+BQcDATAfBgNVHSMEGDAWgBRr5xs/8DODK5+/cC0Ul1Du1jqIazAdBgNVHQ4EFgQU
+xakweDrqyFkO8jE7UamgXUN/fOcwDQYJKoZIhvcNAQELBQADggEBALZaE4lP+Krm
+gEnFfG+CftOzC5zmjAI56aZJ661n87Lh/ECQdgRbvvtAqFGdmJi5GFRkesJSyfIx
+YGFj1mc2TC1BCv7bKJnoM2YFebXHiTJ+/ODuboiGEdbm5xWaowjgWutaXHr7O/aa
+KV9mzFasJegO3i4rCVFfnNVDW+aq23OTYkgNiJjp0H2sSckzsyQFmYLqMPzGXk1F
+ed2YpoNv4GT+HOfdP6ExWH1cL6AZI+jJ7/fEbBiEFyAsfaW7c8knphe/MGztWgCp
+ayCoe48IWwGS2nzdUvjw0w+QF6PpLo6CiFap8qiALlvKK6hijsrKFpVvD1PxggRA
+3mYsWKaRuHo=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/leaf-csr.pem b/proxmox-http/tests/certs/leaf-csr.pem
new file mode 100644
index 00000000..318d2e37
--- /dev/null
+++ b/proxmox-http/tests/certs/leaf-csr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICwTCCAakCAQAwfDELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0G
+A1UEBwwGVmllbm5hMRAwDgYDVQQKDAdQcm94bW94MRAwDgYDVQQLDAdQcm94bW94
+MScwJQYDVQQDDB50bHMtY2VydC12YWxpZGF0aW9uLWxlYWYudGVzdHMwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQcviUHOOS8abSI1HAuKNMqoshgvmS
+Ovat96mO0VXR/+z5YONJRxuCvK9WuOgBrO3gbiG7EWxYDVhz/Cr3S63kVAKffMgP
+9dROOWhM4TtLuPJz5r5qBtk0wSNHa502o08gwBQOcKyzk8Or9L7XmWc92QrxhaTi
++XaFPgaJTc/zG/pZlA9aUkDSBF6YDb+wJ3PG7HRg/nI5Q8n1mdhOAnJd1nVaKCyM
+TDMqCNYHOZEwOnzVPTY/JqfyhSp+sLML+Mih7ILsl6ZkLNVoSGRtFGaH15Lc722s
+iON5cZV2LTglQN155wVZdVE70bbFFzvUvg7Ee4dSgk3JhsiSfqPkiJp7AgMBAAGg
+ADANBgkqhkiG9w0BAQsFAAOCAQEAGtog/UF1Kfs4YP+T0SBoyvKKhPiKQo9muW1b
+YvdcBbWdRSa9YGNVXRCmvDDLzfzH0DoDvQzRcf8OC2wtytB5s0NF5SA+BdRQFcoi
+RIUPx1AmJ6fVXpE0lLB54hjdw+ngq2WBMJN/cWzC0lb+6vckTTH7g9LKp2yn0pUh
+Ycf0O8sD4LCQNJn7yN3TwmpiALStW04yg6CY3KKn2ZeoHbDxfs3Tyyf4mVX2SppX
+50Fnj16Ykypd5iQSGWETZc7/cYdO2d+BX/6G7x91HuQRLV/1/jYVd00G3horuaiw
+P+V84LrJh6/ZOJc660IK+CEn9vjVQ5y/zrSQMfe4cFStc79dFA==
+-----END CERTIFICATE REQUEST-----
diff --git a/proxmox-http/tests/certs/leaf-key.pem b/proxmox-http/tests/certs/leaf-key.pem
new file mode 100644
index 00000000..f64392e2
--- /dev/null
+++ b/proxmox-http/tests/certs/leaf-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCQcviUHOOS8abS
+I1HAuKNMqoshgvmSOvat96mO0VXR/+z5YONJRxuCvK9WuOgBrO3gbiG7EWxYDVhz
+/Cr3S63kVAKffMgP9dROOWhM4TtLuPJz5r5qBtk0wSNHa502o08gwBQOcKyzk8Or
+9L7XmWc92QrxhaTi+XaFPgaJTc/zG/pZlA9aUkDSBF6YDb+wJ3PG7HRg/nI5Q8n1
+mdhOAnJd1nVaKCyMTDMqCNYHOZEwOnzVPTY/JqfyhSp+sLML+Mih7ILsl6ZkLNVo
+SGRtFGaH15Lc722siON5cZV2LTglQN155wVZdVE70bbFFzvUvg7Ee4dSgk3JhsiS
+fqPkiJp7AgMBAAECggEAQ9brLcx/iNSbD1ftHkDY2LnDzAJSKb4teji1VlC0KIM0
+jU5WkGSn4/evtV/z/k10DpJKnyuooZXq89X7a9cMHQ7jiHm3D9/ZTL+jX2/sRDzh
+CVPWG7+JpUALzJAa7r01/WCYSsvaICCGpiy0sFboaOCVRicI8FxOsHcX5MY5oqfN
+M15GIA+jCwfiLQI2JuBE1Q39ieMpRsA0zG+itSm+EOYacHDduUjtLEgSh0fXhSrF
+q1rwK7kdNMGZNg9FdDtROX4FCrQvEjWOj1hl2Z/GzOMkpJu4SCrDIzXPppfkjGbF
+KUBcZ67EEChd8EjJ8LYOuKJf8XrIr4HJRhsJP51syQKBgQDMB27OrWeZHnCisXtr
+Ve3CSCp/BaZ/6fgPe5fufqcymIDb61bgzk1MqwT3pfP8/YiRZfokZT87ehox3gML
+HfAq8STWb4mVp7s/YuAFwlXFhTlBB4Fwge7wy7CjmCKgtknxMiiO8voCCsN0GQLw
+MwGjqTHF4q98iGTNn0XoJWE49QKBgQC1PmEokad/9sHLNr1Jjke8R7K9tNUfFTQP
+m5S/LUfXwcXsJx6QEWnVNgDXvWIq1WnTfwbSnfu+XTrjiaTAi4ZWXusicpTd30em
+3VXDIdXwYJSCU3OFWPQhbCnWDx4aVoJEb44KrGvCC/LJkweXzEminoTDMWdsGscI
+Ik25NFkfrwKBgQCJRqsAdl3RAVEpth7jXkKFyMaHBoc7Y3HbAP59okve2AtDbPnc
+chJCdoL2GXurie6cXa/LUzATVZlQWh9UGIWibvOpMAyzW9K52E4AsfvB1Vxra6Bk
+0Zex/mrP96m81kmz9lqhq8wZGaLed4GpmbgNpOZvTZFjSeYBD5wakSP0DQKBgEC+
+VtC6Lz6L9DBWjomfFMsSRax004j19xH4PsuILljJdJ1mYAmQ3uB2GRj4IwAwGkyd
+3N8R5mLbRPURL1REwylJYO9+ROV5JExcVo2NIbJrncFsdCDXZOYnkE5SOiuoaYJu
+4yu26gt4XzNYnWbBaDB6NezQUiSQ8DZcoq0dIRUrAoGBAMoYm7nV/q11i+08I9J1
+vbCoatf9BVp4kDlbAE3YxQw/QNxDkUfh9VDLIDrxYxDgrK8YS0tZJgHGEmWLzpo9
+8/L5NoD1ayaRHboSBuoVsYCy/DI98cuTPWdCDa29jKXToLfgJCdc9lvk7u6vr2zX
+zAt212NdiNc+OWkwy3m7EYiL
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/certs/root-cert.pem b/proxmox-http/tests/certs/root-cert.pem
new file mode 100644
index 00000000..d6231c81
--- /dev/null
+++ b/proxmox-http/tests/certs/root-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID6zCCAtOgAwIBAgIUcHTmdgi8DylmBhrTr4NBKpnLBk4wDQYJKoZIhvcNAQEL
+BQAwfDELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmll
+bm5hMRAwDgYDVQQKDAdQcm94bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQD
+DB50bHMtY2VydC12YWxpZGF0aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTMzODIw
+WhgPMzAyNTEwMjUxMzM4MjBaMHwxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVu
+bmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwH
+UHJveG1veDEnMCUGA1UEAwwedGxzLWNlcnQtdmFsaWRhdGlvbi1yb290LnRlc3Rz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoIyemjaxlCB1jDMgBooZ
+/Bhhprhbz6OxoAGFpI8T7ppzZFKoFOWIHEJcYrvqAQn0RqZe8EIUXbquIYjBZPnA
+bwxrZHMgcC5J/f0pRD+N68uCUU2EPaoHqk+v/YMM+zTViftWIFpU17HfSmEkPR4l
+9OljcYendp1nMRsRB3dxTPmoK/D1E87LVrz7GlsuoFDJE3CfBQ6eNUMSdNbicDF0
+xwzyzQsCGuCboHa1Q+fFU1Se3Lts1S+X7TdX2XySvFziq3wkAKaMZ46C4wM25+Z7
+Is+kBDxIxaPq13XdpneG5Iis2hT2ruWSbopuD57Zuh83/HQSSNlaaPFivib2O9kA
+1QIDAQABo2MwYTAdBgNVHQ4EFgQUsI4iO4yr1STBmK9TWO0+bgf9fCswHwYDVR0j
+BBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCswDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAEM83xjtDyctuwYTCuwM7KRt
+StuAmtnw/snfBJZgCKNPi0fOM1rzwM1g/h4LrH7go9VpxQ2VtT+9/20MBhlqWdAN
+sI1IpUMArsmzlKaBZUZDrS3An9iRztsmnftLfkXyku6nUcb8TPDmE5r1arnDngsX
++EcINC1DgOTy4Sv9vWv6apJQtNg+/Xqs+Ax+4iIXDJde28SX7p8vTdkBKLhHnGLJ
+zrEI2DzGqy8+sPTKSYGw3oNH3QUwf3FJnZKJGifmiehzdHkVKF3XesBddQjWOM/y
+E4yYJqlwpDhykDEz5d6sD6F/5mw3LOqk2J2jfDbPN5IEagYEDMzAqx10Wi7U+94=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/root-key.pem b/proxmox-http/tests/certs/root-key.pem
new file mode 100644
index 00000000..e123e674
--- /dev/null
+++ b/proxmox-http/tests/certs/root-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCgjJ6aNrGUIHWM
+MyAGihn8GGGmuFvPo7GgAYWkjxPumnNkUqgU5YgcQlxiu+oBCfRGpl7wQhRduq4h
+iMFk+cBvDGtkcyBwLkn9/SlEP43ry4JRTYQ9qgeqT6/9gwz7NNWJ+1YgWlTXsd9K
+YSQ9HiX06WNxh6d2nWcxGxEHd3FM+agr8PUTzstWvPsaWy6gUMkTcJ8FDp41QxJ0
+1uJwMXTHDPLNCwIa4JugdrVD58VTVJ7cu2zVL5ftN1fZfJK8XOKrfCQApoxnjoLj
+Azbn5nsiz6QEPEjFo+rXdd2md4bkiKzaFPau5ZJuim4Pntm6Hzf8dBJI2Vpo8WK+
+JvY72QDVAgMBAAECggEAKhhkt3uSuQt/tqhrA7vvDz3XUM7y57D8cD8l6t1G9R9T
+FSFlB8GdHAe8UHkD5IzXGzUhHG6/B0pcwNcqGg8wCQ3hFJ/pB/DjHrDjwozFaedc
+vnOMMlzkEKA/PUHAxBb4zGp1jRsSNtHhAZAR3+KJQjt1gv12B7BCr8nwf5wuPWen
+7fswPrd0JJO1p1RjQyWeiM91LfJYOuo7rt6Froi1KVSVNUt3H9U664nZTbZM36q5
++e9d940yijX0ckggV35zwSOMyhhK3sISEhsaPulsPvp0nlGtDzhhZjDX6ocklbi/
+hE4fytuxZsEHjrJGA7XMsQ7GpJgxC96DB1Eczf3zOwKBgQDTCsLMlBHEhr+mj7GJ
+6pf03udCzIovC/0QaUVHkItFhxVnrHBhn9VzbqGITYQu6MF8kXsC7rIyt7Ad59DM
+n6L5r6imKt2R+UaaezkCC1c0MWBQQ9rjfO8Yw9f5wgw4nvKE5xEf1sq/374xJKfz
+6rqexTsHWBluYK6BnhJS8oqJRwKBgQDCwDjnW9xrN5exS8zFg8c101O/+MpsJeCU
+sx+oIcxIh/7Gn9ho/vmgL8uNyoro2t31BDhgrgBmUBs6cnDdg+P8X0IlySKsGKXj
+FY7F6IzRDJVgD4Opsn2VnpMJEJGMGcBrDJPwVb1sSKL7ELQhDWTQ5EdTsmGyijkj
+OvgsYj3zAwKBgQC1LLnK8xrFwoBpN1bM9Y56c5nJaNsARKR+IEGPjHFjwPIJTKo1
+xQdzz3fxEcr2km74x9P40n48uCEDq20/HZTGEZ0Q+h+5H20TVdG9BYtZjUIH5hjV
+zv1cH1UcXxAq05mTquKymKz6R9R5T+S3q72Ga/+e8Gz0qx9kuxU0DHAOJQKBgQDC
+U2/0W5MbYQN6I/qV86IpsU7WNXg2Za0sc3fZGrBuh1TP+NvGGPYYwthICZyGMS5c
+t/NRdQ5tCO3CakL4pgwt3RdyALsaIhYU+4PVMvCgAABlM9Xa1IG/c9Wfq+qvc1qu
+9oP/wm4ayHfoMYirmmPIlKAfgdU+g/Hzl3nfP8A05QKBgFHUfa79T7h7nK/YZqgf
+/gVrJRv4vQn80fFQtcebmKmiRcjg9lziOjc1Dozv23WNQZ2y5Cw3u1K8Se86HTmk
+sEK9JDc6q0icWA3bvVG8PGtdrv43CR1Gsg786T5/JcHyupWeqgLHUhvxDXKUU0Bf
+HCdFi6WsyhVZ6k2KOP13k+QY
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/certs/self-signed-cert.pem b/proxmox-http/tests/certs/self-signed-cert.pem
new file mode 100644
index 00000000..14e76081
--- /dev/null
+++ b/proxmox-http/tests/certs/self-signed-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID6zCCAtOgAwIBAgIUSkItlHQ0qbnha5abhSSWT5BGJbQwDQYJKoZIhvcNAQEL
+BQAwgYMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExDzANBgNVBAcMBlZp
+ZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwHUHJveG1veDEuMCwGA1UE
+AwwldGxzLWNlcnQtdmFsaWRhdGlvbi1zZWxmLXNpZ25lZC50ZXN0czAgFw0yNjA2
+MjQxNDI0MzZaGA8zMDI1MTAyNTE0MjQzNlowgYMxCzAJBgNVBAYTAkFUMQ8wDQYD
+VQQIDAZWaWVubmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQ
+MA4GA1UECwwHUHJveG1veDEuMCwGA1UEAwwldGxzLWNlcnQtdmFsaWRhdGlvbi1z
+ZWxmLXNpZ25lZC50ZXN0czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+APlUtV9F35NimI62w+nZVU/eUZPF2YD8j4sjLwle+SmYCXC9VHlYme9OthrQSYAS
+p/ioNyjk8DVGhA6FqLpnKW7iCBRU5mai17uzULMkVcX2lcdHkjxOyNSiEwS8mb7s
+pbiSXUqmuFsWzfNuGR4LUtW4bgqljUkhvEOD2DSI9kshJAOp1VvepvtXTVcLwXJf
+sVyF9lWh5UBjRKhyAWNZQxxdPu3FadFwAcDedbPARVPb9jO/5CJWTG3GRC/4BMiM
+d8HE1uVvW/O++xu26dzY1Vj3brwJUJIZJtVcMOdC2YkOGbe3+5B1tSGqoTuJlRjH
+Js5rNgasviH0Y4PXzag+6z0CAwEAAaNTMFEwHQYDVR0OBBYEFDKcDkUpWP/PKzzT
+PXsLpS2krdBFMB8GA1UdIwQYMBaAFDKcDkUpWP/PKzzTPXsLpS2krdBFMA8GA1Ud
+EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB0uNKO+MA7MJ51CO282DULb
+jH9r2srM4XRoUWwFu4bYgduUa50lOMv6FMIRq1S023HKXcBxSAoCLexOSXdvb5x4
+yEbyqYrzvc5RPZvI1sDqkBbvZ1ZB3SbboHSKzOV9ddYxp4XXA60fL17syPkIGpqG
+UNeX0I/vZfKgS/EPvYi65WO/bVpnEEAIz4hUs5IJk5o74s7Mz7wz5C4Dv08kJKjH
+E4yv5yDjIlID6ksorOTB5WFdEeTibB+mTSbBXZp6c6KUZnNKvVDAHRRCbsp/eNtr
+Nr06RcBqzgwjXhJGD0JNJfe8r/loRlhDCpvXZlOV7dYKDEOODCeeKaFCQwQXYPo=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/self-signed-key.pem b/proxmox-http/tests/certs/self-signed-key.pem
new file mode 100644
index 00000000..97c8060a
--- /dev/null
+++ b/proxmox-http/tests/certs/self-signed-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD5VLVfRd+TYpiO
+tsPp2VVP3lGTxdmA/I+LIy8JXvkpmAlwvVR5WJnvTrYa0EmAEqf4qDco5PA1RoQO
+hai6Zylu4ggUVOZmote7s1CzJFXF9pXHR5I8TsjUohMEvJm+7KW4kl1KprhbFs3z
+bhkeC1LVuG4KpY1JIbxDg9g0iPZLISQDqdVb3qb7V01XC8FyX7FchfZVoeVAY0So
+cgFjWUMcXT7txWnRcAHA3nWzwEVT2/Yzv+QiVkxtxkQv+ATIjHfBxNblb1vzvvsb
+tunc2NVY9268CVCSGSbVXDDnQtmJDhm3t/uQdbUhqqE7iZUYxybOazYGrL4h9GOD
+182oPus9AgMBAAECggEAVhS3W9HRa16ughM6l4mX6S+95XF48w3/dw+qJSebiY86
+ryhGunBrERKRT7easLOAN5rIFH/aKOKUJDlkNBr61JQIKxDWzRequNyjX34LeQH1
+2yvsIpMmxjbArzF4OVEVtCAgQm5GFvjMGR/pXxSUwEHhCB75JQcXKw4hfp3Mvsnf
+YxBPC6EV69B3mDwmJjcP6aAb0t4is0BFrkeg8os1DKKQPpc+NJaWZ8imhUmudEi2
+ZUwVQvJFqXbhFmWLw4nXK93/192uWNw7lxodPpJTJrfZU5uEWLV/1FjKysYWgtTd
+Z3Z73eiq6RdWdPxXnx9g8k/hVycH66XGGNw/VPykpwKBgQD+WsCKk0GVw3J3QU7Y
+VRMG3idkIV2VcM2SIalFG0t9sb7XRN354P0J2A3EBMuqWNw7SKo3GIsFscaIdh1t
+m3fLJgmccZJaWAeQsSt7tU291p6AkeNu0UQw8tK/v+GbljlUxumBZZ009BbRCv0Y
+qb05am2JWvBtmyfkntorg34bNwKBgQD68aL01KOW3MbJcGVFumTwaEuUsv9OTrA8
+SvlTxrVfXU5uncJ+P3g2a/IeS2IELKBsWNIuPAGFVrECPQjS9Y4oYgnaj5ahN30w
+GWQ3lYnzxdvohPUOW1cCwDg6Mz5+Mmx+ygL0Sb0YG36assElipVHB9PiRukuGHSH
+6oCkwpjvKwKBgAZXeOl7lm0HfHkgtbiLFnhbXZwPgOfS8i0sja3dalpt7hYr72Tl
+iSmPq3gxrmpG4ObRfvz0rbKspgiM+VrcP3ZfMmomIsIB495lrHHfKVsMWNNXz9XZ
+fdvCkiKZxCQ+8Jr+gp/pSqwhUdhQb9MHmGIwFx8Pl2MENVBr7YCcPK6tAoGAZzol
+LY+XJ8Tz5QNeNXvCb/6HMMkdKspFxteUjrjL/Um1rN0ql6JmQgTPmVSrIkp1R3yW
+ITy/52jM8b3HtnganVQO96BfdzwLPFEFn7PdBrFaj+C5qck7Fr+ZoZ9Y0rLNXK6e
+3nzC03rj7qEfwOCsHYcDyy4eV77pmMuHVb9TB/cCgYBgh9o+io9RP5N6J16Tx7bt
+6wQeBkiuixcKrUKGJKkgbacSRgiNNNFn/wbaOUhcVu7aktTI/yLDg8TFBBS2cbqn
+9i9iMoxx14+jCXYRnOFRcHafBTi+1S2uqxn97CxNM8RzE2wh4Q44ZeKahiEDAtZP
+fPO+8PfqWIBYr3NKod7Yeg==
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/common/mod.rs b/proxmox-http/tests/common/mod.rs
new file mode 100644
index 00000000..3a787f06
--- /dev/null
+++ b/proxmox-http/tests/common/mod.rs
@@ -0,0 +1,412 @@
+use std::os::unix::net::UnixStream;
+use std::path::PathBuf;
+use std::process::{Child, Command, Stdio};
+use std::sync::atomic::AtomicU32;
+use std::sync::{Arc, Mutex};
+
+use openssl::ssl::{Ssl, SslContextBuilder, SslMethod, SslVerifyMode};
+use openssl::x509::X509;
+use openssl::x509::store::X509StoreBuilder;
+use tempfile::TempPath;
+
+use proxmox_http::{
+    PROXMOX_NEW_TLS_CHECK_VAR, SslVerifyError, get_fingerprint_from_u8, openssl_verify_callback,
+};
+
+/// Helper type to easily get proper certificates for a given test and their fingerprints.
+#[derive(Clone, Copy)]
+pub enum CertificateType {
+    SelfSigned,
+    CertificateChain,
+}
+
+impl CertificateType {
+    pub fn certificate_path(&self) -> &'static str {
+        match self {
+            Self::SelfSigned => "tests/certs/self-signed-cert.pem",
+            Self::CertificateChain => "tests/certs/leaf-cert.pem",
+        }
+    }
+
+    pub fn key_path(&self) -> &'static str {
+        match self {
+            Self::SelfSigned => "tests/certs/self-signed-key.pem",
+            Self::CertificateChain => "tests/certs/leaf-key.pem",
+        }
+    }
+
+    pub fn certificate_chain_path(&self) -> Option<&'static str> {
+        match self {
+            Self::SelfSigned => None,
+            Self::CertificateChain => Some("tests/certs/cert-chain.pem"),
+        }
+    }
+
+    pub fn leaf_certificate(&self) -> X509 {
+        let bytes = match self {
+            Self::SelfSigned => include_str!("../certs/self-signed-cert.pem").as_bytes(),
+            Self::CertificateChain => include_str!("../certs/leaf-cert.pem").as_bytes(),
+        };
+
+        X509::from_pem(bytes).expect("could not get certificate")
+    }
+
+    pub fn intermediate_certificate(&self) -> X509 {
+        let bytes = match self {
+            Self::SelfSigned => return self.leaf_certificate(),
+            Self::CertificateChain => include_str!("../certs/intermediate-cert.pem").as_bytes(),
+        };
+
+        X509::from_pem(bytes).expect("could not get certificate")
+    }
+
+    pub fn root_certificate(&self) -> X509 {
+        let bytes = match self {
+            Self::SelfSigned => return self.leaf_certificate(),
+            Self::CertificateChain => include_str!("../certs/root-cert.pem").as_bytes(),
+        };
+
+        X509::from_pem(bytes).expect("could not get certificate")
+    }
+
+    pub fn leaf_fingerprint(&self) -> String {
+        Self::fingerprint_for_cert(&self.leaf_certificate())
+    }
+
+    pub fn intermediate_fingerprint(&self) -> String {
+        Self::fingerprint_for_cert(&self.intermediate_certificate())
+    }
+
+    pub fn root_fingerprint(&self) -> String {
+        Self::fingerprint_for_cert(&self.root_certificate())
+    }
+
+    fn fingerprint_for_cert(cert: &X509) -> String {
+        let digest = cert
+            .digest(openssl::hash::MessageDigest::sha256())
+            .expect("could not create certificate digest");
+        get_fingerprint_from_u8(&digest)
+    }
+}
+
+/// Used to spawn a test server that listens on a Unix socket that will be cleaned up once dropped.
+pub struct Server {
+    child: Child,
+    socket: TempPath,
+}
+
+impl Drop for Server {
+    fn drop(&mut self) {
+        let _ = self.child.kill();
+    }
+}
+
+impl Server {
+    fn new(cert_type: CertificateType) -> Server {
+        static TEST_NUMBER: AtomicU32 = AtomicU32::new(0);
+
+        let socket = TempPath::from_path(PathBuf::from(format!(
+            "/tmp/.op-cb-test-{}.sock",
+            TEST_NUMBER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+        )));
+
+        let mut command = Command::new("openssl");
+
+        command
+            .arg("s_server")
+            .arg("-unix")
+            .arg(&socket)
+            .arg("-cert")
+            .arg(cert_type.certificate_path())
+            .arg("-key")
+            .arg(cert_type.key_path());
+
+        if let Some(cert_chain) = cert_type.certificate_chain_path() {
+            command
+                .arg("-cert_chain")
+                .arg(cert_chain)
+                .arg("-build_chain");
+        }
+
+        let child = command
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .stdin(Stdio::piped())
+            .spawn()
+            .unwrap();
+
+        Server { child, socket }
+    }
+
+    pub fn connect(cert_type: CertificateType) -> (Server, UnixStream) {
+        let server = Server::new(cert_type);
+
+        // wait up to one second for the server to come up
+        for _i in 1..10 {
+            match UnixStream::connect(&server.socket) {
+                Ok(stream) => return (server, stream),
+                Err(_e) => std::thread::sleep(std::time::Duration::from_millis(100)),
+            }
+        }
+
+        panic!("server did not come up");
+    }
+}
+
+/// Carry out a TLS handshake with the default `SslContext` and the provided certificate type and
+/// fingerprint.
+///
+/// Returns the last result of the `openssl_verify_callback`.
+pub fn handshake(
+    case: CertificateType,
+    fingerprint: Option<String>,
+) -> Option<Result<(), SslVerifyError>> {
+    handshake_with_builder(
+        case,
+        fingerprint,
+        SslContextBuilder::new(SslMethod::tls()).expect("could not get ssl context builder"),
+    )
+}
+
+/// Carry out a TLS handshake with the provided certificate added to the client's `SslContext` for
+/// the provided certificate type and fingerprint.
+///
+/// Returns the last result of the `openssl_verify_callback`.
+pub fn handshake_with_cert(
+    cert_type: CertificateType,
+    fingerprint: Option<String>,
+    cert: X509,
+) -> Option<Result<(), SslVerifyError>> {
+    let mut store = X509StoreBuilder::new().expect("could not create x509 store builder");
+
+    store
+        .add_cert(cert)
+        .expect("could not add custom certificate to context");
+
+    let store = store.build();
+    let mut builder =
+        SslContextBuilder::new(SslMethod::tls()).expect("could not get ssl context builder");
+
+    builder.set_cert_store(store);
+    handshake_with_builder(cert_type, fingerprint, builder)
+}
+
+/// Carry out a TLS handshake with the provided `SslContextBuilder` used to build the
+/// `SslContext` for the provided certificate type and fingerprint.
+///
+/// Returns the last result of the `openssl_verify_callback`.
+fn handshake_with_builder(
+    case: CertificateType,
+    fingerprint: Option<String>,
+    mut builder: SslContextBuilder,
+) -> Option<Result<(), SslVerifyError>> {
+    let last_result = Arc::new(Mutex::new(None));
+    let mv_result = last_result.clone();
+
+    builder.set_verify_callback(SslVerifyMode::PEER, move |p, x| {
+        let res = openssl_verify_callback(p, x, fingerprint.as_deref());
+        let ret = res.is_ok();
+        *mv_result.lock().unwrap() = Some(res);
+
+        ret
+    });
+
+    if super::SSL_NEW_VERIFY {
+        unsafe { std::env::set_var(PROXMOX_NEW_TLS_CHECK_VAR, "1") };
+    }
+
+    let (_server, stream) = Server::connect(case);
+    let ssl = Ssl::new(builder.build().as_ref()).unwrap();
+    // ignore the error from the connect below, we only care about the last result of the
+    // verify callback as that's what we are testing
+    let _res = ssl.connect(stream);
+
+    last_result.lock().unwrap().take()
+}
+
+/// Tests that should behave identical between the two versions of the callback.
+#[cfg(feature = "tls")]
+pub mod tests {
+    use proxmox_http::SslVerifyError;
+
+    use super::*;
+
+    #[test]
+    fn self_signed_certificate_with_fingerprint_valid() {
+        let cert = CertificateType::SelfSigned;
+
+        assert_eq!(handshake(cert, Some(cert.leaf_fingerprint())), Some(Ok(())));
+    }
+
+    #[test]
+    fn self_signed_certificate_without_fingerprint_invalid() {
+        let cert = CertificateType::SelfSigned;
+
+        assert_eq!(
+            handshake(cert, None),
+            Some(Err(SslVerifyError::UntrustedCertificate {
+                fingerprint: cert.leaf_fingerprint(),
+            }))
+        );
+    }
+
+    #[test]
+    fn self_signed_certificate_with_incorrect_fingerprint_invalid() {
+        let cert = CertificateType::SelfSigned;
+        let expected = "ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5".to_string();
+
+        assert_eq!(
+            handshake(cert, Some(expected.clone())),
+            Some(Err(SslVerifyError::FingerprintMismatch {
+                fingerprint: cert.leaf_fingerprint(),
+                expected,
+            }))
+        );
+    }
+
+    #[test]
+    fn self_signed_certificate_with_cert_in_context_valid() {
+        let cert = CertificateType::SelfSigned;
+
+        assert_eq!(
+            handshake_with_cert(cert, None, cert.leaf_certificate()),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    fn self_signed_certificate_with_cert_in_context_and_correct_fingerprint_valid() {
+        let cert = CertificateType::SelfSigned;
+
+        assert_eq!(
+            handshake_with_cert(cert, Some(cert.leaf_fingerprint()), cert.leaf_certificate()),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_correct_leaf_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake(cert, Some(cert.leaf_fingerprint()),),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_correct_intermediate_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake(cert, Some(cert.intermediate_fingerprint())),
+            Some(Ok(()))
+        )
+    }
+
+    #[test]
+    fn certificate_chain_with_correct_root_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(handshake(cert, Some(cert.root_fingerprint())), Some(Ok(())))
+    }
+
+    #[test]
+    fn certificate_chain_with_incorrect_fingerprint_invalid() {
+        let cert = CertificateType::CertificateChain;
+        let expected = "ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5".to_string();
+
+        assert_eq!(
+            handshake(cert, Some(expected.clone())),
+            Some(Err(SslVerifyError::FingerprintMismatch {
+                fingerprint: cert.leaf_fingerprint(),
+                expected,
+            }))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_root_cert_in_context_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake_with_cert(cert, None, cert.root_certificate()),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    /// This is considered invalid, because that is how OpenSSL would handle such a chain. If the
+    /// root is not trusted, it would abort the handshake right away, not caring whether a
+    /// certificate further down the chain would be valid.
+    ///
+    /// > The certificate chain is checked starting with the deepest nesting level (the root CA
+    /// > certificate) and worked upward to the peer's certificate.
+    /// > [..]
+    /// > If verify_callback returns 0, the verification process is immediately stopped with
+    /// > "verification failed" state. If SSL_VERIFY_PEER is set, a verification failure alert is
+    /// > sent to the peer and the TLS/SSL handshake is terminated.
+    /// >
+    /// > - https://docs.openssl.org/master/man3/SSL_CTX_set_verify/#notes
+    fn certificate_chain_with_intermediate_cert_in_context_invalid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake_with_cert(cert, None, cert.intermediate_certificate()),
+            Some(Err(SslVerifyError::UntrustedCertificate {
+                fingerprint: cert.leaf_fingerprint()
+            }))
+        );
+    }
+
+    #[test]
+    /// See description of [`certificate_chain_with_intermediate_cert_in_context_invalid`].
+    fn certificate_chain_with_leaf_cert_in_context_invalid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake_with_cert(cert, None, cert.leaf_certificate()),
+            Some(Err(SslVerifyError::UntrustedCertificate {
+                fingerprint: cert.leaf_fingerprint()
+            }))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_root_cert_in_context_and_correct_root_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake_with_cert(
+                cert,
+                Some(cert.intermediate_fingerprint()),
+                cert.root_certificate()
+            ),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_root_cert_in_context_and_correct_intermediate_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake_with_cert(
+                cert,
+                Some(cert.intermediate_fingerprint()),
+                cert.root_certificate()
+            ),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_root_cert_in_context_and_correct_leaf_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+
+        assert_eq!(
+            handshake_with_cert(cert, Some(cert.leaf_fingerprint()), cert.root_certificate()),
+            Some(Ok(()))
+        );
+    }
+}
diff --git a/proxmox-http/tests/openssl_verify_cb_new.rs b/proxmox-http/tests/openssl_verify_cb_new.rs
new file mode 100644
index 00000000..b1bf53db
--- /dev/null
+++ b/proxmox-http/tests/openssl_verify_cb_new.rs
@@ -0,0 +1,57 @@
+//! Integration tests for the `openssl_verify_callback` that should be used for all client TLS
+//! verification going forward. This differs to the legacy behavior in one way. If a fingerprint is
+//! provided and no certificate matches it, the connection is aborted, regardless of OpenSSL's
+//! validity checks.
+//!
+//! Other than that the tests currently encode the following behavior for both flows:
+//!
+//! * A self-signed certificate is trusted either when:
+//!     1. A matching fingerprint was provided.
+//!     2. No fingerprint was provided, but the OpenSSL context trusts it.
+//! * A certificate chain is trusted when:
+//!     1. A fingerprint is provided that matches any certificate in the chain.
+//!     2. No fingerprint is provided, but the OpenSSL trust context declares the whole chain
+//!        valid.
+//!
+#[cfg(feature = "tls")]
+mod common;
+
+// Make sure tests in the common module use the new verify callback flow.
+#[cfg(feature = "tls")]
+const SSL_NEW_VERIFY: bool = true;
+
+#[cfg(feature = "tls")]
+mod openssl_verify_cb_new {
+
+    use proxmox_http::SslVerifyError;
+
+    use super::common::*;
+
+    #[test]
+    fn self_signed_certificate_with_cert_in_context_and_incorrect_fingerprint_invalid() {
+        let cert = CertificateType::SelfSigned;
+        let expected = "ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5".to_string();
+
+        assert_eq!(
+            handshake_with_cert(cert, Some(expected.clone()), cert.leaf_certificate()),
+            Some(Err(SslVerifyError::FingerprintMismatch {
+                fingerprint: cert.leaf_fingerprint(),
+                expected,
+            }))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_root_cert_in_context_and_incorrect_fingerprint_invalid() {
+        let cert = CertificateType::CertificateChain;
+        let expected = "ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5".to_string();
+
+        assert_eq!(
+            handshake_with_cert(cert, Some(expected.clone()), cert.root_certificate()),
+            Some(Err(SslVerifyError::FingerprintMismatch {
+                fingerprint: cert.leaf_fingerprint(),
+                expected
+            }))
+        );
+    }
+}
diff --git a/proxmox-http/tests/openssl_verify_cb_old.rs b/proxmox-http/tests/openssl_verify_cb_old.rs
new file mode 100644
index 00000000..dbeaecd6
--- /dev/null
+++ b/proxmox-http/tests/openssl_verify_cb_old.rs
@@ -0,0 +1,49 @@
+//! Integration tests for the `openssl_verify_callback` that check that the intended legacy
+//! behavior works correctly. The legacy behavior differs from the new intended behavior in one
+//! main way. If a fingerprint was provided and it does not match any certificate provided by the
+//! server, but OpenSSL declares the certificate valid, the callback accepted it as well.
+//!
+//! Other than that the tests currently encode the following behavior for both flows:
+//!
+//! * A self-signed certificate is trusted either when:
+//!     1. A matching fingerprint was provided.
+//!     2. No fingerprint was provided, but the OpenSSL context trusts it.
+//! * A certificate chain is trusted when:
+//!     1. A fingerprint is provided that matches any certificate in the chain.
+//!     2. No fingerprint is provided, but the OpenSSL trust context declares the whole chain
+//!        valid.
+//!
+#[cfg(feature = "tls")]
+mod common;
+
+// Make sure tests in the common module use the old verify callback flow.
+#[cfg(feature = "tls")]
+const SSL_NEW_VERIFY: bool = false;
+
+#[cfg(feature = "tls")]
+pub mod openssl_verify_cb_old {
+
+    use super::common::*;
+
+    #[test]
+    fn self_signed_certificate_with_cert_in_context_and_incorrect_fingerprint_valid() {
+        let cert = CertificateType::SelfSigned;
+        let expected = "ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5".to_string();
+
+        assert_eq!(
+            handshake_with_cert(cert, Some(expected), cert.leaf_certificate()),
+            Some(Ok(()))
+        );
+    }
+
+    #[test]
+    fn certificate_chain_with_root_cert_in_context_and_incorrect_fingerprint_valid() {
+        let cert = CertificateType::CertificateChain;
+        let expected = "ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5".to_string();
+
+        assert_eq!(
+            handshake_with_cert(cert, Some(expected), cert.root_certificate()),
+            Some(Ok(()))
+        );
+    }
+}
-- 
2.47.3





      parent reply	other threads:[~2026-06-25 11:23 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-17  8:59 [PATCH proxmox{,-backup,-websocket-tunnel} v3 0/6] unify openssl callback logic Dominik Csapak
2026-06-17  8:59 ` [PATCH proxmox v3 1/6] http: factor out openssl verification callback Dominik Csapak
2026-06-25 11:19   ` Shannon Sterz
2026-06-17  8:59 ` [PATCH proxmox v3 2/6] http: tls: use legacy behavior when PROXMOX_NEW_TLS_CHECK is not set Dominik Csapak
2026-06-25 11:19   ` Shannon Sterz
2026-06-17  8:59 ` [PATCH proxmox v3 3/6] client: use proxmox-http's openssl verification callback Dominik Csapak
2026-06-25 11:19   ` Shannon Sterz
2026-06-17  8:59 ` [PATCH proxmox-backup v3 4/6] pbs-client: use proxmox-https openssl callback Dominik Csapak
2026-06-25 11:19   ` Shannon Sterz
2026-06-17  8:59 ` [PATCH proxmox-backup v3 5/6] pbs-client: honor already verified fingerprint Dominik Csapak
2026-06-17  8:59 ` [PATCH proxmox-websocket-tunnel v3 6/6] use proxmox-http's openssl callback Dominik Csapak
2026-06-25 11:19   ` Shannon Sterz
2026-06-25 11:19 ` [PATCH proxmox{,-backup,-websocket-tunnel} v3 0/6] unify openssl callback logic Shannon Sterz
2026-06-25 11:22   ` [PATCH proxmox 1/3] http: tls: move PROXMOX_NEW_TLS_CHECK env var name into constant Shannon Sterz
2026-06-25 11:22     ` [PATCH proxmox 2/3] http: tls: implement `PartialEq` for `SslVerifyError` Shannon Sterz
2026-06-25 11:22     ` Shannon Sterz [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260625112236.188257-3-s.sterz@proxmox.com \
    --to=s.sterz@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal