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
prev 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox