Repository: certbot/certbot

Issue:
Certbot fails to retrieve domains of previous certificate when SAN is a critical extension
## My operating system is (include version):
CentOS 6 and CentOS 7

## I installed Certbot with (certbot-auto, OS package manager, pip, etc):
For CentOS6 (certbot-auto):  https://certbot.eff.org/all-instructions/#centos-6-none-of-the-above
For CentOS7 (yum):  https://certbot.eff.org/all-instructions/#centos-rhel-7-none-of-the-above

Currently using certbot 0.14.1 on CentOS 7 and certbot-auto 0.16.0 on CentOS 6.

## I ran this command and it produced this output:
certbot certonly --manual--cert-name cubs --domains chicago-cubs.venafi.example,cubs.venafi.example

_(note: non-Let's Encrypt ACME server specified in /etc/letsencrypt/cli.ini)_

```
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Starting new HTTPS connection (1): tpp.venafi.example

-------------------------------------------------------------------------------
Confirm that you intend to update certificate cubs to include domains
[u'chicago-cubs.venafi.example', u'cubs.venafi.example']. Note that it
previously contained domains [].
-------------------------------------------------------------------------------
```

## Certbot's behavior differed from what I expected because:
It appears that certbot is unable to parse domains from the previous certificate when it has no CN and the DNS SANs are a critical extension.  Symptom is the unexpected prompt shown above (applicable [previous_cert_pem.txt](https://github.com/certbot/certbot/files/1137352/previous_cert_pem.txt)).


## Background
Venafi has introduced an ACME server interface in Trust Protection Platform 17.2 and the common application involves using certbot to obtain certificates from an enterprise PKI.  Microsoft's Active Directory Certifiate Services (ADCS) is a very popular enterprise PKI and when it enrolls a certificate request that lacks a Subject DN (has only SANs) the subjectAltName (2.5.29.17) extension is always critical.  This behavior is consistent with [Section 4.2.1.6 of RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2.1.6) which states "If the subject field contains an empty sequence, then the issuing CA MUST include a subjectAltName extension that is marked as critical."



Discussion:
Maybe @bmw or @jsha have opinions where we put names into certs
To me it seems possible that you're using the `--cert-name` command to name a cert rather than to find a cert to expand. Did you previously have a cert with that name? It might just be that the way this is described to you is confusing
> It appears that certbot is unable to parse domains from the previous certificate when it has no CN and the DNS SANs are a critical extension.

This seems possible. We wouldn't have run into this in testing against Boulder, because Boulder always includes a CN, and never marks SANs as critical. I am glad you are testing this scenario!

The next step for the Certbot team should be to find the code the extracts names from existing certificates, and test it against previous_cert_pem.txt to make sure it extracts the expected names, ideally in a unittest. If that test fails, we'll have to fix the extraction code (we may have to double check the Python `cryptography` library is doing the right thing).
@bmw or @ynasser would you be interested in writing that test?
@SwartzCr Yes, I previously had a certificate with the same `--cert-name`.  It was running the same certbot command line a second time that initially revealed the problem and this, unfortunately, also prevents the certbot renew functionality from working.  

Thank you all for investigating the problem.
This is almost certainly a problem with [this function](https://github.com/certbot/certbot/blob/v0.16.0/acme/acme/crypto_util.py#L188) where we parse the dumped certificate text with a regex to support older versions of our crypto library used on some of the systems where we are packaged.

Resolving patch:
diff --git a/acme/acme/crypto_util.py b/acme/acme/crypto_util.py
--- a/acme/acme/crypto_util.py
+++ b/acme/acme/crypto_util.py
@@ -218,7 +218,7 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
     text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
     # WARNING: this function does not support multiple SANs extensions.
     # Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
-    match = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text)
+    match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text)
     # WARNING: this function assumes that no SAN can include
     # parts_separator, hence the split!
     sans_parts = [] if match is None else match.group(1).split(parts_separator)


Test patch:
diff --git a/acme/acme/crypto_util_test.py b/acme/acme/crypto_util_test.py
--- a/acme/acme/crypto_util_test.py
+++ b/acme/acme/crypto_util_test.py
@@ -131,6 +131,11 @@ def test_csr_idn_sans(self):
         self.assertEqual(self._call_csr('csr-idnsans.pem'),
                          self._get_idn_names())
 
+    def test_critical_san(self):
+        self.assertEqual(self._call_cert('critical-san.pem'),
+                         ['chicago-cubs.venafi.example', 'cubs.venafi.example'])
+
+
 
 class RandomSnTest(unittest.TestCase):
     """Test for random certificate serial numbers."""