Skip to content

Commit

Permalink
Allow to serialize extension values as DER bytes string.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Oct 31, 2021
1 parent 55414ad commit 88189ad
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Changelog
:func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_pkcs12`,
which will return an object of type
:class:`~cryptography.hazmat.primitives.serialization.pkcs12.PKCS12KeyAndCertificates`.
* Extension values can now be serialized to a DER byte string by calling
:func:`~cryptography.x509.ExtensionType.public_bytes`.

.. _v35-0-0:

Expand Down
8 changes: 8 additions & 0 deletions docs/x509/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,14 @@ X.509 Extensions

Returns the OID associated with the given extension type.

.. method:: public_bytes

.. versionadded:: 36.0

:return bytes:

A bytes string representing the extension's DER encoded value.

.. class:: KeyUsage(digital_signature, content_commitment, key_encipherment, data_encipherment, key_agreement, key_cert_sign, crl_sign, encipher_only, decipher_only)

.. versionadded:: 0.9
Expand Down
1 change: 1 addition & 0 deletions src/cryptography/hazmat/bindings/_rust/x509.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def load_der_x509_crl(data: bytes) -> x509.CertificateRevocationList: ...
def load_pem_x509_csr(data: bytes) -> x509.CertificateSigningRequest: ...
def load_der_x509_csr(data: bytes) -> x509.CertificateSigningRequest: ...
def encode_name_bytes(name: x509.Name) -> bytes: ...
def encode_extension_value(extension: x509.ExtensionType) -> bytes: ...
def create_x509_certificate(
builder: x509.CertificateBuilder,
private_key: PRIVATE_KEY_TYPES,
Expand Down
4 changes: 4 additions & 0 deletions src/cryptography/x509/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from cryptography import utils
from cryptography.hazmat.bindings._rust import asn1
from cryptography.hazmat.bindings._rust import x509 as rust_x509
from cryptography.hazmat.primitives import constant_time, serialization
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
Expand Down Expand Up @@ -90,6 +91,9 @@ def __init__(self, msg: str, oid: ObjectIdentifier) -> None:
class ExtensionType(metaclass=abc.ABCMeta):
oid: typing.ClassVar[ObjectIdentifier]

def public_bytes(self) -> bytes:
return rust_x509.encode_extension_value(self)


class Extensions(object):
def __init__(
Expand Down
51 changes: 51 additions & 0 deletions src/rust/src/x509/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// for complete details.

use crate::asn1::PyAsn1Error;
use crate::x509;
use chrono::{Datelike, TimeZone, Timelike};
use pyo3::ToPyObject;
use std::collections::HashSet;
Expand Down Expand Up @@ -646,6 +647,55 @@ pub(crate) fn encode_extensions<
)))
}

#[pyo3::prelude::pyfunction]
fn encode_extension_value<'p>(
py: pyo3::Python<'p>,
py_ext: &'p pyo3::PyAny,
) -> pyo3::PyResult<&'p pyo3::types::PyBytes> {
let unrecognized_extension_type: &pyo3::types::PyType = py
.import("cryptography.x509")?
.getattr("UnrecognizedExtension")?
.extract()?;
if unrecognized_extension_type.is_instance(py_ext)? {
return Ok(pyo3::types::PyBytes::new(
py,
py_ext.getattr("value")?.extract::<&[u8]>()?,
));
}

let oid = asn1::ObjectIdentifier::from_string(
py_ext
.getattr("oid")?
.getattr("dotted_string")?
.extract::<&str>()?,
)
.unwrap();

for encode_ext in [
x509::certificate::encode_certificate_extension,
x509::crl::encode_crl_entry_extension,
x509::crl::encode_crl_extension,
x509::ocsp_req::encode_ocsp_request_extension,
x509::ocsp_resp::encode_ocsp_basic_response_extension,
]
.iter()
{
match encode_ext(&oid, py_ext)? {
Some(data) => {
// TODO: extra copy
let py_data = pyo3::types::PyBytes::new(py, &data);
return Ok(py_data);
}
None => {}
}
}

return Err(pyo3::exceptions::PyNotImplementedError::new_err(format!(
"Extension not supported: {}",
oid
)));
}

pub(crate) fn chrono_to_py<'p>(
py: pyo3::Python<'p>,
dt: &chrono::DateTime<chrono::Utc>,
Expand Down Expand Up @@ -743,6 +793,7 @@ mod tests {
}

pub(crate) fn add_to_module(module: &pyo3::prelude::PyModule) -> pyo3::PyResult<()> {
module.add_wrapped(pyo3::wrap_pyfunction!(encode_extension_value))?;
module.add_wrapped(pyo3::wrap_pyfunction!(encode_name_bytes))?;

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions src/rust/src/x509/crl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ pub fn parse_crl_entry_ext<'p>(
}
}

fn encode_crl_extension(
pub(crate) fn encode_crl_extension(
oid: &asn1::ObjectIdentifier<'_>,
ext: &pyo3::PyAny,
) -> pyo3::PyResult<Option<Vec<u8>>> {
Expand Down Expand Up @@ -709,7 +709,7 @@ fn encode_crl_extension(
}
}

fn encode_crl_entry_extension(
pub(crate) fn encode_crl_entry_extension(
oid: &asn1::ObjectIdentifier<'_>,
ext: &pyo3::PyAny,
) -> pyo3::PyResult<Option<Vec<u8>>> {
Expand Down
2 changes: 1 addition & 1 deletion src/rust/src/x509/ocsp_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ fn create_ocsp_request(py: pyo3::Python<'_>, builder: &pyo3::PyAny) -> PyAsn1Res
load_der_ocsp_request(py, &data)
}

fn encode_ocsp_request_extension(
pub(crate) fn encode_ocsp_request_extension(
oid: &asn1::ObjectIdentifier<'_>,
ext: &pyo3::PyAny,
) -> pyo3::PyResult<Option<Vec<u8>>> {
Expand Down
2 changes: 1 addition & 1 deletion src/rust/src/x509/ocsp_resp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ fn create_ocsp_response(
load_der_ocsp_response(py, &data)
}

fn encode_ocsp_basic_response_extension(
pub(crate) fn encode_ocsp_basic_response_extension(
oid: &asn1::ObjectIdentifier<'_>,
ext: &pyo3::PyAny,
) -> pyo3::PyResult<Option<Vec<u8>>> {
Expand Down
52 changes: 51 additions & 1 deletion tests/x509/test_x509_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509 import DNSName, NameConstraints, SubjectAlternativeName
from cryptography.x509.extensions import _key_identifier_from_public_key
from cryptography.x509.extensions import (
ExtensionType,
_key_identifier_from_public_key,
)
from cryptography.x509.oid import (
AuthorityInformationAccessOID,
ExtendedKeyUsageOID,
Expand Down Expand Up @@ -198,6 +201,12 @@ def test_indexing(self):
assert ext[-1] == ext[1]
assert ext[0] == x509.TLSFeatureType.status_request

def test_der_string(self):
ext1 = x509.TLSFeature([x509.TLSFeatureType.status_request])
assert ext1.public_bytes() == b"\x30\x03\x02\x01\x05"
ext2 = x509.TLSFeature([x509.TLSFeatureType.status_request_v2])
assert ext2.public_bytes() == b"\x30\x03\x02\x01\x11"


class TestUnrecognizedExtension(object):
def test_invalid_oid(self):
Expand Down Expand Up @@ -251,6 +260,20 @@ def test_hash(self):
assert hash(ext1) == hash(ext2)
assert hash(ext1) != hash(ext3)

def test_der_string(self):
ext1 = x509.UnrecognizedExtension(
x509.ObjectIdentifier("1.2.3.5"), b"\x03\x02\x01"
)
assert ext1.public_bytes() == b"\x03\x02\x01"

# The following creates a BasicConstraints extension with an invalid
# value. The serialization code should still handle it correctly by
# special-casing UnrecognizedExtension.
ext2 = x509.UnrecognizedExtension(
x509.oid.ExtensionOID.BASIC_CONSTRAINTS, b"\x03\x02\x01"
)
assert ext2.public_bytes() == b"\x03\x02\x01"


class TestCertificateIssuer(object):
def test_iter_names(self):
Expand Down Expand Up @@ -308,6 +331,10 @@ def test_hash(self):
assert hash(ci1) == hash(ci2)
assert hash(ci1) != hash(ci3)

def test_der_string(self):
ext = x509.CertificateIssuer([x509.DNSName("cryptography.io")])
assert ext.public_bytes() == b"0\x11\x82\x0fcryptography.io"


class TestCRLReason(object):
def test_invalid_reason_flags(self):
Expand Down Expand Up @@ -2193,6 +2220,10 @@ def test_hash(self):
assert hash(c1) == hash(c2)
assert hash(c1) != hash(c3)

def test_der_string(self):
ext = x509.CRLNumber(15)
assert ext.public_bytes() == b"\x02\x01\x0f"


class TestSubjectAlternativeName(object):
def test_get_values_for_type(self):
Expand Down Expand Up @@ -2735,6 +2766,13 @@ def test_require_explicit_policy(self, backend):
inhibit_policy_mapping=None,
)

def test_der_string(self):
ext = x509.PolicyConstraints(
require_explicit_policy=None,
inhibit_policy_mapping=0,
)
assert ext.public_bytes() == b"\x30\x03\x81\x01\x00"


class TestAuthorityInformationAccess(object):
def test_invalid_descriptions(self):
Expand Down Expand Up @@ -5674,9 +5712,21 @@ def test_hash(self):
assert hash(nonce1) == hash(nonce2)
assert hash(nonce1) != hash(nonce3)

def test_der_string(self):
ext = x509.OCSPNonce(b"0" * 5)
assert ext.public_bytes() == b"00000"


def test_all_extension_oid_members_have_names_defined():
for oid in dir(ExtensionOID):
if oid.startswith("__"):
continue
assert getattr(ExtensionOID, oid) in _OID_NAMES


def test_unknown_extension():
class MyExtension(ExtensionType):
oid = x509.ObjectIdentifier("1.2.3.4")

with pytest.raises(NotImplementedError):
MyExtension().public_bytes()

0 comments on commit 88189ad

Please sign in to comment.