Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`` -
Expand Down
11 changes: 11 additions & 0 deletions src/rust/src/x509/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,17 @@ pub(crate) fn parse_general_names<'a>(
py: pyo3::Python<'a>,
gn_seq: &asn1::SequenceOf<'a, GeneralName<'a>>,
) -> CryptographyResult<pyo3::Bound<'a, pyo3::PyAny>> {
// GeneralNames is defined as SEQUENCE SIZE (1..MAX) OF GeneralName in
// RFC 5280, so an empty sequence is malformed. The path validation engine
// represents a certificate with no subjectAltName using its own empty
// sequence and does not call this.
if gn_seq.clone().next().is_none() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SequenceOf takes a MINIMUM_LEN, is there a reason not to just set that to 1?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i looked at that first, but the SubjectAlternativeName and IssuerAlternativeName aliases are shared with the path validation engine, which stands in for a certificate with no SAN by parsing an empty sequence into SubjectAlternativeName (the \x30\x00 parse in cryptography-x509-verification/src/lib.rs). setting MINIMUM_LEN to 1 on the alias would make that parse fail. keeping the check in parse_general_names also means the other GeneralNames users that go through it, authorityCertIssuer and the CRL distribution point names, pick up the same 1..MAX constraint, since 5280 defines them all the same way. happy to move it to a type-level constraint if you'd rather give verification its own zero-minimum type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I think we should probably fix the \x30\x00 callsites to just store an Option<SequenceOf<>>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. set MINIMUM_LEN to 1 on both aliases and made the verification NameChain store an Option instead of parsing the empty \x30\x00 sequence, so the empty rejection now comes from the type. parse_general_names is generic over the minimum length now so the len-0 GeneralNames callers still go through unchanged. rust builds clean.

return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"GeneralNames must contain at least one GeneralName",
),
));
}
let gns = pyo3::types::PyList::empty(py);
for gn in gn_seq.clone() {
let py_gn = parse_general_name(py, gn)?;
Expand Down
20 changes: 20 additions & 0 deletions tests/x509/test_x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="at least one GeneralName"):
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="at least one GeneralName"):
cert.extensions.get_extension_for_class(x509.IssuerAlternativeName)


class TestNameAttribute:
EXPECTED_TYPES: typing.ClassVar[
Expand Down
9 changes: 9 additions & 0 deletions vectors/cryptography_vectors/x509/custom/empty-ian.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBMjCB2aADAgECAgQHW80VMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD2NyeXB0
b2dyYXBoeS5pbzAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBoxGDAW
BgNVBAMMD2NyeXB0b2dyYXBoeS5pbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BBB0GLaeloeWg3Gco3XAypqHZq48UdTm60oI6KfKH6+dtY/7EKFEJaYNj2tQfMS8
+EwNn3y2nUacAFdPweu0O92jDTALMAkGA1UdEgQCMAAwCgYIKoZIzj0EAwIDSAAw
RQIhAIv3h7z+YD321gf/jnz2FhqcHZ+MZg7+Lhss7ym6Pt+0AiAYYE5CjExH/QTd
muKQ8B78WqZE4glTIkKqN6050Ltokg==
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions vectors/cryptography_vectors/x509/custom/empty-san.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBMjCB2aADAgECAgQHW80VMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMMD2NyeXB0
b2dyYXBoeS5pbzAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBoxGDAW
BgNVBAMMD2NyeXB0b2dyYXBoeS5pbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BBB0GLaeloeWg3Gco3XAypqHZq48UdTm60oI6KfKH6+dtY/7EKFEJaYNj2tQfMS8
+EwNn3y2nUacAFdPweu0O92jDTALMAkGA1UdEQQCMAAwCgYIKoZIzj0EAwIDSAAw
RQIgFtbOPouE/CuyKQ+TuGil2iOk2yq8ISLKjegKpMlRTV4CIQDQ5zSTl1p5nyoc
B7o2lMSPf+6L8FVow8ASh1sb9rS18g==
-----END CERTIFICATE-----
Loading