Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admissions): implement parsing of admissions extension #11903

Merged
merged 18 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3ad9245
feat: implement parsing of admissions extension
hoefling Nov 4, 2024
2991681
chore: add tests for admissions extension parsing
hoefling Nov 6, 2024
454b46c
chore: use cryptography result return type
hoefling Nov 6, 2024
78da2c8
chore: apply fixes done by cargo fmt and clippy
hoefling Nov 6, 2024
d8c5632
add gematik company name and the gmbh abbreviations to known words
hoefling Nov 6, 2024
276f543
fix: regenerate the synthetic certificate with additional admission c…
hoefling Nov 6, 2024
1a79a74
fix: parse none for profession_oids if profession_oids is none
hoefling Nov 6, 2024
0d97a6c
chore: apply formatting to changes in rust codebase
hoefling Nov 6, 2024
3a61d50
refactor: switch return type of parse_profession_infos from PyObject …
hoefling Nov 8, 2024
2102458
refactor: switch return type of parse_naming_authority from PyObject …
hoefling Nov 9, 2024
39819dc
refactor: switch return type of parse_admissions from PyObject to Bou…
hoefling Nov 9, 2024
42b21e2
chore: remove gematik certs from repo
hoefling Nov 9, 2024
4ef7e87
chore: remove gematik certs from this pr
hoefling Nov 9, 2024
20f74b9
chore: extend parser tests with an additional synthetic certificate t…
hoefling Nov 9, 2024
7f39af5
chore: add description for the additional certificate without authority
hoefling Nov 9, 2024
1fbd230
use into_bound(py) as shortcut, refrain from using to_object() in all…
hoefling Nov 10, 2024
20096f1
add better description for the admissions synthetic cert
hoefling Nov 10, 2024
f4cdefd
adjust description to avoid using misspelled words
hoefling Nov 10, 2024
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
10 changes: 10 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,16 @@ Custom X.509 Vectors
This is an invalid certificate per :rfc:`5280` 4.2.1.12.
* ``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`` -
A certificate containing the ``Admissions`` extension with multiple admissions,
signed by ``x509/custom/ca/rsa_ca.pem`` CA. The admissions in this certificate
are prepared using synthetic data to verify the possible corner cases are handled
by the parser correctly (an admission missing naming authority or admission
authority, a profession info missing naming authority or profession OIDs
or the registration number etc).
Comment on lines +549 to +555
Copy link
Member

Choose a reason for hiding this comment

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

This is fine (and won't block merging), but FYI for the future if you contribute again (which I hope you do!), our usual approach would be: "The admissions in this certificate have every field filled in with all the available options."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alex I see - but I did also cover the case of all available fields fille out, too! The extension in the certificate contains five admissions in total, first one has all fields filled out, the remaining four test the corner cases. Either I still didn't a good job at describing the certificate then, will amend this. Or I didn't do the testing part properly - but wasn't sure how to test all the corner cases with missing fields, so I put every corner case in two certificates 🙈

* ``admissions_extension_authority_not_provided.pem`` - A certificate containing
the ``Admissions`` extension with no admissions and no admission authority,
signed by ``x509/custom/ca/rsa_ca.pem`` CA.

Custom X.509 Request Vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
6 changes: 6 additions & 0 deletions src/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ pub static CERTIFICATE_VERSION_V1: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["Version", "v1"]);
pub static CERTIFICATE_VERSION_V3: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["Version", "v3"]);
pub static ADMISSION: LazyPyImport = LazyPyImport::new("cryptography.x509", &["Admission"]);
pub static NAMING_AUTHORITY: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["NamingAuthority"]);
pub static PROFESSION_INFO: LazyPyImport =
LazyPyImport::new("cryptography.x509", &["ProfessionInfo"]);
pub static ADMISSIONS: LazyPyImport = LazyPyImport::new("cryptography.x509", &["Admissions"]);

pub static CRL_REASON_FLAGS: LazyPyImport =
LazyPyImport::new("cryptography.x509.extensions", &["_CRLREASONFLAGS"]);
Expand Down
118 changes: 113 additions & 5 deletions src/rust/src/x509/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use std::hash::{Hash, Hasher};
use cryptography_x509::certificate::Certificate as RawCertificate;
use cryptography_x509::common::{AlgorithmParameters, Asn1ReadableOrWritable};
use cryptography_x509::extensions::{
AuthorityKeyIdentifier, BasicConstraints, DisplayText, DistributionPoint,
DistributionPointName, DuplicateExtensionsError, ExtendedKeyUsage, IssuerAlternativeName,
KeyUsage, MSCertificateTemplate, NameConstraints, PolicyConstraints, PolicyInformation,
PolicyQualifierInfo, Qualifier, RawExtensions, SequenceOfAccessDescriptions,
SequenceOfSubtrees, UserNotice,
Admission, Admissions, AuthorityKeyIdentifier, BasicConstraints, DisplayText,
DistributionPoint, DistributionPointName, DuplicateExtensionsError, ExtendedKeyUsage,
IssuerAlternativeName, KeyUsage, MSCertificateTemplate, NameConstraints, NamingAuthority,
PolicyConstraints, PolicyInformation, PolicyQualifierInfo, ProfessionInfo, Qualifier,
RawExtensions, SequenceOfAccessDescriptions, SequenceOfSubtrees, UserNotice,
};
use cryptography_x509::extensions::{Extension, SubjectAlternativeName};
use cryptography_x509::{common, oid};
Expand Down Expand Up @@ -731,6 +731,100 @@ pub(crate) fn parse_access_descriptions(
Ok(ads.to_object(py))
}

fn parse_naming_authority<'p>(
py: pyo3::Python<'p>,
authority: NamingAuthority<'p>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::PyAny>> {
let py_id = match &authority.id {
Some(data) => oid_to_py_oid(py, data)?,
None => py.None().into_bound(py),
};
let py_url = match authority.url {
Some(data) => pyo3::types::PyString::new_bound(py, data.as_str()).into_any(),
None => py.None().into_bound(py),
};
let py_text = match authority.text {
Some(data) => parse_display_text(py, data)?,
None => py.None(),
};

Ok(types::NAMING_AUTHORITY
.get(py)?
.call1((py_id, py_url, py_text))?)
}

fn parse_profession_infos<'a>(
py: pyo3::Python<'a>,
profession_infos: &asn1::SequenceOf<'a, ProfessionInfo<'a>>,
) -> CryptographyResult<pyo3::Bound<'a, pyo3::PyAny>> {
let py_infos = pyo3::types::PyList::empty_bound(py);
for info in profession_infos.clone() {
let py_naming_authority = match info.naming_authority {
Some(data) => parse_naming_authority(py, data)?,
None => py.None().into_bound(py),
};
let py_profession_items = pyo3::types::PyList::empty_bound(py);
for item in info.profession_items.unwrap_read().clone() {
let py_item = parse_display_text(py, item)?;
py_profession_items.append(py_item)?;
}
let py_profession_oids = match info.profession_oids {
Some(oids) => {
let py_oids = pyo3::types::PyList::empty_bound(py);
for oid in oids.unwrap_read().clone() {
let py_oid = oid_to_py_oid(py, &oid)?;
py_oids.append(py_oid)?;
}
py_oids.into_any()
}
None => py.None().into_bound(py),
};
let py_registration_number = match info.registration_number {
Some(data) => pyo3::types::PyString::new_bound(py, data.as_str()).into_any(),
None => py.None().into_bound(py),
};
let py_add_profession_info = match info.add_profession_info {
Some(data) => pyo3::types::PyBytes::new_bound(py, data).into_any(),
None => py.None().into_bound(py),
};
let py_info = types::PROFESSION_INFO.get(py)?.call1((
py_naming_authority,
py_profession_items,
py_profession_oids,
py_registration_number,
py_add_profession_info,
))?;
py_infos.append(py_info)?;
}
Ok(py_infos.into_any())
}

fn parse_admissions<'a>(
py: pyo3::Python<'a>,
admissions: &asn1::SequenceOf<'a, Admission<'a>>,
) -> CryptographyResult<pyo3::Bound<'a, pyo3::PyAny>> {
let py_admissions = pyo3::types::PyList::empty_bound(py);
for admission in admissions.clone() {
let py_admission_authority = match admission.admission_authority {
Some(authority) => x509::parse_general_name(py, authority)?,
None => py.None(),
};
let py_naming_authority = match admission.naming_authority {
Some(data) => parse_naming_authority(py, data)?,
None => py.None().into_bound(py),
};
let py_infos = parse_profession_infos(py, admission.profession_infos.unwrap_read())?;

let py_entry = types::ADMISSION.get(py)?.call1((
py_admission_authority,
py_naming_authority,
py_infos,
))?;
py_admissions.append(py_entry)?;
}
Ok(py_admissions.into_any())
}

pub fn parse_cert_ext<'p>(
py: pyo3::Python<'p>,
ext: &Extension<'_>,
Expand Down Expand Up @@ -869,6 +963,20 @@ pub fn parse_cert_ext<'p>(
ms_cert_tpl.minor_version,
))?))
}
oid::ADMISSIONS_OID => {
let admissions = ext.value::<Admissions<'_>>()?;
let admission_authority = match admissions.admission_authority {
Some(authority) => x509::parse_general_name(py, authority)?,
None => py.None(),
};
let py_admissions =
parse_admissions(py, admissions.contents_of_admissions.unwrap_read())?;
Ok(Some(
types::ADMISSIONS
.get(py)?
.call1((admission_authority, py_admissions))?,
))
}
_ => Ok(None),
}
}
Expand Down
132 changes: 132 additions & 0 deletions tests/x509/test_x509.py
Original file line number Diff line number Diff line change
Expand Up @@ -1861,6 +1861,138 @@ def test_verify_directly_issued_by_unsupported_key_type(self, backend):
with pytest.raises(TypeError):
cert.verify_directly_issued_by(leaf)

def test_admissions_extension(self, backend):
cert = _load_cert(
os.path.join(
"x509",
"custom",
"admissions_extension_optional_data_not_provided.pem",
),
x509.load_pem_x509_certificate,
)
ext = cert.extensions.get_extension_for_class(x509.Admissions)
assert ext.value == x509.Admissions(
authority=x509.DirectoryName(
value=x509.Name(
[
x509.NameAttribute(
oid=x509.NameOID.COUNTRY_NAME, value="DE"
),
x509.NameAttribute(
oid=x509.NameOID.ORGANIZATION_NAME,
value="Elektronisches Gesundheitsberuferegister",
),
]
)
),
admissions=[
x509.Admission(
admission_authority=x509.RegisteredID(
value=x509.NameOID.ORGANIZATION_NAME
),
naming_authority=x509.NamingAuthority(
id=x509.ObjectIdentifier("1.2.276.0.76.4.223"),
url="",
text="Betriebsstätte GKV-Spitzenverband",
),
profession_infos=[
x509.ProfessionInfo(
naming_authority=x509.NamingAuthority(
id=x509.ObjectIdentifier("1.2.276.0.76.4.225"),
url="https://example.com",
text=(
"Betriebsstätte Deutscher "
"Apothekerverband"
),
),
profession_items=["Ã\x84rztin/Arzt", ""],
profession_oids=[
x509.ObjectIdentifier("1.2.276.0.76.4.30"),
x509.ObjectIdentifier("1.2.276.0.76.4.31"),
],
registration_number="9-999/99999999",
add_profession_info=(
b'\x16"additional profession info example'
),
)
],
),
x509.Admission(
admission_authority=x509.OtherName(
type_id=x509.NameOID.COUNTRY_NAME,
value=b"\x04\x04\x13\x02DE",
),
naming_authority=None,
profession_infos=[
x509.ProfessionInfo(
naming_authority=x509.NamingAuthority(
id=x509.ObjectIdentifier("1.2.276.0.76.4.227"),
url=None,
text=(
"Betriebsstätte der Deutsche Krankenhaus "
"TrustCenter und Informationsverarbeitung "
"GmbH"
),
),
profession_items=["Krankenhaus"],
profession_oids=[
x509.ObjectIdentifier("1.2.276.0.76.4.53"),
x509.ObjectIdentifier("1.2.276.0.76.4.246"),
],
registration_number="9.9.9-99999999",
add_profession_info=None,
),
x509.ProfessionInfo(
naming_authority=None,
profession_items=[
"Krankenhaus",
"Betriebsstätte Geburtshilfe",
],
profession_oids=[
x509.ObjectIdentifier("1.2.276.0.76.4.53")
],
registration_number="",
add_profession_info=None,
),
],
),
x509.Admission(
admission_authority=None,
naming_authority=None,
profession_infos=[
x509.ProfessionInfo(
naming_authority=None,
profession_items=[],
profession_oids=None,
registration_number=None,
add_profession_info=None,
)
],
),
x509.Admission(
admission_authority=None,
naming_authority=x509.NamingAuthority(None, None, None),
profession_infos=[],
),
x509.Admission(
admission_authority=None,
naming_authority=None,
profession_infos=[],
),
],
)

cert = _load_cert(
os.path.join(
"x509",
"custom",
"admissions_extension_authority_not_provided.pem",
),
x509.load_pem_x509_certificate,
)
ext = cert.extensions.get_extension_for_class(x509.Admissions)
assert ext.value == x509.Admissions(authority=None, admissions=[])


class TestRSACertificateRequest:
@pytest.mark.parametrize(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDiTCCAy+gAwIBAgIUDuURI/KxJjJlnU/YDGmX0V0DyNQwCgYIKoZIzj0EAwIw
JzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAeFw0yNDEx
MDkxMzI4MjVaFw0yNDEyMDkxMzI4MjVaMCkxCzAJBgNVBAYTAlVTMRowGAYDVQQD
DBFjcnlwdG9ncmFwaHkgdGVzdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBANBIheRc1HT4MzV5GvUbDk9CFU6DTomRApNqRmizriRqm6OY4Ht3d71BXog6
/IBkqAnZ4/XJQ40G4sVDb52k11oPvfJ/F5pc+6UqPBL+QGzYGkJoubAqXFpI6ow0
qayFNQLv0T9o4yh0QQOoGvgCmv91qmitLrZNXu4U9S76G+DiGST+QyMkMxj+VsGR
sRRBufV1urcnvFWjU6Q2+cr2cp0mMAG96NTyIskYiJ8vL03Wz4DX4klO4X47fPmD
nU/OMn4SbvMZ896j1L0J04S+uVThTkxQWcFcqXhX5qM8kzcjJUmybFlbf150j3Wi
ucW48K/j7fJ0x9q3iUo4Gva0coScglJWcgo/BBCwFDw8NVba7npxSRMiaS3qTv0d
EFcRnvByc+7hyGxxlWdTE9tHisUI1eZVk9P9ziqNOZKscY8ZX1+/C4M9X69Y7A8I
74F5dO27IRycEgOrSo2z1NhfSwbqJr9a2TBtRsFinn8rjKBIzNn0E5p9jO1Wjxtk
cjHfXXpLN8FFMvoYI9l/K+ZWDm9sboaF8jrgozSc004AFemAH79mmCGVRKXn1vDA
o4DLC6p3NiBFYQcYbW9V+beGD6srsF6xJtuY/UwtPROLWSzuCCrZ/4BlmpNsR0eh
IFFvzEKjX6rR2yp3YKlguDbMBMKMpfSGxAFwcZ7OiaxR20UHAgMBAAGjbDBqMA0G
BSskCAMDBAQwAjAAMB0GA1UdDgQWBBTWrADzmGKoPZIVNf6QvnOYMOtMhDA6BgNV
HSMEMzAxoSukKTAnMQswCQYDVQQGEwJVUzEYMBYGA1UEAwwPY3J5cHRvZ3JhcGh5
IENBggIDCTAKBggqhkjOPQQDAgNIADBFAiAnRuoEuL/8c/B3Cb89FOSMlV/sX1QW
MXM8X69xVWxyjAIhAIuZ8HI2TUtuTOGascFW46AjkPfwCggknB7kkq86QOn3
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF1zCCBXygAwIBAgIUckdGKz+upx7gGI/r6y1UvvQQFKowCgYIKoZIzj0EAwIw
JzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2NyeXB0b2dyYXBoeSBDQTAeFw0yNDEx
MDkxMzI0NTlaFw0yNDEyMDkxMzI0NTlaMCkxCzAJBgNVBAYTAlVTMRowGAYDVQQD
DBFjcnlwdG9ncmFwaHkgdGVzdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBANBIheRc1HT4MzV5GvUbDk9CFU6DTomRApNqRmizriRqm6OY4Ht3d71BXog6
/IBkqAnZ4/XJQ40G4sVDb52k11oPvfJ/F5pc+6UqPBL+QGzYGkJoubAqXFpI6ow0
qayFNQLv0T9o4yh0QQOoGvgCmv91qmitLrZNXu4U9S76G+DiGST+QyMkMxj+VsGR
sRRBufV1urcnvFWjU6Q2+cr2cp0mMAG96NTyIskYiJ8vL03Wz4DX4klO4X47fPmD
nU/OMn4SbvMZ896j1L0J04S+uVThTkxQWcFcqXhX5qM8kzcjJUmybFlbf150j3Wi
ucW48K/j7fJ0x9q3iUo4Gva0coScglJWcgo/BBCwFDw8NVba7npxSRMiaS3qTv0d
EFcRnvByc+7hyGxxlWdTE9tHisUI1eZVk9P9ziqNOZKscY8ZX1+/C4M9X69Y7A8I
74F5dO27IRycEgOrSo2z1NhfSwbqJr9a2TBtRsFinn8rjKBIzNn0E5p9jO1Wjxtk
cjHfXXpLN8FFMvoYI9l/K+ZWDm9sboaF8jrgozSc004AFemAH79mmCGVRKXn1vDA
o4DLC6p3NiBFYQcYbW9V+beGD6srsF6xJtuY/UwtPROLWSzuCCrZ/4BlmpNsR0eh
IFFvzEKjX6rR2yp3YKlguDbMBMKMpfSGxAFwcZ7OiaxR20UHAgMBAAGjggK3MIIC
szCCAlQGBSskCAMDBIICSTCCAkWkQjBAMQswCQYDVQQGEwJERTExMC8GA1UECgwo
RWxla3Ryb25pc2NoZXMgR2VzdW5kaGVpdHNiZXJ1ZmVyZWdpc3RlcjCCAf0wgfKg
BYgDVQQKoTQwMgYIKoIUAEwEgV8WAAwkQmV0cmllYnNzdMODwqR0dGUgR0tWLVNw
aXR6ZW52ZXJiYW5kMIGyMIGvoE8wTQYIKoIUAEwEgWEWE2h0dHBzOi8vZXhhbXBs
ZS5jb20MLEJldHJpZWJzc3TDg8KkdHRlIERldXRzY2hlciBBcG90aGVrZXJ2ZXJi
YW5kMBIMDsODwoRyenRpbi9Bcnp0DAAwEgYHKoIUAEwEHgYHKoIUAEwEHxMOOS05
OTkvOTk5OTk5OTkEJBYiYWRkaXRpb25hbCBwcm9mZXNzaW9uIGluZm8gZXhhbXBs
ZTCB8aAPoA0GA1UEBqAGBAQTAkRFMIHdMIGcoGYwZAYIKoIUAEwEgWMMWEJldHJp
ZWJzc3TDg8KkdHRlIGRlciBEZXV0c2NoZSBLcmFua2VuaGF1cyBUcnVzdENlbnRl
ciB1bmQgSW5mb3JtYXRpb25zdmVyYXJiZWl0dW5nIEdtYkgwDQwLS3Jhbmtlbmhh
dXMwEwYHKoIUAEwENQYIKoIUAEwEgXYTDjkuOS45LTk5OTk5OTk5MDwwLQwLS3Jh
bmtlbmhhdXMMHkJldHJpZWJzc3TDg8KkdHRlIEdlYnVydHNoaWxmZTAJBgcqghQA
TAQ1EwAwBjAEMAIwADAGoQIwADAAMAIwADAdBgNVHQ4EFgQU1qwA85hiqD2SFTX+
kL5zmDDrTIQwOgYDVR0jBDMwMaErpCkwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMM
D2NyeXB0b2dyYXBoeSBDQYICAwkwCgYIKoZIzj0EAwIDSQAwRgIhAMz8iUp3Tj0W
3mMOPIyNyQ6ZwydHCX199oH5j0opH+4GAiEAyOF2Mw4H6xDOfsEa2NvnpO4mt8Pa
y7msciyCxhMgUZY=
-----END CERTIFICATE-----