public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH common/proxmox-acme v4 0/3] fix #5978: pem parser: relax parsing of chain entries
@ 2026-08-05 14:11 Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH common v4 1/3] " Thomas Ellmenreich
                   ` (3 more replies)
  0 siblings, 4 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-08-05 14:11 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Ellmenreich

According to RFC 8555, expected certchains should come
without whitespace or explanatory texts inbetween chain
entries. These two patches relax our parser to also
accept text or whitespaces inbetween chain entries.

To make sure that the acme changes work as expected I
setup the pebble acme server [1] locally, and worked
through the acme flow to get a new certificate. I then
manually modified the final certificate to contain
descriptive text which worked without issues.

changes since v3:
- check_pem was reworked to not be recursive

- the check_pem tests now also check the erroring version to
  make sure that an exception is thrown

- moved the tests to their own commit

- wrapped the error message produced by check_pem when
  called in proxmox-acme with chomp to avoid double newlines

- corrected the capitalization of different acronyms

changes since v2:
- cleaner implementation and correction of mistakes in
  check_pem in pve-common

- get_certificate in proxmox-acme now correctly calls
  check_pem with the 'multiple' option enabled

- removed ambiguity in the error messages of
  get_certificate

- correction of tests, to better compare returned value
  to expected value

- performed proper end-to-end test with pebble [1]

- proper formatting (hopefully)

changes since v1:
- Where in v1 check_pem was just a wrapper of split_pem,
  they now perform different functions

- split_pem now purely splits the PEM chain into separate
  entries and does no further validation. Returning each
  entry with its leading text.

- check_pem retains the original functionality, except
  when the multiple option is active, in which case it
  uses split_pem to get single entries and then calls
  itself recursively

- On the ACME side, errors are now captured, wrapped,
  and then rethrown.

[1] https://github.com/letsencrypt/pebble


pve-common:

Thomas Ellmenreich (2):
  fix #5978: pem parser: relax parsing of chain entries
  pem parser: add testsuite for check_ and split_pem

 src/PVE/Certificate.pm |  39 ++++-
 test/Makefile          |   2 +
 test/check_pem_test.pl | 365 +++++++++++++++++++++++++++++++++++++++++
 test/split_pem_test.pl | 279 +++++++++++++++++++++++++++++++
 4 files changed, 677 insertions(+), 8 deletions(-)
 create mode 100755 test/check_pem_test.pl
 create mode 100755 test/split_pem_test.pl


proxmox-acme:

Thomas Ellmenreich (1):
  fix #5978: pem parser: relax parsing of chain entries:

 src/PVE/ACME.pm | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)


Summary over all repositories:
  5 files changed, 684 insertions(+), 13 deletions(-)

-- 
Generated by murpp 0.12.0




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH common v4 1/3] fix #5978: pem parser: relax parsing of chain entries
  2026-08-05 14:11 [PATCH common/proxmox-acme v4 0/3] fix #5978: pem parser: relax parsing of chain entries Thomas Ellmenreich
@ 2026-08-05 14:11 ` Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_ and split_pem Thomas Ellmenreich
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-08-05 14:11 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Ellmenreich

Relaxes the parser to allow for text and whitespaces inbetween certchain
entries. The splitting of PEM chains was also reworked to split each entry at
its end, grouping it with its leading text.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 src/PVE/Certificate.pm | 39 +++++++++++++++++++++++++++++++--------
 1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/src/PVE/Certificate.pm b/src/PVE/Certificate.pm
index b8415e2..53619a3 100644
--- a/src/PVE/Certificate.pm
+++ b/src/PVE/Certificate.pm
@@ -134,26 +134,49 @@ sub strip_leading_text {
     return $content;
 }
 
+# Splits the pem chain into entries with their leading text
 sub split_pem {
     my ($content, %opts) = @_;
-    my $label = $opts{label} // 'CERTIFICATE';
 
-    my $header = $header_re->($label);
-    return split(/(?=$header)/, $content);
+    my $footer = $footer_re->($opts{label} // 'CERTIFICATE');
+
+    return $content =~ /(.*?$footer)/sg;
 }
 
+# Parses the pem or pem chain for complete validity and returns
+# only the pem/pem chain removing any extra text
 sub check_pem {
     my ($content, %opts) = @_;
 
-    $content = strip_leading_text($content);
+    my $label = $opts{label} // 'CERTIFICATE';
+    my $re = $pem_re->($label);
+
+    my @split =
+        $opts{multiple}
+        ? split_pem($content, label => $label)
+        : ($content);
 
-    my $re = $pem_re->($opts{label} // 'CERTIFICATE');
-    $re = qr/($re\n+)*$re/ if $opts{multiple};
+    if (!@split) {
+        return undef if $opts{noerr};
+        die "PEM chain could not be split into separate entries\n";
+    }
+
+    my $result_pem = "";
+    for my $entry (@split) {
+        my $stripped = strip_leading_text($entry);
+
+        if ($stripped !~ /^$re$/) {
+            $result_pem = "";
+            last;
+        }
+
+        $result_pem .= $stripped;
+    }
 
-    return $content if $content =~ /^$re$/; # OK
+    return $result_pem if $result_pem;
 
     return undef if $opts{noerr};
-    die "not a valid PEM-formatted string.\n";
+    die "not a valid PEM-formatted string\n";
 }
 
 sub pem_to_der {
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH common v4 2/3] pem parser: add testsuite for check_ and split_pem
  2026-08-05 14:11 [PATCH common/proxmox-acme v4 0/3] fix #5978: pem parser: relax parsing of chain entries Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH common v4 1/3] " Thomas Ellmenreich
@ 2026-08-05 14:11 ` Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_pem " Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH proxmox-acme v4 3/3] fix #5978: pem parser: relax parsing of chain entries: Thomas Ellmenreich
  3 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-08-05 14:11 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Ellmenreich

having made considerable changes to both check_pem and split_pem
in the previous commit, this commit introduces tests for both methods.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 test/Makefile          |   2 +
 test/check_pem_test.pl | 365 +++++++++++++++++++++++++++++++++++++++++
 test/split_pem_test.pl | 279 +++++++++++++++++++++++++++++++
 3 files changed, 646 insertions(+)
 create mode 100755 test/check_pem_test.pl
 create mode 100755 test/split_pem_test.pl

diff --git a/test/Makefile b/test/Makefile
index 961a0da..e5a5f42 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -15,6 +15,8 @@ TESTS = lock_file.test			\
 	section_config_property_isolation_test.test \
 	file-test.test \
 	cmd-test.test \
+	check_pem_test.test \
+	split_pem_test.test \
 
 all:
 
diff --git a/test/check_pem_test.pl b/test/check_pem_test.pl
new file mode 100755
index 0000000..b9f11a0
--- /dev/null
+++ b/test/check_pem_test.pl
@@ -0,0 +1,365 @@
+#!/usr/bin/perl
+# Tests the PVE::Certificate::check_pem function for
+# correctness and coverage of edgecases.
+use strict;
+use warnings;
+
+use lib '../src';
+
+use Test::More;
+
+use PVE::Certificate;
+
+# Arrange
+my $setup = [
+    {
+        expected_success => 1,
+        name => "full pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+MIIBsjCCAVugAwIBAgIJAO2g8Z0dXk9tMAoGCCqGSM49BAMCMEUxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UEBwwHQmVya2VsZXkxEDAOBgNVBAoMB1Rl
+c3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBFMQswCQYDVQQG
+EwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB0JlcmtlbGV5MRAwDgYDVQQKDAdU
+ZXN0IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEv5Q8q1p7qZ2gqkQ0Qn5x
+0n9yqv8n8n7n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8aNT
+MFEwHQYDVR0OBBYEFOu2Y0bq8v3z7qkq1m1Qwqkq1m1QMB8GA1UdIwQYMBaAFOu2
+Y0bq8v3z7qkq1m1Qwqkq1m1QMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
+SAAwRQIhANfakefakefakefakefakefakefakefakefake
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 1,
+        name => "simple pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+test
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "spaced out pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+
+test
+
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "only newline pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "many newlines pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+
+
+
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "no content pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE----------END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 0,
+        name => "following content pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+test
+-----END CERTIFICATE-----
+some extended content
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "empty pem",
+        pem => <<'EOF',
+
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "upsidedown pem",
+        pem => <<'EOF',
+-----END CERTIFICATE-----
+test
+-----BEGIN CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 0,
+        name => "multiple as single pem",
+        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-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "big pem different label",
+        multiple => 1,
+        label => "LABEL",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+-----BEGIN LABEL-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+-----BEGIN LABEL-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+-----BEGIN LABEL-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+EOF
+
+        expected_result => <<'EOF',
+-----BEGIN LABEL-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+-----BEGIN LABEL-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+-----BEGIN LABEL-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 0,
+        name => "invalid single 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
+dmFpdXdlYmdthese !?''*:;$%&/  are not allowedWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "invalid multi pem",
+        multiple => 1,
+        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
+dmFpdXdlYmdthese !?''*:;$%&/  are not allowedWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+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_success => 1,
+        name => "single as multiple pem",
+        multiple => 1,
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdtheseZ2RmaXVwYWJ2aXV1YXdlYmxmamlraWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "single as multiple pem",
+        multiple => 1,
+        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
+dmFpdXdlYmdtheseZ2RmaXVwYWJ2aXV1YXdlYmxmamlraWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        expected_result => <<'EOF',
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdtheseZ2RmaXVwYWJ2aXV1YXdlYmxmamlraWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+];
+
+for my $data ($setup->@*) {
+    subtest $data->{name} => sub {
+        # Act
+        my $check_result = PVE::Certificate::check_pem(
+            $data->{pem},
+            noerr => 1,
+            multiple => $data->{multiple},
+            label => $data->{label},
+        );
+
+        # Assert
+        if ($data->{expected_success}) {
+            ok($check_result, "is successful");
+
+            my $expected_result =
+                $data->{expected_result}
+                ? $data->{expected_result}
+                : $data->{pem};
+            is(
+                $check_result, $expected_result, "has correct result content",
+            );
+        } else {
+            is($check_result, undef, "has correctly returned undef");
+            eval {
+                PVE::Certificate::check_pem(
+                    $data->{pem},
+                    multiple => $data->{multiple},
+                    label => $data->{label},
+                );
+            };
+            ok($@, "has correctly thrown an error");
+        }
+    }
+}
+
+done_testing();
diff --git a/test/split_pem_test.pl b/test/split_pem_test.pl
new file mode 100755
index 0000000..4239e5c
--- /dev/null
+++ b/test/split_pem_test.pl
@@ -0,0 +1,279 @@
+#!/usr/bin/perl
+# Tests the PVE::Certificate::split_pem function for
+# correctness and coverage of edgecases.
+use strict;
+use warnings;
+
+use lib '../src';
+
+use Test::More;
+
+use PVE::Certificate;
+
+# Arrange
+my $setup = [
+    {
+        expected_success => 1,
+        name => "single pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+MIIBsjCCAVugAwIBAgIJAO2g8Z0dXk9tMAoGCCqGSM49BAMCMEUxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UEBwwHQmVya2VsZXkxEDAOBgNVBAoMB1Rl
+c3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBFMQswCQYDVQQG
+EwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB0JlcmtlbGV5MRAwDgYDVQQKDAdU
+ZXN0IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEv5Q8q1p7qZ2gqkQ0Qn5x
+0n9yqv8n8n7n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8aNT
+MFEwHQYDVR0OBBYEFOu2Y0bq8v3z7qkq1m1Qwqkq1m1QMB8GA1UdIwQYMBaAFOu2
+Y0bq8v3z7qkq1m1Qwqkq1m1QMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
+SAAwRQIhANfakefakefakefakefakefakefakefakefake
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "no extra text pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+12JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+22JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+        expected_result => [
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+12JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+22JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+    },
+    {
+        expected_success => 1,
+        name => "leading and interleaved text pem",
+        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',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+    },
+    {
+        expected_success => 1,
+        name => "massive glob pem",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "no beginning pem",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+        expected_result => [
+            <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+
+    },
+    {
+        expected_success => 0,
+        name => "not a pem pem",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "following text pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+some more text that we would like to remove
+EOF
+
+        expected_result => [
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+    },
+];
+
+for my $data ($setup->@*) {
+    subtest $data->{name} => sub {
+        # Act
+        my @split_result = PVE::Certificate::split_pem(
+            $data->{pem}, label => $data->{label},
+        );
+
+        # Assert
+        if ($data->{expected_success}) {
+            ok(@split_result, "is successful");
+            my $expected_result =
+                $data->{expected_result}
+                ? $data->{expected_result}
+                : [$data->{pem}];
+
+            is_deeply(
+                \@split_result, \@$expected_result, "has correct split output",
+            );
+        } else {
+            is_deeply(\@split_result, [], "has correctly returned empty");
+        }
+    }
+}
+
+done_testing();
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH common v4 2/3] pem parser: add testsuite for check_pem and split_pem
  2026-08-05 14:11 [PATCH common/proxmox-acme v4 0/3] fix #5978: pem parser: relax parsing of chain entries Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH common v4 1/3] " Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_ and split_pem Thomas Ellmenreich
@ 2026-08-05 14:11 ` Thomas Ellmenreich
  2026-08-05 14:23   ` Thomas Ellmenreich
  2026-08-05 14:11 ` [PATCH proxmox-acme v4 3/3] fix #5978: pem parser: relax parsing of chain entries: Thomas Ellmenreich
  3 siblings, 1 reply; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-08-05 14:11 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Ellmenreich

having made considerable changes to both check_pem and split_pem
in the previous commit, this commit introduces tests for both methods.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 test/Makefile          |   2 +
 test/check_pem_test.pl | 365 +++++++++++++++++++++++++++++++++++++++++
 test/split_pem_test.pl | 279 +++++++++++++++++++++++++++++++
 3 files changed, 646 insertions(+)
 create mode 100755 test/check_pem_test.pl
 create mode 100755 test/split_pem_test.pl

diff --git a/test/Makefile b/test/Makefile
index 961a0da..e5a5f42 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -15,6 +15,8 @@ TESTS = lock_file.test			\
 	section_config_property_isolation_test.test \
 	file-test.test \
 	cmd-test.test \
+	check_pem_test.test \
+	split_pem_test.test \
 
 all:
 
diff --git a/test/check_pem_test.pl b/test/check_pem_test.pl
new file mode 100755
index 0000000..b9f11a0
--- /dev/null
+++ b/test/check_pem_test.pl
@@ -0,0 +1,365 @@
+#!/usr/bin/perl
+# Tests the PVE::Certificate::check_pem function for
+# correctness and coverage of edgecases.
+use strict;
+use warnings;
+
+use lib '../src';
+
+use Test::More;
+
+use PVE::Certificate;
+
+# Arrange
+my $setup = [
+    {
+        expected_success => 1,
+        name => "full pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+MIIBsjCCAVugAwIBAgIJAO2g8Z0dXk9tMAoGCCqGSM49BAMCMEUxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UEBwwHQmVya2VsZXkxEDAOBgNVBAoMB1Rl
+c3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBFMQswCQYDVQQG
+EwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB0JlcmtlbGV5MRAwDgYDVQQKDAdU
+ZXN0IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEv5Q8q1p7qZ2gqkQ0Qn5x
+0n9yqv8n8n7n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8aNT
+MFEwHQYDVR0OBBYEFOu2Y0bq8v3z7qkq1m1Qwqkq1m1QMB8GA1UdIwQYMBaAFOu2
+Y0bq8v3z7qkq1m1Qwqkq1m1QMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
+SAAwRQIhANfakefakefakefakefakefakefakefakefake
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 1,
+        name => "simple pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+test
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "spaced out pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+
+test
+
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "only newline pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "many newlines pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+
+
+
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "no content pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE----------END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 0,
+        name => "following content pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+test
+-----END CERTIFICATE-----
+some extended content
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "empty pem",
+        pem => <<'EOF',
+
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "upsidedown pem",
+        pem => <<'EOF',
+-----END CERTIFICATE-----
+test
+-----BEGIN CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 0,
+        name => "multiple as single pem",
+        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-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "big pem different label",
+        multiple => 1,
+        label => "LABEL",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+-----BEGIN LABEL-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+-----BEGIN LABEL-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+-----BEGIN LABEL-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+EOF
+
+        expected_result => <<'EOF',
+-----BEGIN LABEL-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+-----BEGIN LABEL-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+-----BEGIN LABEL-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END LABEL-----
+EOF
+    },
+    {
+        expected_success => 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
+    },
+    {
+        expected_success => 0,
+        name => "invalid single 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
+dmFpdXdlYmdthese !?''*:;$%&/  are not allowedWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 0,
+        name => "invalid multi pem",
+        multiple => 1,
+        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
+dmFpdXdlYmdthese !?''*:;$%&/  are not allowedWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+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_success => 1,
+        name => "single as multiple pem",
+        multiple => 1,
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdtheseZ2RmaXVwYWJ2aXV1YXdlYmxmamlraWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "single as multiple pem",
+        multiple => 1,
+        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
+dmFpdXdlYmdtheseZ2RmaXVwYWJ2aXV1YXdlYmxmamlraWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        expected_result => <<'EOF',
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdtheseZ2RmaXVwYWJ2aXV1YXdlYmxmamlraWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+];
+
+for my $data ($setup->@*) {
+    subtest $data->{name} => sub {
+        # Act
+        my $check_result = PVE::Certificate::check_pem(
+            $data->{pem},
+            noerr => 1,
+            multiple => $data->{multiple},
+            label => $data->{label},
+        );
+
+        # Assert
+        if ($data->{expected_success}) {
+            ok($check_result, "is successful");
+
+            my $expected_result =
+                $data->{expected_result}
+                ? $data->{expected_result}
+                : $data->{pem};
+            is(
+                $check_result, $expected_result, "has correct result content",
+            );
+        } else {
+            is($check_result, undef, "has correctly returned undef");
+            eval {
+                PVE::Certificate::check_pem(
+                    $data->{pem},
+                    multiple => $data->{multiple},
+                    label => $data->{label},
+                );
+            };
+            ok($@, "has correctly thrown an error");
+        }
+    }
+}
+
+done_testing();
diff --git a/test/split_pem_test.pl b/test/split_pem_test.pl
new file mode 100755
index 0000000..4239e5c
--- /dev/null
+++ b/test/split_pem_test.pl
@@ -0,0 +1,279 @@
+#!/usr/bin/perl
+# Tests the PVE::Certificate::split_pem function for
+# correctness and coverage of edgecases.
+use strict;
+use warnings;
+
+use lib '../src';
+
+use Test::More;
+
+use PVE::Certificate;
+
+# Arrange
+my $setup = [
+    {
+        expected_success => 1,
+        name => "single pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+MIIBsjCCAVugAwIBAgIJAO2g8Z0dXk9tMAoGCCqGSM49BAMCMEUxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIDAJDQTEQMA4GA1UEBwwHQmVya2VsZXkxEDAOBgNVBAoMB1Rl
+c3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzAwMTAxMDAwMDAwWjBFMQswCQYDVQQG
+EwJVUzELMAkGA1UECAwCQ0ExEDAOBgNVBAcMB0JlcmtlbGV5MRAwDgYDVQQKDAdU
+ZXN0IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEv5Q8q1p7qZ2gqkQ0Qn5x
+0n9yqv8n8n7n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8n8aNT
+MFEwHQYDVR0OBBYEFOu2Y0bq8v3z7qkq1m1Qwqkq1m1QMB8GA1UdIwQYMBaAFOu2
+Y0bq8v3z7qkq1m1Qwqkq1m1QMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
+SAAwRQIhANfakefakefakefakefakefakefakefakefake
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "no extra text pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+12JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+22JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+        expected_result => [
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+12JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+22JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+    },
+    {
+        expected_success => 1,
+        name => "leading and interleaved text pem",
+        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',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+    },
+    {
+        expected_success => 1,
+        name => "massive glob pem",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+-----BEGIN CERTIFICATE-----
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+-----BEGIN CERTIFICATE-----
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "no beginning pem",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+        expected_result => [
+            <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+2WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+Subject: CN=Org Root CA
+Issuer: CN=Org Root CA
+3WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+
+    },
+    {
+        expected_success => 0,
+        name => "not a pem pem",
+        pem => <<'EOF',
+Subject: CN=some.test.content
+Issuer: CN=Org Issuing CA 3
+1WtzZGhmbGtqYXNoZ2RmaXVwYWJ2aXV1YXdlYmxmamlraGFzZGtmYWlzdXBkdW5i
+dmFpdXdlYmdmw7ZrYWpkc2JoZmdpYXNkbmJpZnVhYmdzaWprZmdyYXNkaHJmaW9h
+dXNkbmF1c2JmZ3ZramFzZGJma2phc2RibmZpanVhIHNkb3ZuYmF3b3VlYmZnb2F3
+c2JlZ2tseTxzZWl2IGF3ZWlidm4=
+Subject: CN=Org Issuing CA 3
+Issuer: CN=Org Root CA
+EOF
+    },
+    {
+        expected_success => 1,
+        name => "following text pem",
+        pem => <<'EOF',
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+some more text that we would like to remove
+EOF
+
+        expected_result => [
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+32JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+
+            <<'EOF',
+-----BEGIN CERTIFICATE-----
+42JlZ2tseTxzZWl2IGF3ZWlidm4=
+-----END CERTIFICATE-----
+EOF
+        ],
+    },
+];
+
+for my $data ($setup->@*) {
+    subtest $data->{name} => sub {
+        # Act
+        my @split_result = PVE::Certificate::split_pem(
+            $data->{pem}, label => $data->{label},
+        );
+
+        # Assert
+        if ($data->{expected_success}) {
+            ok(@split_result, "is successful");
+            my $expected_result =
+                $data->{expected_result}
+                ? $data->{expected_result}
+                : [$data->{pem}];
+
+            is_deeply(
+                \@split_result, \@$expected_result, "has correct split output",
+            );
+        } else {
+            is_deeply(\@split_result, [], "has correctly returned empty");
+        }
+    }
+}
+
+done_testing();
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH proxmox-acme v4 3/3] fix #5978: pem parser: relax parsing of chain entries:
  2026-08-05 14:11 [PATCH common/proxmox-acme v4 0/3] fix #5978: pem parser: relax parsing of chain entries Thomas Ellmenreich
                   ` (2 preceding siblings ...)
  2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_pem " Thomas Ellmenreich
@ 2026-08-05 14:11 ` Thomas Ellmenreich
  3 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-08-05 14:11 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Ellmenreich

Instead of using a custom regex to parse pem chains, now uses
the pve-common Certificate::check_pem function to do so. This
now allows for additional text and whitespace inbetween the
chain entries.

Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
---
 src/PVE/ACME.pm | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/PVE/ACME.pm b/src/PVE/ACME.pm
index e6fb9c2..665087b 100644
--- a/src/PVE/ACME.pm
+++ b/src/PVE/ACME.pm
@@ -526,14 +526,16 @@ sub get_certificate {
                     }
                 }
             }
-            die "no matching alternate chain for '$root' returned by server\n"
+            die "no matching alternate chain for '$root' provided by the acme server\n"
                 if !defined($res);
         }
 
-        if ($res =~ /^(-----BEGIN CERTIFICATE-----)(.+)(-----END CERTIFICATE-----)$/s) { # untaint
-            return $1 . $2 . $3;
-        }
-        die "Server reply does not look like a PEM encoded certificate\n";
+        my $parsed = eval { PVE::Certificate::check_pem($res, multiple => 1) };
+        chomp(my $error = $@);
+        die
+            "value returned by ACME certificate url does not look like a PEM encoded certificate: " . $error . "\n"
+            if $error;
+        return $parsed;
     };
     $self->fatal("POST of '$order->{certificate}' failed - $@", $r) if $@;
     return $return;
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH common v4 2/3] pem parser: add testsuite for check_pem and split_pem
  2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_pem " Thomas Ellmenreich
@ 2026-08-05 14:23   ` Thomas Ellmenreich
  0 siblings, 0 replies; 6+ messages in thread
From: Thomas Ellmenreich @ 2026-08-05 14:23 UTC (permalink / raw)
  To: Thomas Ellmenreich, pve-devel

Please ignore this one, its a duplicate of the other [PATCH common v4 2/3]:

https://lore.proxmox.com/pve-devel/20260805141114.190477-3-t.ellmenreich@proxmox.com/

On Wed Aug 5, 2026 at 4:11 PM CEST, Thomas Ellmenreich wrote:
> having made considerable changes to both check_pem and split_pem
> in the previous commit, this commit introduces tests for both methods.
>
> Signed-off-by: Thomas Ellmenreich <t.ellmenreich@proxmox.com>
> ---
>  test/Makefile          |   2 +
>  test/check_pem_test.pl | 365 +++++++++++++++++++++++++++++++++++++++++
>  test/split_pem_test.pl | 279 +++++++++++++++++++++++++++++++
>  3 files changed, 646 insertions(+)
>  create mode 100755 test/check_pem_test.pl
>  create mode 100755 test/split_pem_test.pl
>
> diff --git a/test/Makefile b/test/Makefile
> index 961a0da..e5a5f42 100644




^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-08-05 14:23 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-05 14:11 [PATCH common/proxmox-acme v4 0/3] fix #5978: pem parser: relax parsing of chain entries Thomas Ellmenreich
2026-08-05 14:11 ` [PATCH common v4 1/3] " Thomas Ellmenreich
2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_ and split_pem Thomas Ellmenreich
2026-08-05 14:11 ` [PATCH common v4 2/3] pem parser: add testsuite for check_pem " Thomas Ellmenreich
2026-08-05 14:23   ` Thomas Ellmenreich
2026-08-05 14:11 ` [PATCH proxmox-acme v4 3/3] fix #5978: pem parser: relax parsing of chain entries: Thomas Ellmenreich

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal