]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/Certificate.pm
bump version to 8.2.1
[pve-common.git] / src / PVE / Certificate.pm
index 65c5c8f6f2f5c2a1e73d7236b9229dc76b56bd1f..f67f6cd5c65ccfc42589d3fb53baf53649b4ff58 100644 (file)
@@ -78,11 +78,19 @@ PVE::JSONSchema::register_standard_option('pve-certificate-info', {
            format => 'pem-certificate',
            optional => 1,
        },
+       'public-key-type' => {
+           type => 'string',
+           description => 'Certificate\'s public key algorithm',
+           optional => 1,
+       },
+       'public-key-bits' => {
+           type => 'integer',
+           description => 'Certificate\'s public key size',
+           optional => 1,
+       },
     },
 });
 
-# see RFC 7468
-my $b64_char_re = qr![0-9A-Za-z\+/]!;
 my $header_re = sub {
     my ($label) = @_;
     return qr!-----BEGIN\ $label-----(?:\s|\n)*!;
@@ -94,6 +102,7 @@ my $footer_re = sub {
 my $pem_re = sub {
     my ($label) = @_;
 
+    my $b64_char_re = qr![0-9A-Za-z\+/]!; # see RFC 7468
     my $header = $header_re->($label);
     my $footer = $footer_re->($label);
 
@@ -124,22 +133,15 @@ sub split_pem {
 sub check_pem {
     my ($content, %opts) = @_;
 
-    my $label = $opts{label} // 'CERTIFICATE';
-    my $multiple = $opts{multiple};
-    my $noerr = $opts{noerr};
-
     $content = strip_leading_text($content);
 
-    my $re = $pem_re->($label);
+    my $re = $pem_re->($opts{label} // 'CERTIFICATE');
+    $re = qr/($re\n+)*$re/ if $opts{multiple};
 
-    $re = qr/($re\n+)*$re/ if $multiple;
+    return $content if $content =~ /^$re$/; # OK
 
-    if ($content =~ /^$re$/) {
-       return $content;
-    } else {
-       return undef if $noerr;
-       die "not a valid PEM-formatted string.\n";
-    }
+    return undef if $opts{noerr};
+    die "not a valid PEM-formatted string.\n";
 }
 
 sub pem_to_der {
@@ -169,15 +171,10 @@ sub der_to_pem {
     return "-----BEGIN $label-----\n$b64\n-----END $label-----\n";
 }
 
-my $ssl_die = sub {
-    my ($msg) = @_;
-    Net::SSLeay::die_now($msg);
-};
-
-my $ssl_warn = sub {
+my sub ssl_die {
     my ($msg) = @_;
-    Net::SSLeay::print_errs();
-    warn $msg if $msg;
+    warn Net::SSLeay::print_errs();
+    Net::SSLeay::die_now("$msg\n");
 };
 
 my $read_certificate = sub {
@@ -186,13 +183,11 @@ my $read_certificate = sub {
     die "'$cert_path' does not exist!\n" if ! -e $cert_path;
 
     my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r')
-       or $ssl_die->("unable to read '$cert_path' - $!\n");
+       or ssl_die("unable to read '$cert_path' - $!");
 
     my $cert = Net::SSLeay::PEM_read_bio_X509($bio);
-    if (!$cert) {
-       Net::SSLeay::BIO_free($bio);
-       die "unable to read certificate from '$cert_path'\n";
-    }
+    Net::SSLeay::BIO_free($bio);
+    die "unable to read certificate from '$cert_path'\n" if !$cert;
 
     return $cert;
 };
@@ -200,12 +195,59 @@ my $read_certificate = sub {
 sub convert_asn1_to_epoch {
     my ($asn1_time) = @_;
 
-    $ssl_die->("invalid ASN1 time object\n") if !$asn1_time;
+    ssl_die("invalid ASN1 time object") if !$asn1_time;
     my $iso_time = Net::SSLeay::P_ASN1_TIME_get_isotime($asn1_time);
-    $ssl_die->("unable to parse ASN1 time\n") if $iso_time eq '';
+    ssl_die("unable to parse ASN1 time") if $iso_time eq '';
     return Date::Parse::str2time($iso_time);
 }
 
+sub get_certificate_fingerprint {
+    my ($cert_path) = @_;
+
+    my $cert = $read_certificate->($cert_path);
+
+    my $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
+    Net::SSLeay::X509_free($cert);
+
+    die "unable to get fingerprint for '$cert_path' - got empty value\n"
+       if !defined($fp) || $fp eq '';
+
+    return $fp;
+}
+
+sub assert_certificate_matches_key {
+    my ($cert_path, $key_path) = @_;
+
+    die "No certificate path given!\n" if !$cert_path;
+    die "No certificate key path given!\n" if !$key_path;
+
+    die "Certificate at '$cert_path' does not exist!\n" if ! -e $cert_path;
+    die "Certificate key '$key_path' does not exist!\n" if ! -e $key_path;
+
+    my $ctx = Net::SSLeay::CTX_new()
+       or ssl_die("Failed to create SSL context in order to verify private key");
+
+    eval {
+       my $filetype = &Net::SSLeay::FILETYPE_PEM;
+
+       Net::SSLeay::CTX_use_PrivateKey_file($ctx, $key_path, $filetype)
+           or ssl_die("Failed to load private key from '$key_path' into SSL context");
+
+       Net::SSLeay::CTX_use_certificate_file($ctx, $cert_path, $filetype)
+           or ssl_die("Failed to load certificate from '$cert_path' into SSL context");
+
+       Net::SSLeay::CTX_check_private_key($ctx)
+           or ssl_die("Failed to validate private key and certificate");
+    };
+    my $err = $@;
+
+    Net::SSLeay::CTX_free($ctx);
+
+    die $err if $err;
+
+    return 1;
+}
+
 sub get_certificate_info {
     my ($cert_path) = @_;
 
@@ -242,13 +284,11 @@ sub get_certificate_info {
 
     $info->{fingerprint} = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
 
-    my $subject = Net::SSLeay::X509_get_subject_name($cert);
-    if ($subject) {
+    if (my $subject = Net::SSLeay::X509_get_subject_name($cert)) {
        $info->{subject} = Net::SSLeay::X509_NAME_oneline($subject);
     }
 
-    my $issuer = Net::SSLeay::X509_get_issuer_name($cert);
-    if ($issuer) {
+    if (my $issuer = Net::SSLeay::X509_get_issuer_name($cert)) {
        $info->{issuer} = Net::SSLeay::X509_NAME_oneline($issuer);
     }
 
@@ -312,14 +352,17 @@ sub generate_csr {
     my $san = [ map { $_->{value} } grep { $_->{type} eq 'dns' } @$identifiers ];
     die "DNS identifiers are required to generate a CSR.\n" if !scalar @$san;
 
+    # optional
+    my $common_name = delete($attr{common_name}) // $san->[0];
+
     my $md = eval { Net::SSLeay::EVP_get_digestbyname($dig_alg) };
     die "Invalid digest algorithm '$dig_alg'\n" if !$md;
 
     my ($bio, $pk, $req);
 
     my $cleanup = sub {
-       my ($warn, $die_msg) = @_;
-       $ssl_warn->() if $warn;
+       my ($die_msg, $no_warn) = @_;
+       Net::SSLeay::print_errs() if !$no_warn;
 
        Net::SSLeay::X509_REQ_free($req) if  $req;
        Net::SSLeay::EVP_PKEY_free($pk) if $pk;
@@ -331,75 +374,70 @@ sub generate_csr {
     # this unfortunately causes a small memory leak, since there is no
     # X509_NAME_free() (yet)
     my $name = Net::SSLeay::X509_NAME_new();
-    $ssl_die->("Failed to allocate X509_NAME object\n") if !$name;
+    ssl_die("Failed to allocate X509_NAME object") if !$name;
     my $add_name_entry = sub {
        my ($k, $v) = @_;
-       if (!Net::SSLeay::X509_NAME_add_entry_by_txt($name,
-                                                    $k,
-                                                    &Net::SSLeay::MBSTRING_UTF8,
-                                                    encode('utf-8', $v))) {
-           $cleanup->(1, "Failed to add '$k'='$v' to DN\n");
-       }
+
+       my $res = Net::SSLeay::X509_NAME_add_entry_by_txt(
+           $name, $k, &Net::SSLeay::MBSTRING_UTF8, encode('utf-8', $v));
+
+       $cleanup->("Failed to add '$k'='$v' to DN\n") if !$res;
     };
 
-    $add_name_entry->('CN', @$san[0]);
+    $add_name_entry->('CN', $common_name);
     for (qw(C ST L O OU)) {
-        if (defined(my $v = $attr{$_})) {
+       if (defined(my $v = $attr{$_})) {
            $add_name_entry->($_, $v);
-        }
+       }
     }
 
     if (defined($pem_key)) {
        my $bio_s_mem = Net::SSLeay::BIO_s_mem();
-       $cleanup->(1, "Failed to allocate BIO_s_mem for private key\n")
-           if !$bio_s_mem;
+       $cleanup->("Failed to allocate BIO_s_mem for private key\n") if !$bio_s_mem;
 
        $bio = Net::SSLeay::BIO_new($bio_s_mem);
-       $cleanup->(1, "Failed to allocate BIO for private key\n") if !$bio;
+       $cleanup->("Failed to allocate BIO for private key\n") if !$bio;
 
-       $cleanup->(1, "Failed to write PEM-encoded key to BIO\n")
+       $cleanup->("Failed to write PEM-encoded key to BIO\n")
            if Net::SSLeay::BIO_write($bio, $pem_key) <= 0;
 
        $pk = Net::SSLeay::PEM_read_bio_PrivateKey($bio);
-       $cleanup->(1, "Failed to read private key into EVP_PKEY\n") if !$pk;
+       $cleanup->("Failed to read private key into EVP_PKEY\n") if !$pk;
     } else {
        $pk = Net::SSLeay::EVP_PKEY_new();
-       $cleanup->(1, "Failed to allocate EVP_PKEY for private key\n") if !$pk;
+       $cleanup->("Failed to allocate EVP_PKEY for private key\n") if !$pk;
 
        my $rsa = Net::SSLeay::RSA_generate_key($bits, 65537);
-       $cleanup->(1, "Failed to generate RSA key pair\n") if !$rsa;
+       $cleanup->("Failed to generate RSA key pair\n") if !$rsa;
 
-       $cleanup->(1, "Failed to assign RSA key to EVP_PKEY\n")
+       $cleanup->("Failed to assign RSA key to EVP_PKEY\n")
            if !Net::SSLeay::EVP_PKEY_assign_RSA($pk, $rsa);
     }
 
     $req = Net::SSLeay::X509_REQ_new();
-    $cleanup->(1, "Failed to allocate X509_REQ\n") if !$req;
+    $cleanup->("Failed to allocate X509_REQ\n") if !$req;
 
-    $cleanup->(1, "Failed to set subject name\n")
+    $cleanup->("Failed to set subject name\n")
        if (!Net::SSLeay::X509_REQ_set_subject_name($req, $name));
 
-    $cleanup->(1, "Failed to add extensions to CSR\n")
-       if !Net::SSLeay::P_X509_REQ_add_extensions($req,
-               &Net::SSLeay::NID_key_usage => 'digitalSignature,keyEncipherment',
-               &Net::SSLeay::NID_basic_constraints => 'CA:FALSE',
-               &Net::SSLeay::NID_ext_key_usage => 'serverAuth,clientAuth',
-               &Net::SSLeay::NID_subject_alt_name => join(',', map { "DNS:$_" } @$san),
-       );
+    Net::SSLeay::P_X509_REQ_add_extensions(
+       $req,
+       &Net::SSLeay::NID_key_usage => 'digitalSignature,keyEncipherment',
+       &Net::SSLeay::NID_basic_constraints => 'CA:FALSE',
+       &Net::SSLeay::NID_ext_key_usage => 'serverAuth,clientAuth',
+       &Net::SSLeay::NID_subject_alt_name => join(',', map { "DNS:$_" } @$san),
+    ) or $cleanup->("Failed to add extensions to CSR\n");
 
-    $cleanup->(1, "Failed to set public key\n")
-       if !Net::SSLeay::X509_REQ_set_pubkey($req, $pk);
+    $cleanup->("Failed to set public key\n") if !Net::SSLeay::X509_REQ_set_pubkey($req, $pk);
 
-    $cleanup->(1, "Failed to set CSR version\n")
-       if !Net::SSLeay::X509_REQ_set_version($req, 2);
+    $cleanup->("Failed to set CSR version\n") if !Net::SSLeay::X509_REQ_set_version($req, 0);
 
-    $cleanup->(1, "Failed to sign CSR\n")
-       if !Net::SSLeay::X509_REQ_sign($req, $pk, $md);
+    $cleanup->("Failed to sign CSR\n") if !Net::SSLeay::X509_REQ_sign($req, $pk, $md);
 
     my $pk_pem = Net::SSLeay::PEM_get_string_PrivateKey($pk);
     my $req_pem = Net::SSLeay::PEM_get_string_X509_REQ($req);
 
-    $cleanup->();
+    $cleanup->(undef, 1);
 
     return wantarray ? ($req_pem, $pk_pem) : $req_pem;
 }