api request helper: enforce TLS cert-check and add cert-fingerprint option

Currently, we do not verify the TLS certificate for API requests
external IPAM and DNS integration. This could allow man-in-the-middle
attacks, albeit most IPAM infrastructure is on controlled and isolated
LANs, so it's not something that should frequently happen; and
technically our IPAM integration is still marked as tech-preview,
which had its reasons.

Enforce verification, and allow users to pass a cert SHA256
fingerprint to ensure a certificates validity if it's not trusted by
the system trust store, as it's, e.g., the case for self-signed certs.

The code was adapted from the one in pve-apiclient, which we cannot
reuse directly as it is only implemented for requests against PVE
nodes, not as a generic HTTP client request helper.

Add the new dependency `libio-socket-ssl-perl` required to get the
verify callback for the TLS certificate used for cert-fingerprint
checking.

Signed-off-by: Hannes Duerr <h.duerr@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
 [TL: return valid for non-leaf certs and rewrite commit message]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Hannes Duerr 2025-02-10 15:19:25 +01:00 committed by Thomas Lamprecht
parent 77671ba327
commit 894d2d33e3
2 changed files with 22 additions and 2 deletions

1
debian/control vendored
View file

@ -19,6 +19,7 @@ Package: libpve-network-perl
Architecture: all Architecture: all
Depends: libpve-common-perl (>= 5.0-45), Depends: libpve-common-perl (>= 5.0-45),
pve-cluster (>= 8.0.10), pve-cluster (>= 8.0.10),
libio-socket-ssl-perl,
libnet-subnet-perl, libnet-subnet-perl,
libnet-ip-perl, libnet-ip-perl,
libnetaddr-ip-perl, libnetaddr-ip-perl,

View file

@ -3,7 +3,9 @@ package PVE::Network::SDN;
use strict; use strict;
use warnings; use warnings;
use IO::Socket::SSL; # important for SSL_verify_callback
use JSON; use JSON;
use Net::SSLeay;
use PVE::INotify; use PVE::INotify;
@ -256,7 +258,7 @@ sub encode_value {
#helpers #helpers
sub api_request { sub api_request {
my ($method, $url, $headers, $data) = @_; my ($method, $url, $headers, $data, $expected_fingerprint) = @_;
my $encoded_data = to_json($data) if $data; my $encoded_data = to_json($data) if $data;
@ -270,7 +272,24 @@ sub api_request {
$ua->env_proxy; $ua->env_proxy;
} }
$ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00); if (defined($expected_fingerprint)) {
my $ssl_verify_callback = sub {
my (undef, undef, undef, undef, $cert, $depth) = @_;
# we don't care about intermediate or root certificates, always return as valid as the
# callback will be executed for all levels and all must be valid.
return 1 if $depth != 0;
my $fingerprint = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
return $fingerprint eq $expected_fingerprint ? 1 : 0;
};
$ua->ssl_opts(
verify_hostname => 0,
SSL_verify_mode => SSL_VERIFY_PEER,
SSL_verify_callback => $ssl_verify_callback,
);
}
my $response = $ua->request($req); my $response = $ua->request($req);