Repository: certbot/certbot

Issue:
RFE: certbot certificates ought to show the serial numbers of the certificates
For situations like https://letsencrypt.org/caaproblem/ it would be great to have a way of seeing the serial numbers of all the current certificates.  `certbot certificates` can show me the filenames, but then I have to study the openssl(1) manual to figure out how to craft the `openssl x509 -text -noout </path/to/fullchain.pem | grep -A1 Serial` command to see the serial number of each.



Discussion:
Sounds like a reasonable addition.

Resolving patch:
diff --git a/certbot/certbot/_internal/cert_manager.py b/certbot/certbot/_internal/cert_manager.py
--- a/certbot/certbot/_internal/cert_manager.py
+++ b/certbot/certbot/_internal/cert_manager.py
@@ -276,12 +276,15 @@ def human_readable_cert_info(config, cert, skip_filter_checks=False):
             status = "VALID: {0} days".format(diff.days)
 
     valid_string = "{0} ({1})".format(cert.target_expiry, status)
+    serial = format(crypto_util.get_serial_from_cert(cert.cert_path), 'x')
     certinfo.append("  Certificate Name: {0}\n"
-                    "    Domains: {1}\n"
-                    "    Expiry Date: {2}\n"
-                    "    Certificate Path: {3}\n"
-                    "    Private Key Path: {4}".format(
+                    "    Serial Number: {1}\n"
+                    "    Domains: {2}\n"
+                    "    Expiry Date: {3}\n"
+                    "    Certificate Path: {4}\n"
+                    "    Private Key Path: {5}".format(
                          cert.lineagename,
+                         serial,
                          " ".join(cert.names()),
                          valid_string,
                          cert.fullchain,
diff --git a/certbot/certbot/crypto_util.py b/certbot/certbot/crypto_util.py
--- a/certbot/certbot/crypto_util.py
+++ b/certbot/certbot/crypto_util.py
@@ -491,3 +491,17 @@ def cert_and_chain_from_fullchain(fullchain_pem):
         crypto.load_certificate(crypto.FILETYPE_PEM, fullchain_pem)).decode()
     chain = fullchain_pem[len(cert):].lstrip()
     return (cert, chain)
+
+def get_serial_from_cert(cert_path):
+    """Retrieve the serial number of a certificate from certificate path
+
+    :param str cert_path: path to a cert in PEM format
+
+    :returns: serial number of the certificate
+    :rtype: int
+    """
+    # pylint: disable=redefined-outer-name
+    with open(cert_path) as f:
+        x509 = crypto.load_certificate(crypto.FILETYPE_PEM,
+                                                           f.read())
+    return x509.get_serial_number()


Test patch:
diff --git a/certbot/tests/cert_manager_test.py b/certbot/tests/cert_manager_test.py
--- a/certbot/tests/cert_manager_test.py
+++ b/certbot/tests/cert_manager_test.py
@@ -200,9 +200,11 @@ def test_certificates_no_files(self, mock_utility, mock_logger):
         self.assertTrue(mock_utility.called)
         shutil.rmtree(empty_tempdir)
 
+    @mock.patch('certbot.crypto_util.get_serial_from_cert')
     @mock.patch('certbot._internal.cert_manager.ocsp.RevocationChecker.ocsp_revoked')
-    def test_report_human_readable(self, mock_revoked):
+    def test_report_human_readable(self, mock_revoked, mock_serial):
         mock_revoked.return_value = None
+        mock_serial.return_value = 1234567890
         from certbot._internal import cert_manager
         import datetime
         import pytz