From: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Thomas Ellmenreich <t.ellmenreich@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 [thread overview]
Message-ID: <20260609131549.104216-2-t.ellmenreich@proxmox.com> (raw)
In-Reply-To: <20260609131549.104216-1-t.ellmenreich@proxmox.com>
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 <t.ellmenreich@proxmox.com>
---
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
next prev parent reply other threads:[~2026-06-09 13:16 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-09 13:15 [PATCH common/proxmox-acme 0/2] fix #5978: pem parser: relax parsing of chain entries Thomas Ellmenreich
2026-06-09 13:15 ` Thomas Ellmenreich [this message]
2026-06-09 13:15 ` [PATCH proxmox-acme 2/2] fix #5978: pem parser: relax parsing of chain entries: Thomas Ellmenreich
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=20260609131549.104216-2-t.ellmenreich@proxmox.com \
--to=t.ellmenreich@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