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

* feat: implement parsing of admissions extension

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* chore: add tests for admissions extension parsing

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* chore: use cryptography result return type

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* chore: apply fixes done by cargo fmt and clippy

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* add gematik company name and the gmbh abbreviations to known words

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* fix: regenerate the synthetic certificate with additional admission covering the case of naming authority with no data

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* fix: parse none for profession_oids if profession_oids is none

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* chore: apply formatting to changes in rust codebase

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>

* refactor: switch return type of parse_profession_infos from PyObject to Bound<PyAny>

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* refactor: switch return type of parse_naming_authority from PyObject to Bound<PyAny>

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* refactor: switch return type of parse_admissions from PyObject to Bound<PyAny>

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* chore: remove gematik certs from repo

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* chore: remove gematik certs from this pr

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* chore: extend parser tests with an additional synthetic certificate to complete rust coverage

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* chore: add description for the additional certificate without authority

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* use into_bound(py) as shortcut, refrain from using to_object() in all added functions

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* add better description for the admissions synthetic cert

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

* adjust description to avoid using misspelled words

Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>

---------

Signed-off-by: oleg.hoefling <oleg.hoefling@gmail.com>
Signed-off-by: Oleg Hoefling <oleg.hoefling@gmail.com>
This commit is contained in:
Oleg Höfling 2024-11-11 02:06:01 +01:00 committed by GitHub
parent 78e89e4975
commit fef127093b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 316 additions and 5 deletions

View file

@ -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).
* ``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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -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"]);

View file

@ -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};
@ -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<'_>,
@ -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),
}
}

View file

@ -1861,6 +1861,138 @@ class TestRSACertificate:
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(

View file

@ -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-----

View file

@ -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-----