From 2c147416387e3c952785134f5bb2d9f3ef2225cd Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:40:37 +0100 Subject: [PATCH 1/9] Add minimal support to integrate status list reference data in the mso/issuer.py - tests update (replaced revocation to status list for the sake of clarity) Load also Y coordinate from DER certificate instead of calculating it + enhanced some "unit" tests Allow multiple doc support in IS0 18013-5 format Add support to load PEM certificate format (and not only DER ones) --- pymdoccbor/mdoc/issuer.py | 38 +++++++++---- pymdoccbor/mso/issuer.py | 31 +++++----- pymdoccbor/mso/verifier.py | 3 +- pymdoccbor/tests/certs/README.md | 6 ++ pymdoccbor/tests/certs/fake-cert.cnf | 31 ++++++++++ pymdoccbor/tests/certs/fake-cert.pem | 15 +++++ pymdoccbor/tests/certs/fake-private-key.pem | 5 ++ pymdoccbor/tests/certs/fake-request.csr | 10 ++++ pymdoccbor/tests/test_02_mdoc_issuer.py | 63 +++++++++++++++------ requirements-dev.txt | 1 + 10 files changed, 157 insertions(+), 46 deletions(-) create mode 100644 pymdoccbor/tests/certs/README.md create mode 100644 pymdoccbor/tests/certs/fake-cert.cnf create mode 100644 pymdoccbor/tests/certs/fake-cert.pem create mode 100644 pymdoccbor/tests/certs/fake-private-key.pem create mode 100644 pymdoccbor/tests/certs/fake-request.csr diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index beca979..6b029c4 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -2,8 +2,10 @@ import binascii import cbor2 import logging +import datetime from cryptography.hazmat.primitives import serialization -from pycose.keys import CoseKey +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey +from pycose.keys import CoseKey, EC2Key from typing import Union from pymdoccbor.mso.issuer import MsoIssuer @@ -72,7 +74,8 @@ def new( validity: dict = None, devicekeyinfo: Union[dict, CoseKey, str] = None, cert_path: str = None, - revocation: dict = None, + pem_cert_path: str = None, + status_list: dict = {}, ): """ create a new mdoc with signed mso @@ -82,15 +85,21 @@ def new( :param validity: dict: validity info :param devicekeyinfo: Union[dict, CoseKey, str]: device key info :param cert_path: str: path to the certificate - :param revocation: dict: revocation info + :param status_list: dict: The status list to include in the mso of the mdoc :return: dict: signed mdoc """ if isinstance(devicekeyinfo, dict): - devicekeyinfo = CoseKey.from_dict(devicekeyinfo) + devicekeyinfoCoseKeyObject = CoseKey.from_dict(devicekeyinfo) + devicekeyinfo = { + 1: devicekeyinfoCoseKeyObject.kty.identifier, + -1: devicekeyinfoCoseKeyObject.crv.identifier, + -2: devicekeyinfoCoseKeyObject.x, + -3: devicekeyinfoCoseKeyObject.y, + } if isinstance(devicekeyinfo, str): device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8")) - public_key = serialization.load_pem_public_key(device_key_bytes) + public_key:EllipticCurvePublicKey = serialization.load_pem_public_key(device_key_bytes) curve_name = public_key.curve.name curve_map = { "secp256r1": 1, # NIST P-256 @@ -130,6 +139,7 @@ def new( msoi = MsoIssuer( data=data, cert_path=cert_path, + pem_cert_path=pem_cert_path, hsm=self.hsm, key_label=self.key_label, user_pin=self.user_pin, @@ -138,7 +148,7 @@ def new( alg=self.alg, kid=self.kid, validity=validity, - revocation=revocation, + status_list=status_list ) else: @@ -147,11 +157,12 @@ def new( private_key=self.private_key, alg=self.alg, cert_path=cert_path, + pem_cert_path=pem_cert_path, validity=validity, - revocation=revocation, + status_list=status_list ) - mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo) + mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now()) mso_cbor = mso.encode( tag=False, @@ -162,18 +173,21 @@ def new( slot_id=self.slot_id, ) + res = { "version": self.version, - "documents": [{ + "documents": [ + { "docType": doctype, # 'org.iso.18013.5.1.mDL' "issuerSigned": { "nameSpaces": { ns: [v for k, v in dgst.items()] for ns, dgst in msoi.disclosure_map.items() - }, + }, "issuerAuth": cbor2.decoder.loads(mso_cbor), - }, - }], + }, + } + ], "status": self.status, } diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 3d943a3..0327aa6 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -7,19 +7,12 @@ logger = logging.getLogger("pymdoccbor") -from pycose.headers import Algorithm -from pycose.keys import CoseKey - -from datetime import timezone - from pycose.headers import Algorithm #, KID from pycose.keys import CoseKey, EC2Key - from pycose.messages import Sign1Message from typing import Union - from pymdoccbor.exceptions import MsoPrivateKeyRequired from pymdoccbor import settings from pymdoccbor.x509 import MsoX509Fabric @@ -40,8 +33,8 @@ def __init__( self, data: dict, validity: dict, - revocation: str = None, cert_path: str = None, + pem_cert_path: str = None, key_label: str = None, user_pin: str = None, lib_path: str = None, @@ -51,13 +44,13 @@ def __init__( hsm: bool = False, private_key: Union[dict, CoseKey] = None, digest_alg: str = settings.PYMDOC_HASHALG, + status_list: dict = {} ) -> None: """ Initialize a new MsoIssuer :param data: dict: the data to sign :param validity: validity: the validity info of the mso - :param revocation: str: the revocation status :param cert_path: str: the path to the certificate :param key_label: str: key label :param user_pin: str: user pin @@ -68,6 +61,7 @@ def __init__( :param hsm: bool: hardware security module :param private_key: Union[dict, CoseKey]: the signing key :param digest_alg: str: the digest algorithm + :param status_list: dict: the status list to include in the mso """ if not hsm: @@ -82,16 +76,17 @@ def __init__( raise ValueError("private_key must be a dict or CoseKey object") else: raise MsoPrivateKeyRequired("MSO Writer requires a valid private key") - + if not validity: raise ValueError("validity must be present") - + if not alg: raise ValueError("alg must be present") self.data: dict = data self.hash_map: dict = {} self.cert_path = cert_path + self.pem_cert_path = pem_cert_path self.disclosure_map: dict = {} self.digest_alg: str = digest_alg self.key_label = key_label @@ -102,7 +97,7 @@ def __init__( self.alg = alg self.kid = kid self.validity = validity - self.revocation = revocation + self.status_list = status_list alg_map = {"ES256": "sha256", "ES384": "sha384", "ES512": "sha512"} @@ -209,11 +204,9 @@ def sign( "deviceKey": device_key, }, "digestAlgorithm": alg_map.get(self.alg), + "status": self.status_list } - if self.revocation is not None: - payload.update({"status": self.revocation}) - if self.cert_path: # Load the DER certificate file with open(self.cert_path, "rb") as file: @@ -221,6 +214,14 @@ def sign( cert = x509.load_der_x509_certificate(certificate) + _cert = cert.public_bytes(getattr(serialization.Encoding, "DER")) + elif self.pem_cert_path: + # Load the PEM certificate file + with open(self.pem_cert_path, "rb") as file: + certificate = file.read() + + cert = x509.load_pem_x509_certificate(certificate) + _cert = cert.public_bytes(getattr(serialization.Encoding, "DER")) else: _cert = self.selfsigned_x509cert() diff --git a/pymdoccbor/mso/verifier.py b/pymdoccbor/mso/verifier.py index 20c4602..f8661de 100644 --- a/pymdoccbor/mso/verifier.py +++ b/pymdoccbor/mso/verifier.py @@ -117,7 +117,8 @@ def load_public_key(self) -> None: crv=settings.COSEKEY_HAZMAT_CRV_MAP[self.public_key.curve.name], x=self.public_key.public_numbers().x.to_bytes( settings.CRV_LEN_MAP[self.public_key.curve.name], 'big' - ) + ), + y=self.public_key.public_numbers().y.to_bytes( settings.CRV_LEN_MAP[self.public_key.curve.name], 'big') ) self.object.key = key diff --git a/pymdoccbor/tests/certs/README.md b/pymdoccbor/tests/certs/README.md new file mode 100644 index 0000000..a4d2758 --- /dev/null +++ b/pymdoccbor/tests/certs/README.md @@ -0,0 +1,6 @@ +### Procedure to create fake certificate fake-cert.pem +``` +openssl ecparam -name prime256v1 -genkey -noout -out fake-private-key.pem +openssl x509 -req -in fake-request.csr -out leaf-asl.pem -days 3650 -sha256 +openssl x509 -req -in fake-request.csr -key fake-private-key.pem -out fake-cert.pem -days 3650 -sha256 +``` \ No newline at end of file diff --git a/pymdoccbor/tests/certs/fake-cert.cnf b/pymdoccbor/tests/certs/fake-cert.cnf new file mode 100644 index 0000000..2540523 --- /dev/null +++ b/pymdoccbor/tests/certs/fake-cert.cnf @@ -0,0 +1,31 @@ +[ req ] +distinguished_name = req_distinguished_name +attributes = req_attributes + +# Stop confirmation prompts. All information is contained below. +prompt= no + + +# The extensions to add to a certificate request - see [ v3_req ] +req_extensions = v3_req + +[ req_distinguished_name ] +# Describe the Subject (ie the origanisation). +# The first 6 below could be shortened to: C ST L O OU CN +# The short names are what are shown when the certificate is displayed. +# Eg the details below would be shown as: +# Subject: C=UK, ST=Hertfordshire, L=My Town, O=Some Organisation, OU=Some Department, CN=www.example.com/emailAddress=bofh@example.com + +countryName= BE +stateOrProvinceName= Brussels Region +localityName= Brussels +organizationName= Test +organizationalUnitName= Test-Unit +commonName= Test ASL Issuer +emailAddress= fake@fake.com + +[ req_attributes ] +# None. Could put Challenge Passwords, don't want them, leave empty + +[ v3_req ] +# None. \ No newline at end of file diff --git a/pymdoccbor/tests/certs/fake-cert.pem b/pymdoccbor/tests/certs/fake-cert.pem new file mode 100644 index 0000000..c79157c --- /dev/null +++ b/pymdoccbor/tests/certs/fake-cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICTzCCAfWgAwIBAgIUN+rPlhGdCIIWrQaKxFcdzJGyL0YwCgYIKoZIzj0EAwIw +gZUxCzAJBgNVBAYTAkJFMRgwFgYDVQQIDA9CcnVzc2VscyBSZWdpb24xETAPBgNV +BAcMCEJydXNzZWxzMQ0wCwYDVQQKDARUZXN0MRIwEAYDVQQLDAlUZXN0LVVuaXQx +GDAWBgNVBAMMD1Rlc3QgQVNMIElzc3VlcjEcMBoGCSqGSIb3DQEJARYNZmFrZUBm +YWtlLmNvbTAeFw0yNTAzMTcxMTA3MTZaFw0zNTAzMTUxMTA3MTZaMIGVMQswCQYD +VQQGEwJCRTEYMBYGA1UECAwPQnJ1c3NlbHMgUmVnaW9uMREwDwYDVQQHDAhCcnVz +c2VsczENMAsGA1UECgwEVGVzdDESMBAGA1UECwwJVGVzdC1Vbml0MRgwFgYDVQQD +DA9UZXN0IEFTTCBJc3N1ZXIxHDAaBgkqhkiG9w0BCQEWDWZha2VAZmFrZS5jb20w +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASgs+CiDRy2Fh1lPA6mtIb/c1fBBIA3 +Qz77kpnxsOid5/2bbUFYOI02djof6hsq7lWuCGwdWThDeiUQV1hISCPyoyEwHzAd +BgNVHQ4EFgQU+jJ/exJHH3gawahlcnWTrlxbw3UwCgYIKoZIzj0EAwIDSAAwRQIg +JJ3N2I7VyCFzN8CVktrs6IylXlDiSC+vsjt1POLnrHYCIQDKkU1XOfQiBGFzeLav +vvqxhGIU/iOVlrLM3JOF9pGKCA== +-----END CERTIFICATE----- diff --git a/pymdoccbor/tests/certs/fake-private-key.pem b/pymdoccbor/tests/certs/fake-private-key.pem new file mode 100644 index 0000000..91bcd0c --- /dev/null +++ b/pymdoccbor/tests/certs/fake-private-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEWpyV6wCzKqJhcvRWg2olReRXLLcUwyL2IZzKNLiR6koAoGCCqGSM49 +AwEHoUQDQgAEoLPgog0cthYdZTwOprSG/3NXwQSAN0M++5KZ8bDonef9m21BWDiN +NnY6H+obKu5VrghsHVk4Q3olEFdYSEgj8g== +-----END EC PRIVATE KEY----- diff --git a/pymdoccbor/tests/certs/fake-request.csr b/pymdoccbor/tests/certs/fake-request.csr new file mode 100644 index 0000000..199d003 --- /dev/null +++ b/pymdoccbor/tests/certs/fake-request.csr @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBUDCB+AIBADCBlTELMAkGA1UEBhMCQkUxGDAWBgNVBAgMD0JydXNzZWxzIFJl +Z2lvbjERMA8GA1UEBwwIQnJ1c3NlbHMxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAsM +CVRlc3QtVW5pdDEYMBYGA1UEAwwPVGVzdCBBU0wgSXNzdWVyMRwwGgYJKoZIhvcN +AQkBFg1mYWtlQGZha2UuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoLPg +og0cthYdZTwOprSG/3NXwQSAN0M++5KZ8bDonef9m21BWDiNNnY6H+obKu5Vrghs +HVk4Q3olEFdYSEgj8qAAMAoGCCqGSM49BAMCA0cAMEQCICtw2VqH3Jg03Ycme7UW +0aQbBll8eQiBDPLCui+yekAMAiBfLqO9P7mgEWPMoSWfGYBiOVDEVUO8vERTZY1e +HKpaRg== +-----END CERTIFICATE REQUEST----- diff --git a/pymdoccbor/tests/test_02_mdoc_issuer.py b/pymdoccbor/tests/test_02_mdoc_issuer.py index 7975c15..d0d3251 100644 --- a/pymdoccbor/tests/test_02_mdoc_issuer.py +++ b/pymdoccbor/tests/test_02_mdoc_issuer.py @@ -1,5 +1,10 @@ import cbor2 import os + +from asn1crypto.x509 import Certificate +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.x509 import load_der_x509_certificate from pycose.messages import Sign1Message from pymdoccbor.mdoc.issuer import MdocCborIssuer @@ -17,15 +22,20 @@ } +def extract_mso(mdoc:dict): + mso_data = mdoc["documents"][0]["issuerSigned"]["issuerAuth"][2] + mso_cbortag = cbor2.loads(mso_data) + mso = cbor2.loads(mso_cbortag.value) + return mso + + def test_mso_writer(): + validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" } msoi = MsoIssuer( data=PID_DATA, private_key=PKEY, - validity={ - "issuance_date": "2024-12-31", - "expiry_date": "2050-12-31" - }, - alg="ES256" + validity=validity, + alg = "ES256" ) assert "eu.europa.ec.eudiw.pid.1" in msoi.hash_map @@ -44,26 +54,43 @@ def test_mso_writer(): def test_mdoc_issuer(): + validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" } mdoci = MdocCborIssuer( private_key=PKEY, - alg="ES256", - ) - - mdoc = mdoci.new( - doctype="eu.europa.ec.eudiw.pid.1", - data=PID_DATA, - #devicekeyinfo=PKEY, TODO - validity={ - "issuance_date": "2024-12-31", - "expiry_date": "2050-12-31" - }, + alg = "ES256" ) + with open("/Users/olivier/PycharmProjects/pymdoccbor_oli/pymdoccbor/tests/certs/fake-cert.pem", "rb") as file: + fake_cert_file = file.read() + asl_signing_cert = x509.load_pem_x509_certificate(fake_cert_file) + _asl_signing_cert = asl_signing_cert.public_bytes(getattr(serialization.Encoding, "DER")) + status_list = { + "status_list": { + "idx": 0, + "uri": "https://issuer.com/statuslists", + "certificate": _asl_signing_cert, + } + } + mdoc = mdoci.new( + doctype="eu.europa.ec.eudiw.pid.1", + data=PID_DATA, + devicekeyinfo=PKEY, + validity=validity, + status_list=status_list + ) mdocp = MdocCbor() aa = cbor2.dumps(mdoc) mdocp.loads(aa) - mdocp.verify() + assert mdocp.verify() is True mdoci.dump() mdoci.dumps() - + + # check mso content for status list + mso = extract_mso(mdoc) + status_list = mso["status"]["status_list"] + assert status_list["idx"] == 0 + assert status_list["uri"] == "https://issuer.com/statuslists" + cert_bytes = status_list["certificate"] + cert:Certificate = load_der_x509_certificate(cert_bytes) + assert "Test ASL Issuer" in cert.subject.rfc4514_string(), "ASL is not signed with the expected certificate" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 951c2d2..d75f084 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ isort autoflake bandit autopep8 +pycose~=1.0.1 From eb60602afcaf0ffa1f2d750d1919fe40ae906de2 Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Tue, 18 Mar 2025 22:33:29 +0100 Subject: [PATCH 2/9] fix broken test --- pymdoccbor/tests/test_02_mdoc_issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymdoccbor/tests/test_02_mdoc_issuer.py b/pymdoccbor/tests/test_02_mdoc_issuer.py index d0d3251..a9b5b48 100644 --- a/pymdoccbor/tests/test_02_mdoc_issuer.py +++ b/pymdoccbor/tests/test_02_mdoc_issuer.py @@ -59,7 +59,7 @@ def test_mdoc_issuer(): private_key=PKEY, alg = "ES256" ) - with open("/Users/olivier/PycharmProjects/pymdoccbor_oli/pymdoccbor/tests/certs/fake-cert.pem", "rb") as file: + with open("pymdoccbor/tests/certs/fake-cert.pem", "rb") as file: fake_cert_file = file.read() asl_signing_cert = x509.load_pem_x509_certificate(fake_cert_file) _asl_signing_cert = asl_signing_cert.public_bytes(getattr(serialization.Encoding, "DER")) From a1bec12059ad715690d1428a0e465cd669df5f8c Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:54:45 +0100 Subject: [PATCH 3/9] Remove pem_cert_path and manage pem or der formats through cert-path parameter set utc in valid from --- pymdoccbor/mdoc/issuer.py | 5 +---- pymdoccbor/mso/issuer.py | 25 +++++++++++-------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index 6b029c4..89d70b4 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -74,7 +74,6 @@ def new( validity: dict = None, devicekeyinfo: Union[dict, CoseKey, str] = None, cert_path: str = None, - pem_cert_path: str = None, status_list: dict = {}, ): """ @@ -139,7 +138,6 @@ def new( msoi = MsoIssuer( data=data, cert_path=cert_path, - pem_cert_path=pem_cert_path, hsm=self.hsm, key_label=self.key_label, user_pin=self.user_pin, @@ -157,12 +155,11 @@ def new( private_key=self.private_key, alg=self.alg, cert_path=cert_path, - pem_cert_path=pem_cert_path, validity=validity, status_list=status_list ) - mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now()) + mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime. UTC)) mso_cbor = mso.encode( tag=False, diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 0327aa6..269a133 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -34,7 +34,6 @@ def __init__( data: dict, validity: dict, cert_path: str = None, - pem_cert_path: str = None, key_label: str = None, user_pin: str = None, lib_path: str = None, @@ -86,7 +85,6 @@ def __init__( self.data: dict = data self.hash_map: dict = {} self.cert_path = cert_path - self.pem_cert_path = pem_cert_path self.disclosure_map: dict = {} self.digest_alg: str = digest_alg self.key_label = key_label @@ -208,20 +206,19 @@ def sign( } if self.cert_path: - # Load the DER certificate file + # Try to load the certificate file with open(self.cert_path, "rb") as file: certificate = file.read() - - cert = x509.load_der_x509_certificate(certificate) - - _cert = cert.public_bytes(getattr(serialization.Encoding, "DER")) - elif self.pem_cert_path: - # Load the PEM certificate file - with open(self.pem_cert_path, "rb") as file: - certificate = file.read() - - cert = x509.load_pem_x509_certificate(certificate) - + try: + cert = x509.load_pem_x509_certificate(certificate) + except Exception as e: + logger.error(f"Certificate at {self.cert_path} could not be loaded as PEM, trying DER") + try: + cert = x509.load_der_x509_certificate(certificate) + except Exception as e: + _err_msg = f"Certificate at {self.cert_path} could not be loaded as DER" + logger.critical(_err_msg) + raise Exception(_err_msg) _cert = cert.public_bytes(getattr(serialization.Encoding, "DER")) else: _cert = self.selfsigned_x509cert() From 9d7a9df2f13850326d629407a493b2189482420f Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:49:12 +0100 Subject: [PATCH 4/9] reinstate revocation parameter in MdocCborIssuer.new method --- pymdoccbor/mdoc/issuer.py | 10 +++++++--- pymdoccbor/mso/issuer.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index 89d70b4..bfc140e 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -74,7 +74,8 @@ def new( validity: dict = None, devicekeyinfo: Union[dict, CoseKey, str] = None, cert_path: str = None, - status_list: dict = {}, + status_list: dict = None, + revocation: dict = None ): """ create a new mdoc with signed mso @@ -85,6 +86,7 @@ def new( :param devicekeyinfo: Union[dict, CoseKey, str]: device key info :param cert_path: str: path to the certificate :param status_list: dict: The status list to include in the mso of the mdoc + :param revocation: dict: The status list to include in the mso of the mdoc :return: dict: signed mdoc """ @@ -146,7 +148,8 @@ def new( alg=self.alg, kid=self.kid, validity=validity, - status_list=status_list + status_list=status_list, + revocation=revocation ) else: @@ -156,7 +159,8 @@ def new( alg=self.alg, cert_path=cert_path, validity=validity, - status_list=status_list + status_list=status_list, + revocation=revocation ) mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime. UTC)) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 269a133..0f66690 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -43,7 +43,8 @@ def __init__( hsm: bool = False, private_key: Union[dict, CoseKey] = None, digest_alg: str = settings.PYMDOC_HASHALG, - status_list: dict = {} + status_list: dict = None, + revocation: dict = None ) -> None: """ Initialize a new MsoIssuer @@ -61,6 +62,7 @@ def __init__( :param private_key: Union[dict, CoseKey]: the signing key :param digest_alg: str: the digest algorithm :param status_list: dict: the status list to include in the mso + :param revocation: dict: the status list to include in the mso """ if not hsm: @@ -96,6 +98,7 @@ def __init__( self.kid = kid self.validity = validity self.status_list = status_list + self.revocation = revocation alg_map = {"ES256": "sha256", "ES384": "sha384", "ES512": "sha512"} @@ -201,9 +204,12 @@ def sign( "deviceKeyInfo": { "deviceKey": device_key, }, - "digestAlgorithm": alg_map.get(self.alg), - "status": self.status_list + "digestAlgorithm": alg_map.get(self.alg) } + if self.status_list is not None: + payload.update({"status": self.status_list}) + if self.revocation is not None: + payload.update({"status": self.revocation}) if self.cert_path: # Try to load the certificate file From 068eb7a7b79c6ba39a92dd2507d68dfcd1bbbbb7 Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:59:57 +0100 Subject: [PATCH 5/9] fix datetime.now(utc) --- pymdoccbor/mdoc/issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index bfc140e..eb39b79 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -163,7 +163,7 @@ def new( revocation=revocation ) - mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime. UTC)) + mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime.UTC)) mso_cbor = mso.encode( tag=False, From d0fac66824bf739cea85a7f338cb7860750b59c3 Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:53:26 +0100 Subject: [PATCH 6/9] Remove useless status_list parameter as the object is de facto in the more general revocation dict parameter --- pymdoccbor/mdoc/issuer.py | 6 +----- pymdoccbor/mso/issuer.py | 7 +------ pymdoccbor/tests/test_02_mdoc_issuer.py | 4 ++-- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index eb39b79..8dbe415 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -74,7 +74,6 @@ def new( validity: dict = None, devicekeyinfo: Union[dict, CoseKey, str] = None, cert_path: str = None, - status_list: dict = None, revocation: dict = None ): """ @@ -85,8 +84,7 @@ def new( :param validity: dict: validity info :param devicekeyinfo: Union[dict, CoseKey, str]: device key info :param cert_path: str: path to the certificate - :param status_list: dict: The status list to include in the mso of the mdoc - :param revocation: dict: The status list to include in the mso of the mdoc + :param revocation: dict: revocation status dict it may include status_list and identifier_list keys :return: dict: signed mdoc """ @@ -148,7 +146,6 @@ def new( alg=self.alg, kid=self.kid, validity=validity, - status_list=status_list, revocation=revocation ) @@ -159,7 +156,6 @@ def new( alg=self.alg, cert_path=cert_path, validity=validity, - status_list=status_list, revocation=revocation ) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 0f66690..5b95bbb 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -43,7 +43,6 @@ def __init__( hsm: bool = False, private_key: Union[dict, CoseKey] = None, digest_alg: str = settings.PYMDOC_HASHALG, - status_list: dict = None, revocation: dict = None ) -> None: """ @@ -61,8 +60,7 @@ def __init__( :param hsm: bool: hardware security module :param private_key: Union[dict, CoseKey]: the signing key :param digest_alg: str: the digest algorithm - :param status_list: dict: the status list to include in the mso - :param revocation: dict: the status list to include in the mso + :param revocation: dict: revocation status dict to include in the mso, it may include status_list and identifier_list keys """ if not hsm: @@ -97,7 +95,6 @@ def __init__( self.alg = alg self.kid = kid self.validity = validity - self.status_list = status_list self.revocation = revocation alg_map = {"ES256": "sha256", "ES384": "sha384", "ES512": "sha512"} @@ -206,8 +203,6 @@ def sign( }, "digestAlgorithm": alg_map.get(self.alg) } - if self.status_list is not None: - payload.update({"status": self.status_list}) if self.revocation is not None: payload.update({"status": self.revocation}) diff --git a/pymdoccbor/tests/test_02_mdoc_issuer.py b/pymdoccbor/tests/test_02_mdoc_issuer.py index a9b5b48..41e8879 100644 --- a/pymdoccbor/tests/test_02_mdoc_issuer.py +++ b/pymdoccbor/tests/test_02_mdoc_issuer.py @@ -10,7 +10,7 @@ from pymdoccbor.mdoc.issuer import MdocCborIssuer from pymdoccbor.mdoc.verifier import MdocCbor from pymdoccbor.mso.issuer import MsoIssuer -from . pid_data import PID_DATA +from pymdoccbor.tests.pid_data import PID_DATA PKEY = { @@ -75,7 +75,7 @@ def test_mdoc_issuer(): data=PID_DATA, devicekeyinfo=PKEY, validity=validity, - status_list=status_list + revocation=status_list ) mdocp = MdocCbor() From f241e9571a8fffbf7c93e032e67fbeef055c681a Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:20:22 +0100 Subject: [PATCH 7/9] Remove pycose deps from requirements-dev.txt Co-authored-by: Giuseppe De Marco --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d75f084..951c2d2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,3 @@ isort autoflake bandit autopep8 -pycose~=1.0.1 From 8556de6d7ed3292e8f922361eefad033df97c507 Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:24:12 +0100 Subject: [PATCH 8/9] Update pymdoccbor/mso/issuer.py code prevents a useless and costly try/except using a conditional when trying to parse issuer cert Co-authored-by: Giuseppe De Marco --- pymdoccbor/mso/issuer.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 5b95bbb..a1f750b 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -210,16 +210,23 @@ def sign( # Try to load the certificate file with open(self.cert_path, "rb") as file: certificate = file.read() + _parsed_cert: Union[Certificate, None] = None try: - cert = x509.load_pem_x509_certificate(certificate) + _parsed_cert = x509.load_pem_x509_certificate(certificate) except Exception as e: logger.error(f"Certificate at {self.cert_path} could not be loaded as PEM, trying DER") + + if not _parsed_cert: try: - cert = x509.load_der_x509_certificate(certificate) + _parsed_cert = x509.load_der_x509_certificate(certificate) except Exception as e: _err_msg = f"Certificate at {self.cert_path} could not be loaded as DER" - logger.critical(_err_msg) - raise Exception(_err_msg) + logger.error(_err_msg) + + if _parsed_cert: + cert = _parsed_cert + else: + raise Exception(f"Certificate at {self.cert_path} failed parse") _cert = cert.public_bytes(getattr(serialization.Encoding, "DER")) else: _cert = self.selfsigned_x509cert() From 5a9a26d27641ccd49ee21cf2a13c4bacc18b51b1 Mon Sep 17 00:00:00 2001 From: oligatorr <31451875+oligatorr@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:32:24 +0100 Subject: [PATCH 9/9] fix indent and 1 missing import --- pymdoccbor/mso/issuer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index a1f750b..43acf90 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -19,6 +19,7 @@ from pymdoccbor.tools import shuffle_dict from cryptography import x509 from cryptography.hazmat.primitives import serialization +from cryptography.x509 import Certificate from cbor_diag import * @@ -223,10 +224,10 @@ def sign( _err_msg = f"Certificate at {self.cert_path} could not be loaded as DER" logger.error(_err_msg) - if _parsed_cert: - cert = _parsed_cert - else: - raise Exception(f"Certificate at {self.cert_path} failed parse") + if _parsed_cert: + cert = _parsed_cert + else: + raise Exception(f"Certificate at {self.cert_path} failed parse") _cert = cert.public_bytes(getattr(serialization.Encoding, "DER")) else: _cert = self.selfsigned_x509cert()