both parse and encode the ASN1 string type for Name attributes (#3896)

* both parse and encode the ASN1 string type for Name attributes

Previously cryptography encoded everything (except country names) as
UTF8String. This caused problems with chain building in libraries like
NSS where the subject and issuer are expected to match byte-for-byte.

With this change we now parse and store the ASN1 string type as a
private _type in NameAttribute. We then use this to encode when issuing
a new certificate. This allows the CertificateBuilder to properly
construct an identical issuer and fixes the issue with NSS.

* make the sentinel private too
This commit is contained in:
Paul Kehrer 2017-09-26 10:23:24 +08:00 committed by Alex Gaynor
parent 7bb0210ef9
commit 72c92f5ed1
6 changed files with 107 additions and 23 deletions

View file

@ -41,6 +41,8 @@ Changelog
* Added support for using labels with
:class:`~cryptography.hazmat.primitives.asymmetric.padding.OAEP` when using
OpenSSL 1.0.2 or greater.
* Improved compatibility with NSS when issuing certificates from an issuer
that has a subject with non-``UTF8String`` string types.
* Add support for the :class:`~cryptography.x509.DeltaCRLIndicator` extension.
* Add support for the :class:`~cryptography.x509.TLSFeature`
extension. This is commonly used for enabling ``OCSP Must-Staple`` in

View file

@ -95,12 +95,6 @@ class Backend(object):
self._ffi = self._binding.ffi
self._lib = self._binding.lib
# Set the default string mask for encoding ASN1 strings to UTF8. This
# is the default for newer OpenSSLs for several years (1.0.1h+) and is
# recommended in RFC 2459.
res = self._lib.ASN1_STRING_set_default_mask_asc(b"utf8only")
self.openssl_assert(res == 1)
self._cipher_registry = {}
self._register_default_ciphers()
self.activate_osrandom_engine()

View file

@ -11,6 +11,7 @@ from asn1crypto.core import Integer, SequenceOf
from cryptography import x509
from cryptography.x509.extensions import _TLS_FEATURE_TYPE_TO_ENUM
from cryptography.x509.name import _ASN1_TYPE_TO_ENUM
from cryptography.x509.oid import (
CRLEntryExtensionOID, CertificatePoliciesOID, ExtensionOID
)
@ -51,8 +52,9 @@ def _decode_x509_name_entry(backend, x509_name_entry):
backend.openssl_assert(data != backend._ffi.NULL)
value = _asn1_string_to_utf8(backend, data)
oid = _obj2txt(backend, obj)
type = _ASN1_TYPE_TO_ENUM[data.type]
return x509.NameAttribute(x509.ObjectIdentifier(oid), value)
return x509.NameAttribute(x509.ObjectIdentifier(oid), value, type)
def _decode_x509_name(backend, x509_name):

View file

@ -14,7 +14,7 @@ from cryptography.hazmat.backends.openssl.decode_asn1 import (
_CRL_ENTRY_REASON_ENUM_TO_CODE, _DISTPOINT_TYPE_FULLNAME,
_DISTPOINT_TYPE_RELATIVENAME
)
from cryptography.x509.oid import CRLEntryExtensionOID, ExtensionOID, NameOID
from cryptography.x509.oid import CRLEntryExtensionOID, ExtensionOID
def _encode_asn1_int(backend, x):
@ -118,17 +118,9 @@ def _encode_sk_name_entry(backend, attributes):
def _encode_name_entry(backend, attribute):
value = attribute.value.encode('utf8')
obj = _txt2obj_gc(backend, attribute.oid.dotted_string)
if attribute.oid in [
NameOID.COUNTRY_NAME, NameOID.JURISDICTION_COUNTRY_NAME
]:
# Per RFC5280 Appendix A.1 countryName should be encoded as
# PrintableString, not UTF8String. EV Guidelines section 9.2.5 says
# jurisdictionCountryName follows the same rules as countryName.
type = backend._lib.MBSTRING_ASC
else:
type = backend._lib.MBSTRING_UTF8
name_entry = backend._lib.X509_NAME_ENTRY_create_by_OBJ(
backend._ffi.NULL, obj, type, value, -1
backend._ffi.NULL, obj, attribute._type.value, value, -1
)
return name_entry

View file

@ -4,14 +4,33 @@
from __future__ import absolute_import, division, print_function
from enum import Enum
import six
from cryptography import utils
from cryptography.x509.oid import NameOID, ObjectIdentifier
class _ASN1Type(Enum):
UTF8String = 12
NumericString = 18
PrintableString = 19
T61String = 20
IA5String = 22
UTCTime = 23
GeneralizedTime = 24
VisibleString = 26
UniversalString = 28
BMPString = 30
_ASN1_TYPE_TO_ENUM = dict((i.value, i) for i in _ASN1Type)
_SENTINEL = object()
class NameAttribute(object):
def __init__(self, oid, value):
def __init__(self, oid, value, _type=_SENTINEL):
if not isinstance(oid, ObjectIdentifier):
raise TypeError(
"oid argument must be an ObjectIdentifier instance."
@ -22,16 +41,33 @@ class NameAttribute(object):
"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"
)
if (
oid == NameOID.COUNTRY_NAME or
oid == NameOID.JURISDICTION_COUNTRY_NAME
):
if len(value.encode("utf8")) != 2:
raise ValueError(
"Country name must be a 2 character country code"
)
if _type == _SENTINEL:
_type = _ASN1Type.PrintableString
if len(value) == 0:
raise ValueError("Value cannot be an empty string")
# Set the default string type for encoding ASN1 strings to UTF8. This
# is the default for newer OpenSSLs for several years (1.0.1h+) and is
# recommended in RFC 2459.
if _type == _SENTINEL:
_type = _ASN1Type.UTF8String
if not isinstance(_type, _ASN1Type):
raise TypeError("_type must be from the _ASN1Type enum")
self._oid = oid
self._value = value
self._type = _type
oid = utils.read_only_property("_oid")
value = utils.read_only_property("_value")

View file

@ -28,6 +28,7 @@ from cryptography.hazmat.primitives.asymmetric import dsa, ec, padding, rsa
from cryptography.hazmat.primitives.asymmetric.utils import (
decode_dss_signature
)
from cryptography.x509.name import _ASN1Type
from cryptography.x509.oid import (
AuthorityInformationAccessOID, ExtendedKeyUsageOID, ExtensionOID,
NameOID, SignatureAlgorithmOID
@ -1496,6 +1497,43 @@ class TestRSACertificateRequest(object):
x509.DNSName(b"cryptography.io"),
]
def test_build_cert_private_type_encoding(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)
name = x509.Name([
x509.NameAttribute(
NameOID.STATE_OR_PROVINCE_NAME, u'Texas',
_ASN1Type.PrintableString),
x509.NameAttribute(NameOID.LOCALITY_NAME, u'Austin'),
x509.NameAttribute(
NameOID.COMMON_NAME, u'cryptography.io', _ASN1Type.IA5String),
])
builder = x509.CertificateBuilder().serial_number(
777
).issuer_name(
name
).subject_name(
name
).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)
for dn in (cert.subject, cert.issuer):
assert dn.get_attributes_for_oid(
NameOID.STATE_OR_PROVINCE_NAME
)[0]._type == _ASN1Type.PrintableString
assert dn.get_attributes_for_oid(
NameOID.STATE_OR_PROVINCE_NAME
)[0]._type == _ASN1Type.PrintableString
assert dn.get_attributes_for_oid(
NameOID.LOCALITY_NAME
)[0]._type == _ASN1Type.UTF8String
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)
@ -3628,6 +3666,26 @@ class TestNameAttribute(object):
with pytest.raises(ValueError):
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'')
def test_country_name_type(self):
na = x509.NameAttribute(NameOID.COUNTRY_NAME, u"US")
assert na._type == _ASN1Type.PrintableString
na2 = x509.NameAttribute(
NameOID.COUNTRY_NAME, u"US", _ASN1Type.IA5String
)
assert na2._type == _ASN1Type.IA5String
def test_types(self):
na = x509.NameAttribute(NameOID.COMMON_NAME, u"common")
assert na._type == _ASN1Type.UTF8String
na2 = x509.NameAttribute(
NameOID.COMMON_NAME, u"common", _ASN1Type.IA5String
)
assert na2._type == _ASN1Type.IA5String
def test_invalid_type(self):
with pytest.raises(TypeError):
x509.NameAttribute(NameOID.COMMON_NAME, u"common", "notanenum")
def test_eq(self):
assert x509.NameAttribute(
x509.ObjectIdentifier('2.999.1'), u'value'