Repository: pyca/cryptography

Issue:
x509.CertifcateBuilder().sign() fails with "Unknown OpenSSL error' when subject has bad country code
The x509.CertifcateBuilder().sign() function fails with an unknown OpenSSL error when the builder is provided with an invalid country code:

```
Traceback (most recent call last):
  File "./demo.py", line 30, in <module>
    ca_crt = builder.sign(private_key=ca_key, algorithm=hashes.SHA256(), backend=default_backend())
  File "/usr/local/lib/python3.4/dist-packages/cryptography/x509/base.py", line 520, in sign
    return backend.create_x509_certificate(self, private_key, algorithm)
  File "/usr/local/lib/python3.4/dist-packages/cryptography/hazmat/backends/multibackend.py", line 381, in create_x509_certificate
    return b.create_x509_certificate(builder, private_key, algorithm)
  File "/usr/local/lib/python3.4/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 1402, in create_x509_certificate
    x509_cert, _encode_name_gc(self, list(builder._subject_name))
  File "/usr/local/lib/python3.4/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 158, in _encode_name_gc
    subject = _encode_name(backend, attributes)
  File "/usr/local/lib/python3.4/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 153, in _encode_name
    backend.openssl_assert(res == 1)
  File "/usr/local/lib/python3.4/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 719, in openssl_assert
    return binding._openssl_assert(self._lib, ok)
  File "/usr/local/lib/python3.4/dist-packages/cryptography/hazmat/bindings/openssl/binding.py", line 43, in _openssl_assert
    errors
cryptography.exceptions.InternalError: Unknown OpenSSL error. Please file an issue at https://github.com/pyca/cryptography/issues with information on how to reproduce this. ([_OpenSSLError(code=218603671, lib=13, func=122, reason=151)])
```

The demo code below reproduces this error. Tested on Ubuntu 14.04.3 x64 w/ Python 3.4.3, cryptography 1.2.1, and libssl-dev 1.0.1f-1ubuntu2.16.

```
#!/usr/bin/env python3

import datetime

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa

DUR_ONE_DAY = datetime.timedelta(1, 0, 0)
DUR_ONE_YEAR = datetime.timedelta(366, 0, 0)

ca_key = rsa.generate_private_key(65537, 4096, default_backend())

sub_attr = [x509.NameAttribute(x509.NameOID.COUNTRY_NAME, "InvalidCC")]

builder = x509.CertificateBuilder()
builder = builder.issuer_name(x509.Name(sub_attr))
builder = builder.subject_name(x509.Name(sub_attr))
builder = builder.not_valid_before(datetime.datetime.today() - DUR_ONE_DAY)
builder = builder.not_valid_after(datetime.datetime.today() + DUR_ONE_YEAR)
builder = builder.serial_number(3)
builder = builder.public_key(ca_key.public_key())

extensions = []
extensions.append(x509.BasicConstraints(ca=True, path_length=1))
for ext in extensions:
    builder = builder.add_extension(ext, critical=True)

ca_crt = builder.sign(private_key=ca_key, algorithm=hashes.SHA256(), backend=default_backend())

# builder.sign() will fail with when CONTRY_NAME is invalid country code:
#    cryptography.exceptions.InternalError: Unknown
#    OpenSSL error. Please file an issue at
#    https://github.com/pyca/cryptography/issues with information on
#    how to reproduce this. ([_OpenSSLError(code=218603671, lib=13,
#    func=122, reason=151)])
```

Might be nice to raise a more relevant error message. Or catch this even sooner when the NameAttribute is added. 



Discussion:
@asayler thanks for the bug report, this is definitely something we should fix. Country codes are required to be two letters so ideally we'd catch this during the NameAttribute construction.

No problem. Let me know if you want me to dig deeper and submit a pull request - I might have some time to work up a fix if it's a simple as checking the code length.

@reaperhulk ok, so your suggestion is that `NameAttribute` should have `oid` specific rules?

I'm not a huge fan of that solution honestly, but I also think that allowing the user to build invalid objects and then having it blow up during signing is kind of weird.

Is there an alternative?

On Thu, Feb 4, 2016 at 9:36 PM, Paul Kehrer notifications@github.com
wrote:

> I'm not a huge fan of that solution honestly, but I also think that
> allowing the user to build invalid objects and then having it blow up
> during signing is kind of weird.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/2665#issuecomment-180164615.

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

I guess the 3 places it could be done are: At signing, when adding to a `Name`, and when building the `NameAttribute`. Of those 3 `NameAttribute` is really the only one that makes sense.

Arguably you could have a backend that would be willing to issue country codes longer than 2 characters, but I don't think I want to make our API worse to support that case.


Resolving patch:
diff --git a/src/cryptography/x509/name.py b/src/cryptography/x509/name.py
--- a/src/cryptography/x509/name.py
+++ b/src/cryptography/x509/name.py
@@ -7,7 +7,7 @@
 import six
 
 from cryptography import utils
-from cryptography.x509.oid import ObjectIdentifier
+from cryptography.x509.oid import NameOID, ObjectIdentifier
 
 
 class NameAttribute(object):
@@ -22,6 +22,11 @@ def __init__(self, oid, value):
                 "value argument must be a text type."
             )
 
+        if oid == NameOID.COUNTRY_NAME and len(value.encode("utf8")) != 2:
+            raise ValueError(
+                "Country name must be a 2 character country code"
+            )
+
         self._oid = oid
         self._value = value
 


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -3327,6 +3327,20 @@ def test_init_bad_value(self):
                 b'bytes'
             )
 
+    def test_init_bad_country_code_value(self):
+        with pytest.raises(ValueError):
+            x509.NameAttribute(
+                NameOID.COUNTRY_NAME,
+                u'United States'
+            )
+
+        # unicode string of length 2, but > 2 bytes
+        with pytest.raises(ValueError):
+            x509.NameAttribute(
+                NameOID.COUNTRY_NAME,
+                u'\U0001F37A\U0001F37A'
+            )
+
     def test_eq(self):
         assert x509.NameAttribute(
             x509.ObjectIdentifier('2.999.1'), u'value'