Commit 679a10110e for openssl.org

commit 679a10110e4e60dbfe8acc87f5c697cebd501876
Author: snowdroppe <stefanrieche@gmail.com>
Date:   Sat Nov 15 19:58:46 2025 +0000

    fix(x509.c): Fixed regression of openssl x509 -checkend return values

    Fixes #28928

    Also adds functionality to -checkend to account for -multi behaviour.
    Man page and unit tests updated accordingly.

    Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
    Reviewed-by: Tomas Mraz <tomas@openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/29155)

diff --git a/apps/x509.c b/apps/x509.c
index c9d26f8b20..d8c8dc9ae6 100644
--- a/apps/x509.c
+++ b/apps/x509.c
@@ -1098,13 +1098,22 @@ int x509_main(int argc, char **argv)

     if (checkend) {
         time_t tcheck = time(NULL) + checkoffset;
+        int expired = X509_cmp_time(X509_get0_notAfter(x), &tcheck) < 0;

-        ret = X509_cmp_time(X509_get0_notAfter(x), &tcheck) < 0;
-        if (ret)
+        if (expired)
             BIO_printf(out, "Certificate will expire\n");
         else
             BIO_printf(out, "Certificate will not expire\n");
-        goto end_cert_loop;
+
+        if (multi && k > 0)
+            ret |= expired;
+        else
+            ret = expired;
+
+        if (multi && k < sk_X509_num(certs) - 1)
+            goto end_cert_loop;
+        else
+            goto end;
     }

     if (!check_cert_attributes(out, x, checkhost, checkemail, checkip, 1))
diff --git a/doc/man1/openssl-x509.pod.in b/doc/man1/openssl-x509.pod.in
index fbe42b2034..835a55eddf 100644
--- a/doc/man1/openssl-x509.pod.in
+++ b/doc/man1/openssl-x509.pod.in
@@ -352,8 +352,12 @@ contained in the input.

 =item B<-checkend> I<arg>

-Checks if the certificate expires within the next I<arg> seconds and exits
-nonzero if yes it will expire or zero if not.
+Without B<-multi> checks if the certificate expires within the next
+I<arg> seconds and exits nonzero if it will expire or zero if not.
+
+With B<-multi> checks if any certificate in the input will expire
+within the next I<arg> seconds and exits nonzero if any will expire
+or zero if none will.

 =item B<-checkhost> I<host>

@@ -792,6 +796,12 @@ Set a certificate to be trusted for SSL client use and change set its alias to
  openssl x509 -in cert.pem -addtrust clientAuth \
         -setalias "Steve's Class 1 CA" -out trust.pem

+Check if any certificates in a chain are due to expire within the next 30 days
+(returns zero if none will expire, nonzero if any will expire):
+
+ openssl x509 -in chain.pem -multi -checkend $[3600*24*30] \
+        && echo 'perform renewal' || echo 'renewal unnecessary'
+
 =head1 NOTES

 The conversion to UTF8 format used with the name options assumes that
diff --git a/test/recipes/25-test_x509.t b/test/recipes/25-test_x509.t
index 1b343392aa..665ea164c6 100644
--- a/test/recipes/25-test_x509.t
+++ b/test/recipes/25-test_x509.t
@@ -17,7 +17,7 @@ use File::Compare qw/compare_text/;

 setup("test_x509");

-plan tests => 140;
+plan tests => 150;

 # Prevent MSys2 filename munging for arguments that look like file paths but
 # aren't
@@ -630,3 +630,75 @@ SKIP: {

     ok(run(test(["x509_test", $psscert])), "running x509_test");
 }
+
+# Tests for -checkend including -multi
+# Discussed in https://github.com/openssl/openssl/pull/29155
+
+my $c_early = "c-early.pem";
+my $c_late = "c-late.pem";
+my $c_chain = "c-chain.pem";
+my $c_key = srctop_file(@certs, 'ca-key.pem');
+ok(run(app(["openssl", "x509", "-new", "-key", $c_key, "-subj", "/CN=EARLY",
+            "-extfile", $extfile, "-days", "100", "-text", "-out", $c_early]))
+&& run(app(["openssl", "x509", "-new", "-key", $c_key, "-subj", "/CN=LATE",
+            "-extfile", $extfile, "-days", "200", "-text", "-out", $c_late])));
+my $c_time = Time::Piece->gmtime->epoch;
+my $delta_early = Time::Piece->strptime(
+                    get_field($c_early, "Not After "),
+                       "%b %d %T %Y %Z")->epoch - $c_time;
+my $delta_late = Time::Piece->strptime(
+                    get_field($c_late, "Not After "),
+                        "%b %d %T %Y %Z")->epoch - $c_time;
+sub mkchain {
+    open(my $out, ">:raw", $c_chain) or die;
+    foreach my $fn (@_) {
+        open(my $in, "<:raw", $fn) or die;
+        print {$out} <$in>;
+        close($in);
+    }
+    close($out);
+    return 0;
+}
+# Single + not expiring
+ok(run(app(["openssl", "x509", "-checkend", $delta_early - 3600,
+            "-in", $c_early])),
+    "Single cert + not expiring in -checkend window");
+# Single + expiring
+ok(!run(app(["openssl", "x509", "-checkend", $delta_early + 3600,
+            "-in", $c_early])),
+    "Single cert + expiring in -checkend window");
+# Single + expiring at boundary
+# Test may fail erroneously due to sequential now() calls
+# See https://github.com/openssl/openssl/pull/29155
+my $delta_exact = Time::Piece->strptime( get_field($c_early, "Not After "),
+                    "%b %d %T %Y %Z")->epoch - Time::Piece->gmtime->epoch;
+ok(!run(app(["openssl", "x509", "-checkend", $delta_exact, "-in", $c_early])),
+    "Single cert + expiring at -checkend boundary");
+# Multi + none expiring
+mkchain($c_early, $c_late, $c_late);
+ok(run(app(["openssl", "x509", "-multi", "-checkend",
+            $delta_early - 3600, "-in", $c_chain])),
+    "Multi cert + none expiring in -checkend window");
+# Multi + 1st expiring
+mkchain($c_early, $c_late, $c_late);
+ok(!run(app(["openssl", "x509", "-multi", "-checkend",
+             $delta_early + 3600, "-in", $c_chain])),
+    "Multi cert + 1st expiring in -checkend window");
+# Multi + 2nd expiring
+mkchain($c_late, $c_early, $c_late);
+ok(!run(app(["openssl", "x509", "-multi", "-checkend",
+             $delta_early + 3600, "-in", $c_chain])),
+    "Multi cert + 2nd expiring in -checkend window");
+# Multi + 3rd expiring
+mkchain($c_late, $c_late, $c_early);
+ok(!run(app(["openssl", "x509", "-multi", "-checkend",
+             $delta_late - 3600, "-in", $c_chain])),
+    "Multi cert + 3rd expiring in -checkend window");
+# Multi + all expiring
+mkchain($c_early, $c_late, $c_early);
+ok(!run(app(["openssl", "x509", "-multi", "-checkend",
+             $delta_late + 3600, "-in", $c_chain])),
+    "Multi cert + all expiring in -checkend window");
+# Bad parse still returns non-zero
+ok(!run(app(["openssl", "x509", "-checkend", "60", "-in", $c_key])),
+    "Bad parse with -checkend returns non-zero");