diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 9c0c2f4d3..b724b6a18 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -64,6 +64,7 @@ jobs: python -m pip install -e . python -m pip install "Pillow>=10.0.0,<10.1" "device_detector>=5.0,<6" "satosa>=8.4,<8.6" "jinja2>=3.0,<4" "pymongo>=4.4.1,<4.5" aiohttp python -m pip install git+https://github.com/peppelinux/pyMDOC-CBOR.git + python -m pip list - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/pyeudiw/tests/satosa/backends/openid4vp/test_vp_mdoc_cbor.py b/pyeudiw/tests/satosa/backends/openid4vp/test_vp_mdoc_cbor.py index 22a0f4bcc..fdae3d8d3 100644 --- a/pyeudiw/tests/satosa/backends/openid4vp/test_vp_mdoc_cbor.py +++ b/pyeudiw/tests/satosa/backends/openid4vp/test_vp_mdoc_cbor.py @@ -10,6 +10,7 @@ from pyeudiw.storage.db_engine import DBEngine from pyeudiw.trust.dynamic import CombinedTrustEvaluator from pyeudiw.x509.chain_builder import ChainBuilder +from datetime import datetime, timedelta, timezone def base64url_to_int(val): @@ -43,32 +44,35 @@ def base64url_to_int(val): chain = ChainBuilder() chain.gen_certificate( cn="ca.example.com", - org_name="Example CA", + organization_name="Example CA", country_name="IT", dns="ca.example.com", uri="https://ca.example.com", crl_distr_point="http://ca.example.com/crl.pem", ca=True, path_length=1, + email_address="info@ca.example.com", ) chain.gen_certificate( cn="intermediate.example.com", - org_name="Example Intermediate", + organization_name="Example Intermediate", country_name="IT", dns="intermediate.example.com", uri="https://intermediate.example.com", ca=True, path_length=0, + email_address="info@intermediate.example.com", ) chain.gen_certificate( cn="example.com", - org_name="Example Leaf", + organization_name="Example Leaf", country_name="IT", dns="example.com", uri="https://example.com", private_key=private_key, ca=False, path_length=None, + email_address="info@example.com", ) chain_der = chain.get_chain("DER") diff --git a/pyeudiw/tests/satosa/test_backend.py b/pyeudiw/tests/satosa/test_backend.py index f719b71a0..1a468a90b 100644 --- a/pyeudiw/tests/satosa/test_backend.py +++ b/pyeudiw/tests/satosa/test_backend.py @@ -52,6 +52,7 @@ from pyeudiw.tools.utils import exp_from_now, iat_now from pyeudiw.trust.model.trust_source import TrustSourceData, TrustEvaluationType from pyeudiw.x509.verify import PEM_cert_to_B64DER_cert, to_pem_list +from datetime import datetime, timezone, timedelta PKEY = { 'KTY': 'EC2', diff --git a/pyeudiw/tests/trust/handler/test_x509.py b/pyeudiw/tests/trust/handler/test_x509.py index d006ebaba..ae19dadc2 100644 --- a/pyeudiw/tests/trust/handler/test_x509.py +++ b/pyeudiw/tests/trust/handler/test_x509.py @@ -96,32 +96,35 @@ def test_chain_crl_passing(): chain = ChainBuilder() chain.gen_certificate( cn="ca.example.com", - org_name="Example CA", + organization_name="Example CA", country_name="IT", dns="ca.example.com", uri="https://ca.example.com", crl_distr_point="http://ca.example.com/crl.pem", ca=True, path_length=1, + email_address="info@ca.example.com", ) chain.gen_certificate( cn="intermediate.example.com", - org_name="Example Intermediate", + organization_name="Example Intermediate", country_name="IT", dns="intermediate.example.com", uri="https://intermediate.example.com", ca=True, path_length=0, + email_address="info@intermediate.example.com", ) chain.gen_certificate( cn="example.com", - org_name="Example Leaf", + organization_name="Example Leaf", country_name="IT", dns="example.com", uri="https://example.com", private_key=DEFAULT_X509_LEAF_PRIVATE_KEY, ca=False, path_length=None, + email_address="info@example.com", ) chain = chain.get_chain("DER") @@ -179,7 +182,7 @@ def test_chain_crl_fail(): chain = ChainBuilder() chain.gen_certificate( cn="ca.example.com", - org_name="Example CA", + organization_name="Example CA", country_name="IT", dns="ca.example.com", uri="https://ca.example.com", @@ -187,26 +190,29 @@ def test_chain_crl_fail(): private_key=ca_key, ca=True, path_length=1, + email_address="info@ca.example.com", ) chain.gen_certificate( cn="intermediate.example.com", - org_name="Example Intermediate", + organization_name="Example Intermediate", country_name="IT", dns="intermediate.example.com", uri="https://intermediate.example.com", ca=True, path_length=0, serial_number=44442, + email_address="info@intermediate.example.com", ) chain.gen_certificate( cn="example.com", - org_name="Example Leaf", + organization_name="Example Leaf", country_name="IT", dns="example.com", uri="https://example.com", private_key=DEFAULT_X509_LEAF_PRIVATE_KEY, ca=False, path_length=None, + email_address="info@example.com", ) chain = chain.get_chain("DER") diff --git a/pyeudiw/tests/x509/test_x509.py b/pyeudiw/tests/x509/test_x509.py index 7212312fb..9ab91b237 100755 --- a/pyeudiw/tests/x509/test_x509.py +++ b/pyeudiw/tests/x509/test_x509.py @@ -1,5 +1,6 @@ from typing import Any from datetime import datetime +from ipaddress import IPv4Address, IPv4Network from ssl import DER_cert_to_PEM_cert from pyeudiw.x509.chain_builder import ChainBuilder from pyeudiw.x509.verify import ( @@ -12,27 +13,37 @@ def gen_chain( date: datetime| None = None, - ca_cn: str = "CN=ca.example.com, O=Example CA, C=IT", + ca_cn: str = "ca.example.com", ca_dns: str = "ca.example.com", - leaf_cn: str = "CN=leaf.example.com, O=Example Leaf, C=IT", - leaf_dns: str = "leaf.example.org", - leaf_uri: str = "leaf.example.org", + intermediate_cn: str = "intermediate.example.org", + intermediate_dns: str = "intermediate.example.org", + leaf_cn: str = "leaf.example.it", + leaf_dns: str = "leaf.example.it", + leaf_uri: str = "leaf.example.it", leaf_private_key: Any = None ) -> list[bytes]: ca_cert_params = { "cn": ca_cn, - "org_name":"Example CA", - "country_name":"IT", - "dns":ca_dns, - "uri":"https://ca.example.com", - "crl_distr_point":"http://ca.example.com/crl.pem", + "organization_name": "Example CA", + "country_name": "IT", + "email_address": f"info@{ca_dns}", + "dns": ca_dns, + "uri": f"https://{ca_dns}", + "crl_distr_point": f"https://{ca_dns}/crl/{ca_dns}.crl", "ca": True, - "path_length": 1, + "path_length": None, + # since the CA should not know a priori leave's dns names and allow intermediates to know that + # when the CA issues the certificate to a intermediate, it would not put + # subtree constraints to intermediate dns name to prevent constraints validation failures + # "permitted_subtrees": [ + # x509.DNSName(ca_dns), + # x509.DNSName(intermediate_dns), + # ], "excluded_subtrees": [ x509.DNSName("localhost"), x509.DNSName("localhost.localdomain"), - x509.DNSName("127.0.0.1") + x509.IPAddress(IPv4Network("127.0.0.1/32")) ], "key_usage": x509.KeyUsage( digital_signature=True, @@ -48,17 +59,22 @@ def gen_chain( } intermediate_cert_params = { - "cn": "intermediate.example.com", - "org_name": "Example Intermediate", + "cn": intermediate_cn, + "organization_name": "Example Intermediate", "country_name": "IT", - "dns": "intermediate.example.com", - "uri": "https://intermediate.example.com", + "email_address": f"info@{intermediate_dns}", + "dns": intermediate_dns, + "uri": f"https://{intermediate_dns}", "ca": True, "path_length": 0, + "permitted_subtrees": [ + x509.DNSName(intermediate_dns), + x509.DNSName(leaf_dns), + ], "excluded_subtrees": [ x509.DNSName("localhost"), x509.DNSName("localhost.localdomain"), - x509.DNSName("127.0.0.1") + x509.IPAddress(IPv4Network("127.0.0.1/32")) ], "key_usage": x509.KeyUsage( digital_signature=True, @@ -71,26 +87,26 @@ def gen_chain( encipher_only=False, decipher_only=False ), - "crl_distr_point": "https://intermediate.example.net/crl/intermediate.example.net.crl" + "crl_distr_point": f"https://{intermediate_dns}/crl/{intermediate_dns}.crl" } leaf_cert_params = { "cn": leaf_cn, - "org_name": "Example Leaf", + "organization_name": "Example Leaf", "country_name": "IT", + "email_address": f"info@{leaf_dns}", "dns": leaf_dns, "uri": leaf_uri, "ca": False, "path_length": None, "private_key": leaf_private_key, "permitted_subtrees": [ - x509.UniformResourceIdentifier(f"https://leaf.example.com"), - x509.DNSName("leaf.example.com"), + x509.DNSName(leaf_dns), ], "excluded_subtrees": [ x509.DNSName("localhost"), x509.DNSName("localhost.localdomain"), - x509.DNSName("127.0.0.1") + x509.IPAddress(IPv4Network("127.0.0.1/32")) ], "key_usage": x509.KeyUsage( digital_signature=True, @@ -103,7 +119,7 @@ def gen_chain( encipher_only=False, decipher_only=False ), - "crl_distr_point": "https://leaf.example.com/crl/leaf.example.com.crl" + "crl_distr_point": f"https://{leaf_dns}/crl/{leaf_dns}.crl", } if date: @@ -162,7 +178,7 @@ def test_chain_issuer(): issuer = get_issuer_from_x5c(chain) trust_anchor = get_trust_anchor_from_x5c(chain) - assert issuer == "leaf.example.org" + assert issuer == "leaf.example.it" assert trust_anchor == "ca.example.com" diff --git a/pyeudiw/x509/chain_builder.py b/pyeudiw/x509/chain_builder.py index d77f2715c..d4a7a7676 100644 --- a/pyeudiw/x509/chain_builder.py +++ b/pyeudiw/x509/chain_builder.py @@ -7,7 +7,6 @@ from typing import Literal from cryptography import x509 - class ChainBuilder: def __init__(self): self.chain = [] @@ -16,8 +15,9 @@ def __init__(self): def gen_certificate( self, cn: str, - org_name: str, + organization_name: str, country_name: str, + email_address: str, dns: str, uri: str, ca: bool, @@ -29,15 +29,17 @@ def gen_certificate( not_valid_after: datetime = datetime.now() + timedelta(days=365), excluded_subtrees: list[x509.DNSName | x509.UniformResourceIdentifier] | None = None, permitted_subtrees: list[x509.DNSName | x509.UniformResourceIdentifier] | None = None, - key_usage: x509.KeyUsage | None = None + key_usage: x509.KeyUsage | None = None, + organization_identifier: str | None = None ) -> None: """ Generate a certificate and add it to the chain. :param cn: Common Name :type cn: str - :param org_name: Organization Name - :type org_name: str + :param organization_name: Organization name for the certificate + :type organization_name: str + :type organization_name: str | None :param country_name: Country Name :type country_name: str :param dns: DNS Name @@ -60,6 +62,10 @@ def gen_certificate( :type excluded_subtrees: list[x509.DNSName | x509.UniformResourceIdentifier] :param permitted_subtrees: List of DNS names to permit in the certificate :type permitted_subtrees: list[x509.DNSName | x509.UniformResourceIdentifier] + :param key_usage: Key usage for the certificate + :type key_usage: x509.KeyUsage | None + :param organization_identifier: Organization identifier for the certificate + :type organization_identifier: str | None :return: None """ @@ -68,39 +74,39 @@ def gen_certificate( ec.SECP256R1(), ) - cert = x509.CertificateBuilder() \ - .subject_name( - x509.Name( - [ - x509.NameAttribute(NameOID.COMMON_NAME, - cn - ), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, - org_name - ), - x509.NameAttribute(NameOID.COUNTRY_NAME, - country_name - ), - ] - ) + cert = x509.CertificateBuilder() + + x5c_names = [ + x509.NameAttribute(NameOID.COMMON_NAME, + cn + ), + x509.NameAttribute(NameOID.COUNTRY_NAME, + country_name + ), + x509.NameAttribute(NameOID.EMAIL_ADDRESS, + email_address + ), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, + organization_name ) - + ] - cert = cert.issuer_name( - x509.Name( - [ - x509.NameAttribute(NameOID.COMMON_NAME, - cn if len(self.certificates_attributes) == 0 else self.certificates_attributes[0]["cn"] - ), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, - org_name if len(self.certificates_attributes) == 0 else self.certificates_attributes[0]["org_name"] - ), - x509.NameAttribute(NameOID.COUNTRY_NAME, - country_name if len(self.certificates_attributes) == 0 else self.certificates_attributes[0]["country_name"] - ), - ] + subject_names = x509.Name(x5c_names) + + if organization_identifier: + x5c_names.append( + x509.NameAttribute(NameOID.ORGANIZATION_IDENTIFIER, organization_identifier) ) - ) \ + + + cert = cert.subject_name(subject_names) + + if not self.certificates_attributes: + issuer_name = subject_names + else: + issuer_name = self.certificates_attributes[0]["certificate"].subject + + cert = cert.issuer_name(issuer_name) \ .public_key(private_key.public_key()) \ .serial_number(x509.random_serial_number() if not serial_number else serial_number) \ .not_valid_before(not_valid_before) \ @@ -146,13 +152,26 @@ def gen_certificate( ]), critical=False ) \ - .sign(private_key if len(self.certificates_attributes) == 0 else self.certificates_attributes[0]["private_key"], hashes.SHA256()) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()), + critical=False + ) + + if self.certificates_attributes: + cert = cert.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_public_key( + self.certificates_attributes[0]["certificate"].public_key() + ), + critical=False + ) + + cert = cert.sign( + private_key if len(self.certificates_attributes) == 0 else self.certificates_attributes[0]["private_key"], hashes.SHA256() + ) self.certificates_attributes.insert(0, { - "cn": cn, - "org_name": org_name, - "country_name": country_name, - "private_key": private_key + "private_key": private_key, + "certificate": cert }) self.chain.insert(0, cert) diff --git a/requirements-dev.txt b/requirements-dev.txt index dbe732e22..401ac0dee 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,5 +19,4 @@ playwright freezegun pytest-mock PyJWT -cryptography -cose \ No newline at end of file +cose diff --git a/setup.py b/setup.py index de2564a2f..a59a7f910 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,8 @@ def readme(): "cryptojwt>=1.9,<1.10", "pydantic>=2.10.6,<3.0.0", "pyqrcode>=1.2,<1.3", - "pem>=23.1,<23.2" + "pem>=23.1,<23.2", + "cryptography>=45.0.0,<46.0.0" ], extras_require={ "satosa": [