Skip to content

Commit d698b90

Browse files
committed
Implement PolicyMappings X.509 extension
1 parent d062d06 commit d698b90

10 files changed

Lines changed: 176 additions & 1 deletion

File tree

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Changelog
88

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

11+
* Added support for the :class:`~cryptography.x509.PolicyMappings` extension.
12+
1113
.. _v50-0-0:
1214

1315
50.0.0 - 2026-07-31

docs/x509/reference.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2986,6 +2986,26 @@ X.509 Extensions
29862986
mapping may be processed in certificates issued by the subject of this
29872987
certificate, but not in additional certificates in the chain.
29882988

2989+
.. class:: PolicyMappings(mappings)
2990+
:canonical: cryptography.x509.extensions.PolicyMappings
2991+
2992+
.. versionadded:: 51.0.0
2993+
2994+
The policy mappings extension is used in CA certificates to map policy
2995+
identifiers in the issuer's policy domain to policy identifiers in the
2996+
subject's policy domain. For more information see :rfc:`5280`.
2997+
2998+
:param mappings: A non-empty iterable of 2-tuples. Each tuple contains an
2999+
issuer domain policy :class:`ObjectIdentifier` followed by a subject
3000+
domain policy :class:`ObjectIdentifier`.
3001+
3002+
.. attribute:: oid
3003+
3004+
:type: :class:`ObjectIdentifier`
3005+
3006+
Returns :attr:`~cryptography.x509.oid.ExtensionOID.POLICY_MAPPINGS`.
3007+
3008+
29893009
.. class:: CRLNumber(crl_number)
29903010
:canonical: cryptography.x509.extensions.CRLNumber
29913011

src/cryptography/x509/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
OCSPNonce,
6565
PolicyConstraints,
6666
PolicyInformation,
67+
PolicyMappings,
6768
PrecertificateSignedCertificateTimestamps,
6869
PrecertPoison,
6970
PrivateKeyUsagePeriod,
@@ -233,6 +234,7 @@
233234
"OtherName",
234235
"PolicyConstraints",
235236
"PolicyInformation",
237+
"PolicyMappings",
236238
"PrecertPoison",
237239
"PrecertificateSignedCertificateTimestamps",
238240
"PrivateKeyUsagePeriod",

src/cryptography/x509/extensions.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
)
3838
from cryptography.x509.name import Name, RelativeDistinguishedName
3939
from cryptography.x509.oid import (
40+
CertificatePoliciesOID,
4041
CRLEntryExtensionOID,
4142
ExtensionOID,
4243
ObjectIdentifier,
@@ -815,6 +816,52 @@ def public_bytes(self) -> bytes:
815816
return rust_x509.encode_extension_value(self)
816817

817818

819+
class PolicyMappings(ExtensionType):
820+
oid = ExtensionOID.POLICY_MAPPINGS
821+
822+
def __init__(
823+
self,
824+
mappings: Iterable[tuple[ObjectIdentifier, ObjectIdentifier]],
825+
) -> None:
826+
mappings = list(mappings)
827+
if not mappings:
828+
raise ValueError("mappings must be a non-empty list")
829+
if not all(
830+
isinstance(mapping, tuple)
831+
and len(mapping) == 2
832+
and all(isinstance(oid, ObjectIdentifier) for oid in mapping)
833+
for mapping in mappings
834+
):
835+
raise TypeError(
836+
"Every item in the mappings list must be a 2-tuple of "
837+
"ObjectIdentifier"
838+
)
839+
if any(
840+
CertificatePoliciesOID.ANY_POLICY in mapping
841+
for mapping in mappings
842+
):
843+
raise ValueError("Policy mappings must not contain anyPolicy")
844+
845+
self._mappings = mappings
846+
847+
__len__, __iter__, __getitem__ = _make_sequence_methods("_mappings")
848+
849+
def __repr__(self) -> str:
850+
return f"<PolicyMappings({self._mappings})>"
851+
852+
def __eq__(self, other: object) -> bool:
853+
if not isinstance(other, PolicyMappings):
854+
return NotImplemented
855+
856+
return self._mappings == other._mappings
857+
858+
def __hash__(self) -> int:
859+
return hash(tuple(self._mappings))
860+
861+
def public_bytes(self) -> bytes:
862+
return rust_x509.encode_extension_value(self)
863+
864+
818865
class CertificatePolicies(ExtensionType):
819866
oid = ExtensionOID.CERTIFICATE_POLICIES
820867

src/rust/cryptography-x509/src/extensions.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ pub struct PolicyConstraints {
8787
pub inhibit_policy_mapping: Option<u64>,
8888
}
8989

90+
#[derive(asn1::Asn1Read, asn1::Asn1Write)]
91+
pub struct PolicyMapping {
92+
pub issuer_domain_policy: asn1::ObjectIdentifier,
93+
pub subject_domain_policy: asn1::ObjectIdentifier,
94+
}
95+
96+
pub type PolicyMappings<'a, Op> =
97+
<Op as Asn1Operation>::SequenceOfVec<'a, PolicyMapping>;
98+
9099
#[derive(asn1::Asn1Read, asn1::Asn1Write)]
91100
pub struct AccessDescription<'a> {
92101
pub access_method: asn1::ObjectIdentifier,

src/rust/cryptography-x509/src/oid.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub const CERTIFICATE_ISSUER_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29,
3838
pub const NAME_CONSTRAINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 30);
3939
pub const CRL_DISTRIBUTION_POINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 31);
4040
pub const CERTIFICATE_POLICIES_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 32);
41+
pub const POLICY_MAPPINGS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 33);
4142
pub const AUTHORITY_KEY_IDENTIFIER_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 35);
4243
pub const POLICY_CONSTRAINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 36);
4344
pub const EXTENDED_KEY_USAGE_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 37);

src/rust/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ pub static INHIBIT_ANY_POLICY: LazyPyImport =
153153
pub static OCSP_NO_CHECK: LazyPyImport = LazyPyImport::new("cryptography.x509", &["OCSPNoCheck"]);
154154
pub static POLICY_CONSTRAINTS: LazyPyImport =
155155
LazyPyImport::new("cryptography.x509", &["PolicyConstraints"]);
156+
pub static POLICY_MAPPINGS: LazyPyImport =
157+
LazyPyImport::new("cryptography.x509", &["PolicyMappings"]);
156158
pub static CERTIFICATE_POLICIES: LazyPyImport =
157159
LazyPyImport::new("cryptography.x509", &["CertificatePolicies"]);
158160
pub static SUBJECT_INFORMATION_ACCESS: LazyPyImport =

src/rust/src/x509/certificate.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use cryptography_x509::extensions::{
1111
Admission, Admissions, AuthorityKeyIdentifier, BasicConstraints, DisplayText,
1212
DistributionPoint, DistributionPointName, DuplicateExtensionsError, ExtendedKeyUsage,
1313
Extension, IssuerAlternativeName, KeyUsage, MSCertificateTemplate, NameConstraints,
14-
NamingAuthority, PolicyConstraints, PolicyInformation, PolicyQualifierInfo,
14+
NamingAuthority, PolicyConstraints, PolicyInformation, PolicyMappings, PolicyQualifierInfo,
1515
PrivateKeyUsagePeriod, ProfessionInfo, Qualifier, RawExtensions, SequenceOfAccessDescriptions,
1616
SequenceOfSubtrees, SubjectAlternativeName, UserNotice,
1717
};
@@ -889,6 +889,19 @@ pub fn parse_cert_ext<'p>(
889889
pc.inhibit_policy_mapping,
890890
))?))
891891
}
892+
oid::POLICY_MAPPINGS_OID => {
893+
let mappings = ext.value::<PolicyMappings<'_, Asn1Read>>()?;
894+
let py_mappings = pyo3::types::PyList::empty(py);
895+
for mapping in mappings {
896+
py_mappings.append((
897+
oid_to_py_oid(py, &mapping.issuer_domain_policy)?,
898+
oid_to_py_oid(py, &mapping.subject_domain_policy)?,
899+
))?;
900+
}
901+
Ok(Some(
902+
types::POLICY_MAPPINGS.get(py)?.call1((py_mappings,))?,
903+
))
904+
}
892905
oid::OCSP_NO_CHECK_OID => {
893906
ext.value::<()>()?;
894907
Ok(Some(types::OCSP_NO_CHECK.get(py)?.call0()?))

src/rust/src/x509/extensions.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,18 @@ pub(crate) fn encode_extension(
595595
};
596596
Ok(Some(asn1::write_single(&pc)?))
597597
}
598+
&oid::POLICY_MAPPINGS_OID => {
599+
let mut mappings = vec![];
600+
for py_mapping in ext.try_iter()? {
601+
let py_mapping = py_mapping?;
602+
mappings.push(extensions::PolicyMapping {
603+
issuer_domain_policy: py_oid_to_oid(py_mapping.get_item(0)?)?,
604+
subject_domain_policy: py_oid_to_oid(py_mapping.get_item(1)?)?,
605+
});
606+
}
607+
let mappings = asn1::SequenceOfWriter::new(mappings);
608+
Ok(Some(asn1::write_single(&mappings)?))
609+
}
598610
&oid::NAME_CONSTRAINTS_OID => {
599611
let ka_bytes = cryptography_keepalive::KeepAlive::new();
600612
let ka_str = cryptography_keepalive::KeepAlive::new();

tests/x509/test_x509_ext.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3124,6 +3124,73 @@ def test_public_bytes(self):
31243124
assert ext.public_bytes() == b"\x30\x03\x81\x01\x00"
31253125

31263126

3127+
class TestPolicyMappings:
3128+
issuer_policy = ObjectIdentifier("1.2.3.4")
3129+
subject_policy = ObjectIdentifier("1.2.3.5")
3130+
mappings = [(issuer_policy, subject_policy)]
3131+
3132+
def test_invalid_mappings(self):
3133+
with pytest.raises(TypeError):
3134+
x509.PolicyMappings(
3135+
[(self.issuer_policy, typing.cast(typing.Any, "invalid"))]
3136+
)
3137+
with pytest.raises(TypeError):
3138+
x509.PolicyMappings(
3139+
[typing.cast(typing.Any, (self.issuer_policy,))]
3140+
)
3141+
3142+
def test_empty_mappings(self):
3143+
with pytest.raises(ValueError):
3144+
x509.PolicyMappings([])
3145+
3146+
@pytest.mark.parametrize("position", [0, 1])
3147+
def test_any_policy(self, position):
3148+
mapping = list(self.mappings[0])
3149+
mapping[position] = x509.CertificatePoliciesOID.ANY_POLICY
3150+
with pytest.raises(ValueError, match="must not contain anyPolicy"):
3151+
x509.PolicyMappings([tuple(mapping)]) # type: ignore[list-item]
3152+
3153+
def test_iter_len_index(self):
3154+
mappings = x509.PolicyMappings(iter(self.mappings))
3155+
assert len(mappings) == 1
3156+
assert list(mappings) == self.mappings
3157+
assert mappings[0] == self.mappings[0]
3158+
3159+
def test_repr(self):
3160+
assert repr(x509.PolicyMappings(self.mappings)) == (
3161+
"<PolicyMappings([(<ObjectIdentifier(oid=1.2.3.4, name=Unknown "
3162+
"OID)>, <ObjectIdentifier(oid=1.2.3.5, name=Unknown OID)>)])>"
3163+
)
3164+
3165+
def test_eq_hash(self):
3166+
mappings = x509.PolicyMappings(self.mappings)
3167+
mappings2 = x509.PolicyMappings(self.mappings)
3168+
mappings3 = x509.PolicyMappings(
3169+
[(self.subject_policy, self.issuer_policy)]
3170+
)
3171+
assert mappings == mappings2
3172+
assert mappings != mappings3
3173+
assert mappings != object()
3174+
assert hash(mappings) == hash(mappings2)
3175+
assert hash(mappings) != hash(mappings3)
3176+
3177+
def test_public_bytes(self):
3178+
mappings = x509.PolicyMappings(self.mappings)
3179+
assert mappings.public_bytes() == (
3180+
b"\x30\x0c\x30\x0a\x06\x03\x2a\x03\x04\x06\x03\x2a\x03\x05"
3181+
)
3182+
3183+
def test_certbuilder(self, rsa_key_2048: rsa.RSAPrivateKey):
3184+
cert = (
3185+
_make_certbuilder(rsa_key_2048)
3186+
.add_extension(x509.PolicyMappings(self.mappings), critical=True)
3187+
.sign(rsa_key_2048, hashes.SHA256())
3188+
)
3189+
ext = cert.extensions.get_extension_for_class(x509.PolicyMappings)
3190+
assert ext.critical is True
3191+
assert list(ext.value) == self.mappings
3192+
3193+
31273194
class TestAuthorityInformationAccess:
31283195
def test_invalid_descriptions(self):
31293196
with pytest.raises(TypeError):

0 commit comments

Comments
 (0)