From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 888421FF146 for ; Tue, 09 Jun 2026 15:16:18 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7741B113C8; Tue, 9 Jun 2026 15:16:04 +0200 (CEST) From: Thomas Ellmenreich To: pve-devel@lists.proxmox.com Subject: [PATCH common 1/2] fix #5978: pem parser: relax parsing of chain entries: Date: Tue, 9 Jun 2026 15:15:07 +0200 Message-ID: <20260609131549.104216-2-t.ellmenreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260609131549.104216-1-t.ellmenreich@proxmox.com> References: <20260609131549.104216-1-t.ellmenreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1781010911742 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.098 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 55PF6NU7YWUS326CZS7HJPMHRLZZH7WT X-Message-ID-Hash: 55PF6NU7YWUS326CZS7HJPMHRLZZH7WT X-MailFrom: t.ellmenreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Thomas Ellmenreich X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Relaxes the parser to allow for text and whitespaces inbetween certchain entries, thus also refactored the extraction to only extract chain entries from the string. Added testsuite to cover a bunch of parsing edge cases. Signed-off-by: Thomas Ellmenreich --- src/PVE/Certificate.pm | 23 ++-- test/Makefile | 1 + test/check_pem_test.pl | 264 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+), 12 deletions(-) create mode 100755 test/check_pem_test.pl diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm index b8415e2..fc45b63 100644 --- a/src/PVE/Certificate.pm +++ b/src/PVE/Certificate.pm @@ -136,24 +136,23 @@ sub strip_leading_text { sub split_pem { my ($content, %opts) = @_; - my $label = $opts{label} // 'CERTIFICATE'; + + my $re = $pem_re->($opts{label} // 'CERTIFICATE'); - my $header = $header_re->($label); - return split(/(?=$header)/, $content); + if (!$content =~ /^$re$/) { + return undef if $opts{noerr}; + die "not a valid PEM-formatted string.\n"; + } + + my @pem_chain = $content =~ /($re)/sg; + return @pem_chain; } sub check_pem { my ($content, %opts) = @_; - $content = strip_leading_text($content); - - my $re = $pem_re->($opts{label} // 'CERTIFICATE'); - $re = qr/($re\n+)*$re/ if $opts{multiple}; - - return $content if $content =~ /^$re$/; # OK - - return undef if $opts{noerr}; - die "not a valid PEM-formatted string.\n"; + my @result = split_pem($content, %opts); + return join("", @result); } sub pem_to_der { diff --git a/test/Makefile b/test/Makefile index 9b9f81b..de6ae60 100644 --- a/test/Makefile +++ b/test/Makefile @@ -14,6 +14,7 @@ TESTS = lock_file.test \ is_deeply_test.test \ section_config_property_isolation_test.pl \ file-test.pl \ + check_pem_test.pl \ all: diff --git a/test/check_pem_test.pl b/test/check_pem_test.pl new file mode 100755 index 0000000..dc5fd7e --- /dev/null +++ b/test/check_pem_test.pl @@ -0,0 +1,264 @@ +#!/usr/bin/perl +# Tests the PVE::Certificate::check_pem and the +# PVE::Certificate::split_pem function for +# correctness and coverage of edgecases. + +use lib '../src'; + +use Test::More; + +use PVE::Certificate; + +# Arrange +my $setup = [ + { + successful => 1, + name => "full pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----- +MIIBsjCCAVugAwIBAgIJAO2g8Z0dXk9tMAoGCCqGSM49BAMCMEUxCzAJBgNVBAYT +AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UEBwwHQmVya2VsZXkxEDAOBgNVBAoMB1Rl +c3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBFMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB0JlcmtlbGV5MRAwDgYDVQQKDAdU +ZXN0IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEv5Q8q1p7qZ2gqkQ0Qn5x +0n9yqv8n8n7n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8aNT +MFEwHQYDVR0OBBYEFOu2Y0bq8v3z7qkq1m1Qwqkq1m1QMB8GA1UdIwQYMBaAFOu2 +Y0bq8v3z7qkq1m1Qwqkq1m1QMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID +SAAwRQIhANfakefakefakefakefakefakefakefakefake +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "other label pem", + label => "SOME LABEL", + pem => <<'EOF', +-----BEGIN SOME LABEL----- +MIIBsjCCAVugAwIBAgIJAO2g8Z0dXk9tMAoGCCqGSM49BAMCMEUxCzAJBgNVBAYT +AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UEBwwHQmVya2VsZXkxEDAOBgNVBAoMB1Rl +c3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBFMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB0JlcmtlbGV5MRAwDgYDVQQKDAdU +ZXN0IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEv5Q8q1p7qZ2gqkQ0Qn5x +0n9yqv8n8n7n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8aNT +MFEwHQYDVR0OBBYEFOu2Y0bq8v3z7qkq1m1Qwqkq1m1QMB8GA1UdIwQYMBaAFOu2 +Y0bq8v3z7qkq1m1Qwqkq1m1QMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID +SAAwRQIhANfakefakefakefakefakefakefakefakefake +-----END SOME LABEL----- +EOF + }, + { + successful => 1, + name => "simple pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----- +test +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "spaced out pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----- + +test + +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "only newline pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "many newlines pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----- + + + +-----END CERTIFICATE----- +EOF + }, + { + successful => 0, + name => "no content pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----------END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "preceding content pem", + pem => <<'EOF', +some extended content +-----BEGIN CERTIFICATE----- +test +-----END CERTIFICATE----- +EOF +, + expected_result => << 'EOF', +-----BEGIN CERTIFICATE----- +test +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "following content pem", + pem => <<'EOF', +-----BEGIN CERTIFICATE----- +test +-----END CERTIFICATE----- +some extended content +EOF +, + expected_result => << 'EOF', +-----BEGIN CERTIFICATE----- +test +-----END CERTIFICATE----- +EOF + }, + { + successful => 0, + name => "empty pem", + pem => <<'EOF', + +EOF + }, + { + successful => 0, + name => "upsidedown pem", + pem => <<'EOF', +-----END CERTIFICATE----- +test +-----BEGIN CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "multi pem", + multiple => 1, + pem => <<'EOF', +-----BEGIN CERTIFICATE----- +12JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +22JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +32JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +42JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "big pem", + multiple => 1, + pem => <<'EOF', +Subject: CN=some.test.content +Issuer: CN=Org Issuing CA 3 +-----BEGIN CERTIFICATE----- +1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +Subject: CN=Org Issuing CA 3 +Issuer: CN=Org Root CA +-----BEGIN CERTIFICATE----- +2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +Subject: CN=Org Root CA +Issuer: CN=Org Root CA +-----BEGIN CERTIFICATE----- +3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +EOF +, + expected_result => << 'EOF', +-----BEGIN CERTIFICATE----- +1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +EOF + }, + { + successful => 1, + name => "other real pem", + pem => <<'EOF', +Subject: CN=Some subject with 2 Points,O=A Organization,L=Some Lation,C=TT +Issuer: CN=The Orgs Name RootCA 2015,O=The Orgs name Institutions Cert. Authority,L=Location,C=TT +-----BEGIN CERTIFICATE----- +1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +EOF +, + expected_result => << 'EOF', +-----BEGIN CERTIFICATE----- +1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i +dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h +dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3 +c2JlZ2tseTxzZWl2IGF3ZWlidm4= +-----END CERTIFICATE----- +EOF + } +]; + +for my $data ($setup->@*) { + # Act + my $check_result = PVE::Certificate::check_pem( + $data->{pem}, + noerr => 1, + multiple => $data->{multiple}, + label => $data->{label}, + ); + + # Assert + if($data->{successful}) { + ok($check_result, $data->{name} . " accepted"); + } else { + ok(!$check_result, $data->{name} . " correctly rejected"); + } + + my $expected_result = $data->{expected_result} + ? $data->{expected_result} + : $data->{pem}; + + if ($data->{successful}) { + ok($check_result eq $expected_result, $data->{name} . " has correct check output"); + } +} + +done_testing(); -- 2.47.3