support x509 request challenge password parsing (#4944)

* support x509 request challenge password parsing

* switch to a more generic (but not too generic) attribute parsing

* make it raise a valueerror

* Update tests/x509/test_x509.py

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
This commit is contained in:
Paul Kehrer 2020-07-05 21:29:32 -05:00 committed by GitHub
parent 7a233b9a60
commit 28e2783a81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 130 additions and 3 deletions

View file

@ -42,6 +42,8 @@ Changelog
X.509 extension.
* Added support for parsing
:class:`~cryptography.x509.SignedCertificateTimestamps` in OCSP responses.
* Added support for parsing attributes in certificate signing requests via
:meth:`~cryptography.x509.CertificateSigningRequest.get_attribute_for_oid`.
.. _v2-9-2:

View file

@ -894,6 +894,17 @@ X.509 CSR (Certificate Signing Request) Object
:raises UnicodeError: If an extension contains IDNA encoding that is
invalid or not compliant with IDNA 2008.
.. method:: get_attribute_for_oid(oid)
.. versionadded:: 3.0
:param oid: An :class:`ObjectIdentifier` instance.
:returns: The bytes value of the attribute or an exception if not
found.
:raises cryptography.x509.AttributeNotFound: If the request does
not have the attribute requested.
.. method:: public_bytes(encoding)
@ -3217,6 +3228,15 @@ instances. The following common OIDs are available as constants.
Corresponds to the dotted string ``"1.3.6.1.5.5.7.48.1.2"``.
.. class:: AttributeOID
.. versionadded:: 3.0
.. attribute:: CHALLENGE_PASSWORD
Corresponds to the dotted string ``"1.2.840.113549.1.9.7"``.
Helper Functions
~~~~~~~~~~~~~~~~
.. currentmodule:: cryptography.x509
@ -3264,6 +3284,18 @@ Exceptions
Returns the OID.
.. class:: AttributeNotFound
This is raised when calling
:meth:`CertificateSigningRequest.get_attribute_for_oid` with
an attribute OID that is not present in the request.
.. attribute:: oid
:type: :class:`ObjectIdentifier`
Returns the OID.
.. class:: UnsupportedGeneralNameType
This is raised when a certificate contains an unsupported general name

View file

@ -16,10 +16,11 @@ from cryptography.hazmat.backends.openssl.decode_asn1 import (
_asn1_string_to_bytes, _decode_x509_name, _obj2txt, _parse_asn1_time
)
from cryptography.hazmat.backends.openssl.encode_asn1 import (
_encode_asn1_int_gc
_encode_asn1_int_gc, _txt2obj_gc
)
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
from cryptography.x509.name import _ASN1Type
@utils.register_interface(x509.Certificate)
@ -485,6 +486,41 @@ class _CertificateSigningRequest(object):
return True
def get_attribute_for_oid(self, oid):
obj = _txt2obj_gc(self._backend, oid.dotted_string)
pos = self._backend._lib.X509_REQ_get_attr_by_OBJ(
self._x509_req, obj, -1
)
if pos == -1:
raise x509.AttributeNotFound(
"No {} attribute was found".format(oid), oid
)
attr = self._backend._lib.X509_REQ_get_attr(self._x509_req, pos)
self._backend.openssl_assert(attr != self._backend._ffi.NULL)
asn1_type = self._backend._lib.X509_ATTRIBUTE_get0_type(attr, pos)
self._backend.openssl_assert(asn1_type != self._backend._ffi.NULL)
# We need this to ensure that our C type cast is safe.
# Also this should always be a sane string type, but we'll see if
# that is true in the real world...
if asn1_type.type not in (
_ASN1Type.UTF8String.value,
_ASN1Type.PrintableString.value,
_ASN1Type.IA5String.value,
):
raise ValueError("OID {} has a disallowed ASN.1 type: {}".format(
oid, asn1_type.type
))
data = self._backend._lib.X509_ATTRIBUTE_get0_data(
attr, pos, asn1_type.type, self._backend._ffi.NULL
)
self._backend.openssl_assert(data != self._backend._ffi.NULL)
# This cast is safe iff we assert on the type above to ensure
# that it is always a type of ASN1_STRING
data = self._backend._ffi.cast("ASN1_STRING *", data)
return _asn1_string_to_bytes(self._backend, data)
@utils.register_interface(
x509.certificate_transparency.SignedCertificateTimestamp

View file

@ -6,8 +6,8 @@ from __future__ import absolute_import, division, print_function
from cryptography.x509 import certificate_transparency
from cryptography.x509.base import (
Certificate, CertificateBuilder, CertificateRevocationList,
CertificateRevocationListBuilder,
AttributeNotFound, Certificate, CertificateBuilder,
CertificateRevocationList, CertificateRevocationListBuilder,
CertificateSigningRequest, CertificateSigningRequestBuilder,
InvalidVersion, RevokedCertificate, RevokedCertificateBuilder,
Version, load_der_x509_certificate, load_der_x509_crl, load_der_x509_csr,
@ -121,6 +121,7 @@ __all__ = [
"load_pem_x509_crl",
"load_der_x509_crl",
"random_serial_number",
"AttributeNotFound",
"InvalidVersion",
"DeltaCRLIndicator",
"DuplicateExtension",

View file

@ -22,6 +22,12 @@ from cryptography.x509.name import Name
_EARLIEST_UTC_TIME = datetime.datetime(1950, 1, 1)
class AttributeNotFound(Exception):
def __init__(self, msg, oid):
super(AttributeNotFound, self).__init__(msg)
self.oid = oid
def _reject_duplicate_extension(extension, extensions):
# This is quadratic in the number of extensions
for e in extensions:
@ -367,6 +373,12 @@ class CertificateSigningRequest(object):
Verifies signature of signing request.
"""
@abc.abstractproperty
def get_attribute_for_oid(self):
"""
Get the attribute value for a given OID.
"""
@six.add_metaclass(abc.ABCMeta)
class RevokedCertificate(object):

View file

@ -162,6 +162,10 @@ class CertificatePoliciesOID(object):
ANY_POLICY = ObjectIdentifier("2.5.29.32.0")
class AttributeOID(object):
CHALLENGE_PASSWORD = ObjectIdentifier("1.2.840.113549.1.9.7")
_OID_NAMES = {
NameOID.COMMON_NAME: "commonName",
NameOID.COUNTRY_NAME: "countryName",
@ -265,4 +269,5 @@ _OID_NAMES = {
CertificatePoliciesOID.CPS_QUALIFIER: "id-qt-cps",
CertificatePoliciesOID.CPS_USER_NOTICE: "id-qt-unotice",
OCSPExtensionOID.NONCE: "OCSPNonce",
AttributeOID.CHALLENGE_PASSWORD: "challengePassword",
}

View file

@ -1232,6 +1232,45 @@ class TestRSACertificateRequest(object):
assert isinstance(extensions, x509.Extensions)
assert list(extensions) == []
def test_get_attribute_for_oid(self, backend):
request = _load_cert(
os.path.join(
"x509", "requests", "challenge.pem"
), x509.load_pem_x509_csr, backend
)
assert request.get_attribute_for_oid(
x509.oid.AttributeOID.CHALLENGE_PASSWORD
) == b"challenge me!"
def test_invalid_attribute_for_oid(self, backend):
"""
This test deliberately triggers a ValueError because to parse
CSR attributes we need to do a C cast. If we're wrong about the
type that would be Very Bad so this test confirms we properly explode
in the presence of the wrong types.
"""
request = _load_cert(
os.path.join(
"x509", "requests", "challenge-invalid.der"
), x509.load_der_x509_csr, backend
)
with pytest.raises(ValueError):
request.get_attribute_for_oid(
x509.oid.AttributeOID.CHALLENGE_PASSWORD
)
def test_no_challenge_password(self, backend):
request = _load_cert(
os.path.join(
"x509", "requests", "rsa_sha256.pem"
), x509.load_pem_x509_csr, backend
)
with pytest.raises(x509.AttributeNotFound) as exc:
request.get_attribute_for_oid(
x509.oid.AttributeOID.CHALLENGE_PASSWORD
)
assert exc.value.oid == x509.oid.AttributeOID.CHALLENGE_PASSWORD
@pytest.mark.parametrize(
"loader_func",
[x509.load_pem_x509_csr, x509.load_der_x509_csr]