Repository: certbot/certbot

Issue:
Get common name from CSR in new_order in ClientV2
https://github.com/certbot/certbot/pull/5578#issue-169507815.

We have code in `certbot.crypto_util` that does this with the help of some ACME code. The code in `new_order` for extracting SANs is nicer than the code in `acme.crypto_util._pyopenssl_cert_or_req_san`. What I'd do is make (a) function(s) in `acme.crypto_util` that takes a string CSR and uses the SANs extracting code from `new_order` with the common name extracting code from `certbot.crypto_util` and returns the list of domains. If we're currently removing duplicates, we should do this here as well.


Discussion:
If we're generating the CSR ourselves, we don't actually need to get the common name because we can stick what we need in the SANs. Then we could keep all that extra OpenSSL nastiness out of `acme.crypto_util`.
~Also I think our code there might be wrong... shouldn't it be ignoring the common name if there are SANs?~ I guess we support old code.
> If we're generating the CSR ourselves, we don't actually need to get the common name because we can stick what we need in the SANs. Then we could keep all that extra OpenSSL nastiness out of acme.crypto_util.

We can extract the common name with `cryptography` too if we want. https://cryptography.io/en/latest/x509/reference/#cryptography.x509.CertificateSigningRequest.subject. We should try not to spend too much time refactoring code we can change later though.

Resolving patch:
diff --git a/acme/acme/client.py b/acme/acme/client.py
--- a/acme/acme/client.py
+++ b/acme/acme/client.py
@@ -1,7 +1,6 @@
 """ACME client API."""
 import base64
 import collections
-import cryptography
 import datetime
 from email.utils import parsedate_tz
 import heapq
@@ -17,6 +16,7 @@
 import requests
 import sys
 
+from acme import crypto_util
 from acme import errors
 from acme import jws
 from acme import messages
@@ -568,11 +568,9 @@ def new_order(self, csr_pem):
         :returns: The newly created order.
         :rtype: OrderResource
         """
-        csr = cryptography.x509.load_pem_x509_csr(csr_pem,
-            cryptography.hazmat.backends.default_backend())
-        san_extension = next(ext for ext in csr.extensions
-            if ext.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
-        dnsNames = san_extension.value.get_values_for_type(cryptography.x509.DNSName)
+        csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
+        # pylint: disable=protected-access
+        dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr)
 
         identifiers = []
         for name in dnsNames:
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
@@ -186,6 +186,15 @@ def make_csr(private_key_pem, domains, must_staple=False):
     return OpenSSL.crypto.dump_certificate_request(
         OpenSSL.crypto.FILETYPE_PEM, csr)
 
+def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req):
+    common_name = loaded_cert_or_req.get_subject().CN
+    sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req)
+
+    if common_name is None:
+        return sans
+    else:
+        return [common_name] + [d for d in sans if d != common_name]
+
 def _pyopenssl_cert_or_req_san(cert_or_req):
     """Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
 
diff --git a/certbot/crypto_util.py b/certbot/crypto_util.py
--- a/certbot/crypto_util.py
+++ b/certbot/crypto_util.py
@@ -340,14 +340,8 @@ def _get_names_from_cert_or_req(cert_or_req, load_func, typ):
 
 
 def _get_names_from_loaded_cert_or_req(loaded_cert_or_req):
-    common_name = loaded_cert_or_req.get_subject().CN
     # pylint: disable=protected-access
-    sans = acme_crypto_util._pyopenssl_cert_or_req_san(loaded_cert_or_req)
-
-    if common_name is None:
-        return sans
-    else:
-        return [common_name] + [d for d in sans if d != common_name]
+    return acme_crypto_util._pyopenssl_cert_or_req_all_names(loaded_cert_or_req)
 
 
 def get_names_from_cert(csr, typ=OpenSSL.crypto.FILETYPE_PEM):


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
@@ -65,6 +65,30 @@ def test_probe_not_recognized_name(self):
     #    self.assertRaises(errors.Error, self._probe, b'bar')
 
 
+class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase):
+    """Test for acme.crypto_util._pyopenssl_cert_or_req_all_names."""
+
+    @classmethod
+    def _call(cls, loader, name):
+        # pylint: disable=protected-access
+        from acme.crypto_util import _pyopenssl_cert_or_req_all_names
+        return _pyopenssl_cert_or_req_all_names(loader(name))
+
+    def _call_cert(self, name):
+        return self._call(test_util.load_cert, name)
+
+    def test_cert_one_san_no_common(self):
+        self.assertEqual(self._call_cert('cert-nocn.der'),
+                         ['no-common-name.badssl.com'])
+
+    def test_cert_no_sans_yes_common(self):
+        self.assertEqual(self._call_cert('cert.pem'), ['example.com'])
+
+    def test_cert_two_sans_yes_common(self):
+        self.assertEqual(self._call_cert('cert-san.pem'),
+                         ['example.com', 'www.example.com'])
+
+
 class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
     """Test for acme.crypto_util._pyopenssl_cert_or_req_san."""