Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Changelog

.. note:: This version is not yet released and is under active development.

* Added support for the :class:`~cryptography.x509.PolicyMappings` extension.

.. _v50-0-0:

50.0.0 - 2026-07-31
Expand Down
20 changes: 20 additions & 0 deletions docs/x509/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2986,6 +2986,26 @@ X.509 Extensions
mapping may be processed in certificates issued by the subject of this
certificate, but not in additional certificates in the chain.

.. class:: PolicyMappings(mappings)
:canonical: cryptography.x509.extensions.PolicyMappings

.. versionadded:: 51.0.0

The policy mappings extension is used in CA certificates to map policy
identifiers in the issuer's policy domain to policy identifiers in the
subject's policy domain. For more information see :rfc:`5280`.

:param mappings: A non-empty iterable of 2-tuples. Each tuple contains an
issuer domain policy :class:`ObjectIdentifier` followed by a subject
domain policy :class:`ObjectIdentifier`.

.. attribute:: oid

:type: :class:`ObjectIdentifier`

Returns :attr:`~cryptography.x509.oid.ExtensionOID.POLICY_MAPPINGS`.


.. class:: CRLNumber(crl_number)
:canonical: cryptography.x509.extensions.CRLNumber

Expand Down
2 changes: 2 additions & 0 deletions src/cryptography/x509/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
OCSPNonce,
PolicyConstraints,
PolicyInformation,
PolicyMappings,
PrecertificateSignedCertificateTimestamps,
PrecertPoison,
PrivateKeyUsagePeriod,
Expand Down Expand Up @@ -233,6 +234,7 @@
"OtherName",
"PolicyConstraints",
"PolicyInformation",
"PolicyMappings",
"PrecertPoison",
"PrecertificateSignedCertificateTimestamps",
"PrivateKeyUsagePeriod",
Expand Down
47 changes: 47 additions & 0 deletions src/cryptography/x509/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
from cryptography.x509.name import Name, RelativeDistinguishedName
from cryptography.x509.oid import (
CertificatePoliciesOID,
CRLEntryExtensionOID,
ExtensionOID,
ObjectIdentifier,
Expand Down Expand Up @@ -815,6 +816,52 @@ def public_bytes(self) -> bytes:
return rust_x509.encode_extension_value(self)


class PolicyMappings(ExtensionType):
oid = ExtensionOID.POLICY_MAPPINGS

def __init__(
self,
mappings: Iterable[tuple[ObjectIdentifier, ObjectIdentifier]],
) -> None:
mappings = list(mappings)
if not mappings:
raise ValueError("mappings must be a non-empty list")
if not all(
isinstance(mapping, tuple)
and len(mapping) == 2
and all(isinstance(oid, ObjectIdentifier) for oid in mapping)
for mapping in mappings
):
raise TypeError(
"Every item in the mappings list must be a 2-tuple of "
"ObjectIdentifier"
)
if any(
CertificatePoliciesOID.ANY_POLICY in mapping
for mapping in mappings
):
raise ValueError("Policy mappings must not contain anyPolicy")

self._mappings = mappings

__len__, __iter__, __getitem__ = _make_sequence_methods("_mappings")

def __repr__(self) -> str:
return f"<PolicyMappings({self._mappings})>"

def __eq__(self, other: object) -> bool:
if not isinstance(other, PolicyMappings):
return NotImplemented

return self._mappings == other._mappings

def __hash__(self) -> int:
return hash(tuple(self._mappings))

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


class CertificatePolicies(ExtensionType):
oid = ExtensionOID.CERTIFICATE_POLICIES

Expand Down
8 changes: 8 additions & 0 deletions src/rust/cryptography-x509/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ pub struct PolicyConstraints {
pub inhibit_policy_mapping: Option<u64>,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub struct PolicyMapping {
pub issuer_domain_policy: asn1::ObjectIdentifier,
pub subject_domain_policy: asn1::ObjectIdentifier,
}

pub type PolicyMappings<'a, Op> = <Op as Asn1Operation>::SequenceOfVec<'a, PolicyMapping>;

#[derive(asn1::Asn1Read, asn1::Asn1Write)]
pub struct AccessDescription<'a> {
pub access_method: asn1::ObjectIdentifier,
Expand Down
1 change: 1 addition & 0 deletions src/rust/cryptography-x509/src/oid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub const CERTIFICATE_ISSUER_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29,
pub const NAME_CONSTRAINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 30);
pub const CRL_DISTRIBUTION_POINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 31);
pub const CERTIFICATE_POLICIES_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 32);
pub const POLICY_MAPPINGS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 33);
pub const AUTHORITY_KEY_IDENTIFIER_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 35);
pub const POLICY_CONSTRAINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 36);
pub const EXTENDED_KEY_USAGE_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 37);
Expand Down
2 changes: 2 additions & 0 deletions src/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ pub static INHIBIT_ANY_POLICY: LazyPyImport =
pub static OCSP_NO_CHECK: LazyPyImport = LazyPyImport::new("cryptography.x509", &["OCSPNoCheck"]);
pub static POLICY_CONSTRAINTS: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["PolicyConstraints"]);
pub static POLICY_MAPPINGS: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["PolicyMappings"]);
pub static CERTIFICATE_POLICIES: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["CertificatePolicies"]);
pub static SUBJECT_INFORMATION_ACCESS: LazyPyImport =
Expand Down
12 changes: 11 additions & 1 deletion src/rust/src/x509/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use cryptography_x509::extensions::{
Admission, Admissions, AuthorityKeyIdentifier, BasicConstraints, DisplayText,
DistributionPoint, DistributionPointName, DuplicateExtensionsError, ExtendedKeyUsage,
Extension, IssuerAlternativeName, KeyUsage, MSCertificateTemplate, NameConstraints,
NamingAuthority, PolicyConstraints, PolicyInformation, PolicyQualifierInfo,
NamingAuthority, PolicyConstraints, PolicyInformation, PolicyMappings, PolicyQualifierInfo,
PrivateKeyUsagePeriod, ProfessionInfo, Qualifier, RawExtensions, SequenceOfAccessDescriptions,
SequenceOfSubtrees, SubjectAlternativeName, UserNotice,
};
Expand Down Expand Up @@ -889,6 +889,16 @@ pub fn parse_cert_ext<'p>(
pc.inhibit_policy_mapping,
))?))
}
oid::POLICY_MAPPINGS_OID => {
let mappings = ext.value::<PolicyMappings<'_, Asn1Read>>()?;
let py_mappings = pyo3::types::PyList::empty(py);
for mapping in mappings {
let issuer_policy = oid_to_py_oid(py, &mapping.issuer_domain_policy)?;
let subject_policy = oid_to_py_oid(py, &mapping.subject_domain_policy)?;
py_mappings.append((issuer_policy, subject_policy))?;
}
Ok(Some(types::POLICY_MAPPINGS.get(py)?.call1((py_mappings,))?))
}
oid::OCSP_NO_CHECK_OID => {
ext.value::<()>()?;
Ok(Some(types::OCSP_NO_CHECK.get(py)?.call0()?))
Expand Down
12 changes: 12 additions & 0 deletions src/rust/src/x509/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,18 @@ pub(crate) fn encode_extension(
};
Ok(Some(asn1::write_single(&pc)?))
}
&oid::POLICY_MAPPINGS_OID => {
let mut mappings = vec![];
for py_mapping in ext.try_iter()? {
let py_mapping = py_mapping?;
mappings.push(extensions::PolicyMapping {
issuer_domain_policy: py_oid_to_oid(py_mapping.get_item(0)?)?,
subject_domain_policy: py_oid_to_oid(py_mapping.get_item(1)?)?,
});
}
let mappings = asn1::SequenceOfWriter::new(mappings);
Ok(Some(asn1::write_single(&mappings)?))
}
&oid::NAME_CONSTRAINTS_OID => {
let ka_bytes = cryptography_keepalive::KeepAlive::new();
let ka_str = cryptography_keepalive::KeepAlive::new();
Expand Down
68 changes: 68 additions & 0 deletions tests/x509/test_x509_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
)
from cryptography.x509.oid import (
AuthorityInformationAccessOID,
CertificatePoliciesOID,
ExtendedKeyUsageOID,
ExtensionOID,
NameOID,
Expand Down Expand Up @@ -3124,6 +3125,73 @@ def test_public_bytes(self):
assert ext.public_bytes() == b"\x30\x03\x81\x01\x00"


class TestPolicyMappings:
issuer_policy = ObjectIdentifier("1.2.3.4")
subject_policy = ObjectIdentifier("1.2.3.5")
mapping = ((issuer_policy, subject_policy),)

def test_invalid_mappings(self):
with pytest.raises(TypeError):
x509.PolicyMappings(
[(self.issuer_policy, typing.cast(typing.Any, "invalid"))]
)
with pytest.raises(TypeError):
x509.PolicyMappings(
[typing.cast(typing.Any, (self.issuer_policy,))]
)

def test_empty_mappings(self):
with pytest.raises(ValueError):
x509.PolicyMappings([])

@pytest.mark.parametrize("position", [0, 1])
def test_any_policy(self, position):
mapping = list(self.mapping[0])
mapping[position] = CertificatePoliciesOID.ANY_POLICY
with pytest.raises(ValueError, match="must not contain anyPolicy"):
x509.PolicyMappings([tuple(mapping)]) # type: ignore[list-item]

def test_iter_len_index(self):
mappings = x509.PolicyMappings(self.mapping)
assert len(mappings) == 1
assert list(mappings) == list(self.mapping)

def test_repr(self):
assert repr(x509.PolicyMappings(self.mapping)) == (
"<PolicyMappings([(<ObjectIdentifier(oid=1.2.3.4, name=Unknown "
"OID)>, <ObjectIdentifier(oid=1.2.3.5, name=Unknown OID)>)])>"
)

def test_eq_hash(self):
iss_to_sub = [(self.issuer_policy, self.subject_policy)]
sub_to_iss = [(self.subject_policy, self.issuer_policy)]

mappings = x509.PolicyMappings(iss_to_sub)
mappings2 = x509.PolicyMappings(iss_to_sub)
mappings3 = x509.PolicyMappings(sub_to_iss)
assert mappings == mappings2
assert mappings != mappings3
assert mappings != object()
assert hash(mappings) == hash(mappings2)
assert hash(mappings) != hash(mappings3)

def test_public_bytes(self):
mappings = x509.PolicyMappings(self.mapping)
assert mappings.public_bytes() == (
b"\x30\x0c\x30\x0a\x06\x03\x2a\x03\x04\x06\x03\x2a\x03\x05"
)

def test_certbuilder(self, rsa_key_2048: rsa.RSAPrivateKey):
cert = (
_make_certbuilder(rsa_key_2048)
.add_extension(x509.PolicyMappings(self.mapping), critical=True)
.sign(rsa_key_2048, hashes.SHA256())
)
ext = cert.extensions.get_extension_for_class(x509.PolicyMappings)
assert ext.critical is True
assert list(ext.value) == list(self.mapping)


class TestAuthorityInformationAccess:
def test_invalid_descriptions(self):
with pytest.raises(TypeError):
Expand Down