Repository: pyca/cryptography

Issue:
cryptography.x509 encodes countryName as a Utf8String but it should be a PrintableString
[RFC 5280](https://www.ietf.org/rfc/rfc5280.txt) says that countryName should be as above, and unlike the other elements there (localeName etc.) cannot be a DirectoryString.

The consequences are that while it seems like `openssl s_server` is OK with certificates generated in this way, golang's (seemingly correct implementation which follows the RFC) cannot parse the certificate.

@reaperhulk says https://github.com/pyca/cryptography/blob/master/src/cryptography/hazmat/backends/openssl/backend.py#L114 is what should be changed.



Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -63,6 +63,7 @@
     "pretend",
     "iso8601",
     "hypothesis",
+    "pyasn1_modules",
 ]
 
 # If there's no vectors locally that probably means we are in a tarball and
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -52,7 +52,7 @@
 from cryptography.hazmat.primitives.ciphers.modes import (
     CBC, CFB, CFB8, CTR, ECB, GCM, OFB
 )
-from cryptography.x509.oid import ExtensionOID
+from cryptography.x509.oid import ExtensionOID, NameOID
 
 
 _MemoryBIO = collections.namedtuple("_MemoryBIO", ["bio", "char_ptr"])
@@ -119,12 +119,14 @@ def _encode_name(backend, attributes):
     for attribute in attributes:
         value = attribute.value.encode('utf8')
         obj = _txt2obj_gc(backend, attribute.oid.dotted_string)
+        if attribute.oid == NameOID.COUNTRY_NAME:
+            # Per RFC5280 Appendix A.1 countryName should be encoded as
+            # PrintableString, not UTF8String
+            type = backend._lib.MBSTRING_ASC
+        else:
+            type = backend._lib.MBSTRING_UTF8
         res = backend._lib.X509_NAME_add_entry_by_OBJ(
-            subject,
-            obj,
-            backend._lib.MBSTRING_UTF8,
-            value,
-            -1, -1, 0,
+            subject, obj, type, value, -1, -1, 0,
         )
         backend.openssl_assert(res == 1)
     return subject


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -9,6 +9,10 @@
 import ipaddress
 import os
 
+from pyasn1.codec.der import decoder
+
+from pyasn1_modules import rfc2459
+
 import pytest
 
 import six
@@ -834,6 +838,43 @@ def test_build_cert(self, backend):
             x509.DNSName(u"cryptography.io"),
         ]
 
+    def test_build_cert_printable_string_country_name(self, backend):
+        issuer_private_key = RSA_KEY_2048.private_key(backend)
+        subject_private_key = RSA_KEY_2048.private_key(backend)
+
+        not_valid_before = datetime.datetime(2002, 1, 1, 12, 1)
+        not_valid_after = datetime.datetime(2030, 12, 31, 8, 30)
+
+        builder = x509.CertificateBuilder().serial_number(
+            777
+        ).issuer_name(x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
+            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'Texas'),
+        ])).subject_name(x509.Name([
+            x509.NameAttribute(NameOID.COUNTRY_NAME, u'US'),
+            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u'Texas'),
+        ])).public_key(
+            subject_private_key.public_key()
+        ).not_valid_before(
+            not_valid_before
+        ).not_valid_after(
+            not_valid_after
+        )
+
+        cert = builder.sign(issuer_private_key, hashes.SHA256(), backend)
+
+        parsed, _ = decoder.decode(
+            cert.public_bytes(serialization.Encoding.DER),
+            asn1Spec=rfc2459.Certificate()
+        )
+        tbs_cert = parsed.getComponentByName('tbsCertificate')
+        subject = tbs_cert.getComponentByName('subject')
+        issuer = tbs_cert.getComponentByName('issuer')
+        # \x13 is printable string. The first byte of the value of the
+        # node corresponds to the ASN.1 string type.
+        assert subject[0][0][0][1][0] == b"\x13"[0]
+        assert issuer[0][0][0][1][0] == b"\x13"[0]
+
 
 class TestCertificateBuilder(object):
     @pytest.mark.requires_backend_interface(interface=RSABackend)