Skip to content

Commit 280c026

Browse files
alexclaude
andauthored
Add Name.from_bytes for parsing a Name from DER bytes (pyca#14980)
* Add Name.from_bytes for parsing a Name from DER bytes This is the inverse of Name.public_bytes. https://claude.ai/code/session_01TpZ3ogUWEYqoazBJn9ExmF * Fold round-trip assertion into test_from_bytes https://claude.ai/code/session_01TpZ3ogUWEYqoazBJn9ExmF --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9169605 commit 280c026

7 files changed

Lines changed: 71 additions & 1 deletion

File tree

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Changelog
4040
registers an :class:`enum.Enum` subclass as an ASN.1 value set: members
4141
are encoded as their underlying value, and decoding fails if the decoded
4242
value does not match one of the declared members.
43+
* Added :meth:`~cryptography.x509.Name.from_bytes` for parsing a
44+
:class:`~cryptography.x509.Name` from DER bytes, the inverse of
45+
:meth:`~cryptography.x509.Name.public_bytes`.
4346

4447
.. _v48-0-0:
4548

docs/x509/reference.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,23 @@ X.509 CSR (Certificate Signing Request) Builder Object
16331633

16341634
:type: list of :class:`RelativeDistinguishedName`
16351635

1636+
.. classmethod:: from_bytes(data)
1637+
1638+
.. versionadded:: 49.0.0
1639+
1640+
Parse a DER encoded name (the inverse of :meth:`public_bytes`).
1641+
1642+
:param bytes data: The DER encoded name.
1643+
1644+
:returns: A :class:`Name` parsed from ``data``.
1645+
1646+
.. doctest::
1647+
1648+
>>> x509.Name.from_bytes(
1649+
... b"0\x1a1\x180\x16\x06\x03U\x04\x03\x0c\x0fcryptography.io"
1650+
... )
1651+
<Name(CN=cryptography.io)>
1652+
16361653
.. classmethod:: from_rfc4514_string(data, attr_name_overrides=None)
16371654

16381655
.. versionadded: 37.0.0

src/cryptography/hazmat/bindings/_rust/x509.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def load_der_x509_csr(
3939
data: bytes, backend: typing.Any = None
4040
) -> x509.CertificateSigningRequest: ...
4141
def encode_name_bytes(name: x509.Name) -> bytes: ...
42+
def parse_name_bytes(data: bytes) -> x509.Name: ...
4243
def encode_extension_value(extension: x509.ExtensionType) -> bytes: ...
4344
def create_x509_certificate(
4445
builder: x509.CertificateBuilder,

src/cryptography/x509/name.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,13 @@ def from_rfc4514_string(
320320
) -> Name:
321321
return _RFC4514NameParser(data, attr_name_overrides or {}).parse()
322322

323+
@classmethod
324+
def from_bytes(cls, data: bytes) -> Name:
325+
"""
326+
Parse a DER encoded X.509 name (the inverse of ``public_bytes``).
327+
"""
328+
return rust_x509.parse_name_bytes(data)
329+
323330
def rfc4514_string(
324331
self, attr_name_overrides: _OidNameMap | None = None
325332
) -> str:

src/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ mod _rust {
169169
load_pem_x509_certificates, Certificate,
170170
};
171171
#[pymodule_export]
172-
use crate::x509::common::{encode_extension_value, encode_name_bytes};
172+
use crate::x509::common::{encode_extension_value, encode_name_bytes, parse_name_bytes};
173173
#[pymodule_export]
174174
use crate::x509::crl::{
175175
create_revoked_certificate, create_x509_crl, load_der_x509_crl, load_pem_x509_crl,

src/rust/src/x509/common.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ pub(crate) fn encode_name_bytes<'p>(
120120
Ok(pyo3::types::PyBytes::new(py, &result))
121121
}
122122

123+
#[pyo3::pyfunction]
124+
pub(crate) fn parse_name_bytes<'p>(
125+
py: pyo3::Python<'p>,
126+
data: &[u8],
127+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::PyAny>> {
128+
let name = asn1::parse_single::<NameReadable<'_>>(data)?;
129+
parse_name(py, &name)
130+
}
131+
123132
pub(crate) fn encode_general_names<'a>(
124133
py: pyo3::Python<'_>,
125134
ka_bytes: &'a cryptography_keepalive::KeepAlive<pyo3::pybacked::PyBackedBytes>,

tests/x509/test_x509.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6688,6 +6688,39 @@ def test_bytes(self, backend):
66886688
b"b060355040a0c0450794341"
66896689
)
66906690

6691+
def test_from_bytes(self):
6692+
data = binascii.unhexlify(
6693+
b"30293118301606035504030c0f63727970746f6772617068792e696f310d"
6694+
b"300b060355040a0c0450794341"
6695+
)
6696+
name = x509.Name.from_bytes(data)
6697+
assert name == x509.Name(
6698+
[
6699+
x509.NameAttribute(NameOID.COMMON_NAME, "cryptography.io"),
6700+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "PyCA"),
6701+
]
6702+
)
6703+
assert name.public_bytes() == data
6704+
6705+
@pytest.mark.parametrize(
6706+
"data",
6707+
[
6708+
# Empty.
6709+
b"",
6710+
# Not valid DER.
6711+
b"\x00\x01\x02",
6712+
# Wrong tag (SET instead of SEQUENCE).
6713+
b"\x31\x00",
6714+
# Trailing data after the name.
6715+
binascii.unhexlify(b"300000"),
6716+
# RDN with a UTF8String value that is not valid UTF-8.
6717+
binascii.unhexlify(b"300c310a300806035504030c01ff"),
6718+
],
6719+
)
6720+
def test_from_bytes_invalid(self, data):
6721+
with pytest.raises(ValueError):
6722+
x509.Name.from_bytes(data)
6723+
66916724
def test_bitstring_encoding(self):
66926725
name = x509.Name(
66936726
[

0 commit comments

Comments
 (0)