diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7904e9e48c0c..92a625410a13 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,11 @@ Changelog .. note:: This version is not yet released and is under active development. +* Parsing a ``subjectAltName`` or ``issuerAltName`` extension now rejects an + empty ``GeneralNames`` sequence, matching the ``SIZE (1..MAX)`` constraint + RFC 5280 4.2.1.6/4.2.1.7 places on the field and the strictness already + applied to ``extendedKeyUsage``. + .. _v50-0-0: 50.0.0 - 2026-07-31 diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 192682aac64e..c67ad8582211 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -681,6 +681,10 @@ Custom X.509 Vectors This is an invalid certificate per CA/B 7.1.2.7.6. * ``empty-eku.pem`` - A leaf certificate containing an empty EKU extension. This is an invalid certificate per :rfc:`5280` 4.2.1.12. +* ``empty-san.pem`` - A leaf certificate containing an empty subjectAltName + extension. This is an invalid certificate per :rfc:`5280` 4.2.1.6. +* ``empty-ian.pem`` - A leaf certificate containing an empty issuerAltName + extension. This is an invalid certificate per :rfc:`5280` 4.2.1.7. * ``malformed-san.pem`` - A certificate with a malformed SAN. * ``malformed-ian.pem`` - A certificate with a malformed IAN. * ``admissions_extension_optional_data_not_provided.pem`` - diff --git a/src/rust/cryptography-x509-verification/src/lib.rs b/src/rust/cryptography-x509-verification/src/lib.rs index 110e9d8c1864..b449a570552f 100644 --- a/src/rust/cryptography-x509-verification/src/lib.rs +++ b/src/rust/cryptography-x509-verification/src/lib.rs @@ -144,7 +144,7 @@ impl Budget { struct NameChain<'a, 'chain> { child: Option<&'a NameChain<'a, 'chain>>, - sans: SubjectAlternativeName<'chain>, + sans: Option>, } impl<'a, 'chain> NameChain<'a, 'chain> { @@ -157,10 +157,8 @@ impl<'a, 'chain> NameChain<'a, 'chain> { self_issued_intermediate, extensions.get_extension(&SUBJECT_ALTERNATIVE_NAME_OID), ) { - (false, Some(sans)) => sans.value::>()?, - // TODO: there really ought to be a better way to express an empty - // `asn1::SequenceOf`. - _ => asn1::parse_single(b"\x30\x00")?, + (false, Some(sans)) => Some(sans.value::>()?), + _ => None, }; Ok(Self { child, sans }) @@ -253,7 +251,7 @@ impl<'a, 'chain> NameChain<'a, 'chain> { child.evaluate_constraints(constraints, budget)?; } - for san in self.sans.clone() { + for san in self.sans.iter().flat_map(|sans| sans.clone()) { // If there are no applicable constraints, the SAN is considered valid so the default is true. let mut permit = true; if let Some(permitted_subtrees) = &constraints.permitted_subtrees { diff --git a/src/rust/cryptography-x509/src/extensions.rs b/src/rust/cryptography-x509/src/extensions.rs index 8a9df2a16482..c5e93c5750f6 100644 --- a/src/rust/cryptography-x509/src/extensions.rs +++ b/src/rust/cryptography-x509/src/extensions.rs @@ -209,8 +209,8 @@ pub struct BasicConstraints { pub path_length: Option, } -pub type SubjectAlternativeName<'a> = asn1::SequenceOf<'a, name::GeneralName<'a>>; -pub type IssuerAlternativeName<'a> = asn1::SequenceOf<'a, name::GeneralName<'a>>; +pub type SubjectAlternativeName<'a> = asn1::SequenceOf<'a, name::GeneralName<'a>, 1>; +pub type IssuerAlternativeName<'a> = asn1::SequenceOf<'a, name::GeneralName<'a>, 1>; pub type ExtendedKeyUsage<'a> = asn1::SequenceOf<'a, asn1::ObjectIdentifier, 1>; pub struct KeyUsage<'a>(asn1::BitString<'a>); diff --git a/src/rust/src/x509/common.rs b/src/rust/src/x509/common.rs index 332ec046e625..c9a6a0e66934 100644 --- a/src/rust/src/x509/common.rs +++ b/src/rust/src/x509/common.rs @@ -336,9 +336,9 @@ pub(crate) fn parse_general_name<'p>( Ok(py_gn) } -pub(crate) fn parse_general_names<'a>( +pub(crate) fn parse_general_names<'a, const MINIMUM_LEN: usize>( py: pyo3::Python<'a>, - gn_seq: &asn1::SequenceOf<'a, GeneralName<'a>>, + gn_seq: &asn1::SequenceOf<'a, GeneralName<'a>, MINIMUM_LEN>, ) -> CryptographyResult> { let gns = pyo3::types::PyList::empty(py); for gn in gn_seq.clone() { diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index 19bb40152821..d8344d4eef50 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -6419,6 +6419,26 @@ def test_invalid_empty_eku(self): with pytest.raises(ValueError, match="InvalidSize"): cert.extensions.get_extension_for_class(ExtendedKeyUsage) + def test_invalid_empty_subject_alternative_name(self): + cert = _load_cert( + os.path.join("x509", "custom", "empty-san.pem"), + x509.load_pem_x509_certificate, + ) + + with pytest.raises(ValueError, match="InvalidSize"): + cert.extensions.get_extension_for_class( + x509.SubjectAlternativeName + ) + + def test_invalid_empty_issuer_alternative_name(self): + cert = _load_cert( + os.path.join("x509", "custom", "empty-ian.pem"), + x509.load_pem_x509_certificate, + ) + + with pytest.raises(ValueError, match="InvalidSize"): + cert.extensions.get_extension_for_class(x509.IssuerAlternativeName) + class TestNameAttribute: EXPECTED_TYPES: typing.ClassVar[ diff --git a/vectors/cryptography_vectors/x509/custom/empty-ian.pem b/vectors/cryptography_vectors/x509/custom/empty-ian.pem new file mode 100644 index 000000000000..1ccb20237dc4 --- /dev/null +++ b/vectors/cryptography_vectors/x509/custom/empty-ian.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBMjCB2aADAgECAgQHW80VMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD2NyeXB0 +b2dyYXBoeS5pbzAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBoxGDAW +BgNVBAMMD2NyeXB0b2dyYXBoeS5pbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BBB0GLaeloeWg3Gco3XAypqHZq48UdTm60oI6KfKH6+dtY/7EKFEJaYNj2tQfMS8 ++EwNn3y2nUacAFdPweu0O92jDTALMAkGA1UdEgQCMAAwCgYIKoZIzj0EAwIDSAAw +RQIhAIv3h7z+YD321gf/jnz2FhqcHZ+MZg7+Lhss7ym6Pt+0AiAYYE5CjExH/QTd +muKQ8B78WqZE4glTIkKqN6050Ltokg== +-----END CERTIFICATE----- diff --git a/vectors/cryptography_vectors/x509/custom/empty-san.pem b/vectors/cryptography_vectors/x509/custom/empty-san.pem new file mode 100644 index 000000000000..36e547b4e505 --- /dev/null +++ b/vectors/cryptography_vectors/x509/custom/empty-san.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBMjCB2aADAgECAgQHW80VMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD2NyeXB0 +b2dyYXBoeS5pbzAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBoxGDAW +BgNVBAMMD2NyeXB0b2dyYXBoeS5pbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BBB0GLaeloeWg3Gco3XAypqHZq48UdTm60oI6KfKH6+dtY/7EKFEJaYNj2tQfMS8 ++EwNn3y2nUacAFdPweu0O92jDTALMAkGA1UdEQQCMAAwCgYIKoZIzj0EAwIDSAAw +RQIgFtbOPouE/CuyKQ+TuGil2iOk2yq8ISLKjegKpMlRTV4CIQDQ5zSTl1p5nyoc +B7o2lMSPf+6L8FVow8ASh1sb9rS18g== +-----END CERTIFICATE-----