diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index beca979..8dbe415 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,7 @@ def new( validity: dict = None, devicekeyinfo: Union[dict, CoseKey, str] = None, cert_path: str = None, - revocation: dict = None, + revocation: dict = None ): """ create a new mdoc with signed mso @@ -82,15 +84,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 revocation: dict: revocation status dict it may include status_list and identifier_list keys :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 @@ -138,7 +146,7 @@ def new( alg=self.alg, kid=self.kid, validity=validity, - revocation=revocation, + revocation=revocation ) else: @@ -148,10 +156,10 @@ def new( alg=self.alg, cert_path=cert_path, validity=validity, - revocation=revocation, + revocation=revocation ) - mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo) + mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime.UTC)) mso_cbor = mso.encode( tag=False, @@ -162,18 +170,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..43acf90 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -7,25 +7,19 @@ 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 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 * @@ -40,7 +34,6 @@ def __init__( self, data: dict, validity: dict, - revocation: str = None, cert_path: str = None, key_label: str = None, user_pin: str = None, @@ -51,13 +44,13 @@ def __init__( hsm: bool = False, private_key: Union[dict, CoseKey] = None, digest_alg: str = settings.PYMDOC_HASHALG, + revocation: dict = None ) -> 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 revocation: dict: revocation status dict to include in the mso, it may include status_list and identifier_list keys """ if not hsm: @@ -82,10 +76,10 @@ 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") @@ -208,19 +202,32 @@ def sign( "deviceKeyInfo": { "deviceKey": device_key, }, - "digestAlgorithm": alg_map.get(self.alg), + "digestAlgorithm": alg_map.get(self.alg) } - if self.revocation is not None: payload.update({"status": self.revocation}) 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) - + _parsed_cert: Union[Certificate, None] = None + try: + _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: + _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.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() 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..41e8879 100644 --- a/pymdoccbor/tests/test_02_mdoc_issuer.py +++ b/pymdoccbor/tests/test_02_mdoc_issuer.py @@ -1,11 +1,16 @@ 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 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 = { @@ -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("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, + revocation=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