From 39ab4fe946c9faff97959c25a0575d3b70b06f0c Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Mon, 1 Jul 2024 10:59:47 +0100 Subject: [PATCH 01/31] preprod version --- .github/workflows/python-app.yml | 2 +- README.md | 58 +++-- examples/it_data_model.py | 67 ------ pymdoccbor/__init__.py | 2 +- pymdoccbor/mdoc/exceptions.py | 11 - pymdoccbor/mdoc/issuer.py | 226 +++++++++++------- pymdoccbor/mdoc/issuersigned.py | 33 +-- pymdoccbor/mdoc/verifier.py | 61 +---- pymdoccbor/mso/issuer.py | 279 +++++++++++++--------- pymdoccbor/mso/verifier.py | 64 ++--- pymdoccbor/settings.py | 46 ++-- pymdoccbor/tests/micov_data.py | 23 -- pymdoccbor/tests/pkey.py | 5 - pymdoccbor/tests/test_01_mdoc_parser.py | 2 +- pymdoccbor/tests/test_02_mdoc_issuer.py | 16 +- pymdoccbor/tests/test_03_mdoc_issuer.py | 57 ----- pymdoccbor/tests/test_04_issuer_signed.py | 46 ---- pymdoccbor/tests/test_05_mdoc_verifier.py | 79 ------ pymdoccbor/tests/test_06_mso_issuer.py | 34 --- pymdoccbor/tests/test_07_mso_verifier.py | 58 ----- setup.py | 53 ++-- 21 files changed, 435 insertions(+), 787 deletions(-) delete mode 100644 examples/it_data_model.py delete mode 100644 pymdoccbor/mdoc/exceptions.py delete mode 100644 pymdoccbor/tests/micov_data.py delete mode 100644 pymdoccbor/tests/pkey.py delete mode 100644 pymdoccbor/tests/test_03_mdoc_issuer.py delete mode 100644 pymdoccbor/tests/test_04_issuer_signed.py delete mode 100644 pymdoccbor/tests/test_05_mdoc_verifier.py delete mode 100644 pymdoccbor/tests/test_06_mso_issuer.py delete mode 100644 pymdoccbor/tests/test_07_mso_verifier.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1eb97f4..9910dfa 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -18,8 +18,8 @@ jobs: fail-fast: false matrix: python-version: + - '3.9' - '3.10' - - '3.11' steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 6509795..5d471bf 100644 --- a/README.md +++ b/README.md @@ -28,25 +28,52 @@ according to ISO 18013-5. ## Setup ```` -pip install pymdoccbor +pip install pymdlmdoc ```` or ```` -pip install git+https://github.com/peppelinux/pyMDOC-CBOR.git +pip install git+https://github.com/devisefutures/pyMDOC-CBOR.git@cert_arg ```` ## Usage +### Issue an MDOC CBOR signed with HSM key + +```` +PID_DATA = { + "eu.europa.ec.eudiw.pid.1": { + "family_name": "Raffaello", + "given_name": "Mascetti", + "birth_date": "1922-03-13" + } + } + +mdoci = MdocCborIssuer( + alg = 'ES256', + kid = "demo-kid", + hsm=True, + key_label="p256-1", + user_pin="1234", + lib_path="/etc/utimaco/libcs2_pkcs11.so", + slot_id=3 +) + +mdoc = mdoci.new( + doctype="eu.europa.ec.eudiw.pid.1", + data=PID_DATA, + cert_path="app/keys/IACAmDLRoot01.der" # DS certificate +) + +```` + ### Issue an MDOC CBOR `MdocCborIssuer` must be initialized with a private key. The method `.new()` gets the user attributes, devicekeyinfo and doctype. ```` -import os - from pymdoccbor.mdoc.issuer import MdocCborIssuer PKEY = { @@ -58,17 +85,12 @@ PKEY = { } PID_DATA = { - "eu.europa.ec.eudiw.pid.1": { - "family_name": "Raffaello", - "given_name": "Mascetti", - "birth_date": "1922-03-13", - "birth_place": "Rome", - "birth_country": "IT" - }, - "eu.europa.ec.eudiw.pid.it.1": { - "tax_id_code": "TINIT-XXXXXXXXXXXXXXX" + "eu.europa.ec.eudiw.pid.1": { + "family_name": "Raffaello", + "given_name": "Mascetti", + "birth_date": "1922-03-13" + } } -} mdoci = MdocCborIssuer( private_key=PKEY @@ -78,16 +100,20 @@ mdoc = mdoci.new( doctype="eu.europa.ec.eudiw.pid.1", data=PID_DATA, devicekeyinfo=PKEY # TODO + cert_path="/path/" ) mdoc >> returns a python dictionay +mdoc.dump() +>> returns mdoc MSO bytes + mdoci.dump() >> returns mdoc bytes mdoci.dumps() ->> returns AF Binary string representation +>> returns AF Binary mdoc string representation ```` ### Issue an MSO alone @@ -213,8 +239,6 @@ Other examples at [cbor official documentation](https://github.com/agronholm/cbo #### CBOR Diagnostic representation - [CBOR-DIAG-PY](https://github.com/chrysn/cbor-diag-py) -- [Authlete's CBOR diagnostic tools](https://nextdev-api.authlete.net/api/cbor) -- [Auth0 CBOR diagnostic tool](https://www.mdl.me/) #### X.509 certificates and chains diff --git a/examples/it_data_model.py b/examples/it_data_model.py deleted file mode 100644 index 195557d..0000000 --- a/examples/it_data_model.py +++ /dev/null @@ -1,67 +0,0 @@ -import cbor2 -import os - -from pymdoccbor.mdoc.issuer import MdocCborIssuer - -PKEY = { - 'KTY': 'EC2', - 'CURVE': 'P_256', - 'ALG': 'ES256', - 'D': os.urandom(32), - 'KID': b"demo-kid" -} - -PID_DATA = { - "org.iso.18013.5.1": { - "expiry_date": "2024-02-22", - "issue_date": "2023-11-14", - "issuing_country": "IT", - "issuing_authority": "Gli amici della Salaria", - "family_name": "Rossi", - "given_name": "Mario", - "birth_date": "1956-01-12", - "document_number": "XX1234567", - "portrait": b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00\x90\x00\x90\x00\x00\xff\xdb\x00C\x00\x13\r\x0e\x11\x0e\x0c\x13\x11\x0f\x11\x15\x14\x13\x17\x1d0\x1f\x1d\x1a\x1a\x1d:*,#0E=IGD=CALVm]LQhRAC_\x82`hqu{|{J\\\x86\x90\x85w\x8fmx{v\xff\xdb\x00C\x01\x14\x15\x15\x1d\x19\x1d8\x1f\x1f8vOCOvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\xff\xc0\x00\x11\x08\x00\x18\x00d\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1b\x00\x00\x03\x01\x00\x03\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x06\x04\x01\x02\x03\x07\xff\xc4\x002\x10\x00\x01\x03\x03\x03\x02\x05\x02\x03\t\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x00\x05\x11\x06\x12!\x131\x14\x15Qaq"A\x07\x81\xa1\x165BRs\x91\xb2\xc1\xf1\xff\xc4\x00\x15\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xc4\x00\x1a\x11\x01\x01\x01\x00\x03\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01A\x11!1a\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xa5\xbb\xde"\xda#)\xc7\xd6\x92\xbc}\r\x03\xf5,\xfb\x0f\xf7^z~\xf3\xe7p\x97#\xa1\xd0\xda\xe1F\xdd\xfb\xb3\xc09\xce\x07\xad+\xd4z~2\xdb\xb8\xdd\x1dR\xd6\xefK(Od\xa4\x80\x06}\xfbQ\xf8\x7f\xfb\x95\xff\x00\xeb\x9f\xf1M!]\xe6j\xf0\x89\xceD\xb7\xdb\xde\x9c\xb6\x89\n(8\xed\xdf\x18\x07\x8fz\xddb\xd4\x11\xefM\xb9\xb1\ne\xd6\xb9Z\x14s\x81\xea\rI[\x932u\xfek\xbau\xc1\x14\x10J\x8b\xa4\x10A>\x98=\xff\x00OZ\xf5\xd3KKL\xdec-\x0b\xf1\xfd\x15\x92\xbd\xd9\x1cd\x11\xf3\x93L/\xa6\xafkT\x97]\x10m\xcfJe\xaeV\xe8V\x00\x1e\xbc\x03\xc7\xce)\xdd\x9e\xef\x1e\xf1\x0f\xc4G\xdc\x9d\xa7j\xd2\xae\xe957\xa1\xba~Op\xdd\x8e\xff\x00W\xc6\xdf\xfb^\x1a\x19\x85J\x83u\x8eTR\x87P\x94n\xc6pHP\xcd\x03{\xce\xb0\x8bm},\xc7m3\x17\xfc{\\\xc0O\xb6pri\xc5\xc6\xe0\xc5\xb6\n\xe5I$!#\xb0\xe4\x93\xf6\x02\xa0uU\x9e5\x99p\xd9\x8d\xb8\x95%EkQ\xc9Q\xc8\xaf\xa1>\xa8\xe9\x8e\x89Q\xdb}\xa3\x96\xdcHRO\xb1\xa8\xbda\x1aZ\xa2\xa2C/0\xabB\nzm2@\xc7\x18\xcf\x03\x1f\xa9\xefL\x9a\xd5P Z\xa0)Q\xdfJ\x1dl\x84!\xb0\x15\xb7i\xdb\x8c\x92)\x83~\xa2\xbe\x8b\x1b\r9\xd0\xeb\xa9\xc5\x14\x84\xef\xdb\x8c\x0e\xfd\x8d%\x8d\xaf\t\xd1\xa2\x14P\x96\x1c\xbb>\xa8\xa9VC;x\x1f\x1c\xe3=\xfe\xd5O\x0e+P\xa2\xb7\x1d\x84\xedm\xb1\x80(\xa2\x81u\xf7O\xc6\xbd\xa1\x05\xc5)\xa7\x91\xc2\\O dict: + data: dict, + doctype: str, + validity: dict = None, + devicekeyinfo: Union[dict, CoseKey, str] = None, + cert_path: str = None, + ): """ create a new mdoc with signed mso - - :param data: the data to sign - Can be a dict, representing the single document, or a list of dicts containg the doctype and the data - Example: - {doctype: "org.iso.18013.5.1.mDL", data: {...}} - :type data: dict | list[dict] - :param devicekeyinfo: the device key info - :type devicekeyinfo: dict | CoseKey - :param doctype: the document type (optional if data is a list) - :type doctype: str | None - - :return: the signed mdoc - :rtype: dict """ if isinstance(devicekeyinfo, dict): devicekeyinfo = CoseKey.from_dict(devicekeyinfo) + if isinstance(devicekeyinfo, str): + device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8")) + public_key = serialization.load_pem_public_key(device_key_bytes) + curve_name = public_key.curve.name + curve_map = { + "secp256r1": 1, # NIST P-256 + "secp384r1": 2, # NIST P-384 + "secp521r1": 3, # NIST P-521 + "brainpoolP256r1": 8, # Brainpool P-256 + "brainpoolP384r1": 9, # Brainpool P-384 + "brainpoolP512r1": 10, # Brainpool P-512 + # Add more curve mappings as needed + } + curve_identifier = curve_map.get(curve_name) + + # Extract the x and y coordinates from the public key + x = public_key.public_numbers().x.to_bytes( + (public_key.public_numbers().x.bit_length() + 7) + // 8, # Number of bytes needed + "big", # Byte order + ) + + y = public_key.public_numbers().y.to_bytes( + (public_key.public_numbers().y.bit_length() + 7) + // 8, # Number of bytes needed + "big", # Byte order + ) + + devicekeyinfo = { + 1: 2, + -1: curve_identifier, + -2: x, + -3: y, + } + else: devicekeyinfo: CoseKey = devicekeyinfo - if isinstance(data, dict): - data = [{"doctype": doctype, "data": data}] - - documents = [] + if self.hsm: + msoi = MsoIssuer( + data=data, + cert_path=cert_path, + hsm=self.hsm, + key_label=self.key_label, + user_pin=self.user_pin, + lib_path=self.lib_path, + slot_id=self.slot_id, + alg=self.alg, + kid=self.kid, + validity=validity, + ) - for doc in data: + else: msoi = MsoIssuer( - data=doc["data"], - private_key=self.private_key + data=data, + private_key=self.private_key, + alg=self.alg, + cert_path=cert_path, + validity=validity, ) - mso = msoi.sign() - - document = { - 'docType': doc["doctype"], # 'org.iso.18013.5.1.mDL' - 'issuerSigned': { - "nameSpaces": { - ns: [ - cbor2.CBORTag(24, value={k: v}) for k, v in dgst.items() - ] - for ns, dgst in msoi.disclosure_map.items() - }, - "issuerAuth": mso.encode() - }, - # this is required during the presentation. - # 'deviceSigned': { - # # TODO - # } - } + mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo) + + mso_cbor = mso.encode( + tag=False, + hsm=self.hsm, + key_label=self.key_label, + user_pin=self.user_pin, + lib_path=self.lib_path, + slot_id=self.slot_id, + ) + + # TODO: for now just a single document, it would be trivial having + # also multiple but for now I don't have use cases for this + res = { + # "version": self.version, + # "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, + } - documents.append(document) + # print("mso diganostic notation: \n", cbor2diag(mso_cbor)) - self.signed = { - 'version': self.version, - 'documents': documents, - 'status': self.status - } + self.signed = res return self.signed - + def dump(self): """ - Returns the signed mdoc in CBOR format - - :return: the signed mdoc in CBOR format - :rtype: bytes + returns bytes """ - return cbor2.dumps(self.signed) + return cbor2.dumps(self.signed, canonical=True) def dumps(self): """ - Returns the signed mdoc in AF binary repr - - :return: the signed mdoc in AF binary repr - :rtype: bytes + returns AF binary repr """ - return binascii.hexlify(cbor2.dumps(self.signed)) + return binascii.hexlify(cbor2.dumps(self.signed, canonical=True)) diff --git a/pymdoccbor/mdoc/issuersigned.py b/pymdoccbor/mdoc/issuersigned.py index f50fd00..1c39b84 100644 --- a/pymdoccbor/mdoc/issuersigned.py +++ b/pymdoccbor/mdoc/issuersigned.py @@ -2,13 +2,10 @@ from typing import Union from pymdoccbor.mso.verifier import MsoVerifier -from pymdoccbor.mdoc.exceptions import MissingIssuerAuth class IssuerSigned: """ - IssuerSigned helper class to handle issuer signed data - nameSpaces provides the definition within which the data elements of the document are defined. A document may have multiple nameSpaces. @@ -25,43 +22,19 @@ class IssuerSigned: ] """ - def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]) -> None: - """ - Create a new IssuerSigned instance - - :param nameSpaces: the namespaces - :type nameSpaces: dict - :param issuerAuth: the issuer auth - :type issuerAuth: dict | bytes - - :raises MissingIssuerAuth: if no issuer auth is provided - """ + def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]): self.namespaces: dict = nameSpaces - if not issuerAuth: - raise MissingIssuerAuth("issuerAuth must be provided") - + # if isinstance(ia, dict): self.issuer_auth = MsoVerifier(issuerAuth) def dump(self) -> dict: - """ - Returns a dict representation of the issuer signed data - - :return: the issuer signed data as dict - :rtype: dict - """ return { 'nameSpaces': self.namespaces, 'issuerAuth': self.issuer_auth } - def dumps(self) -> bytes: - """ - Returns a CBOR representation of the issuer signed data - - :return: the issuer signed data as CBOR - :rtype: bytes - """ + def dumps(self) -> dict: return cbor2.dumps( { 'nameSpaces': self.namespaces, diff --git a/pymdoccbor/mdoc/verifier.py b/pymdoccbor/mdoc/verifier.py index d9ddf51..4eea8df 100644 --- a/pymdoccbor/mdoc/verifier.py +++ b/pymdoccbor/mdoc/verifier.py @@ -6,57 +6,25 @@ from pymdoccbor.exceptions import InvalidMdoc from pymdoccbor.mdoc.issuersigned import IssuerSigned -from pymdoccbor.mdoc.exceptions import NoDocumentTypeProvided, NoSignedDocumentProvided logger = logging.getLogger('pymdoccbor') class MobileDocument: - """ - MobileDocument helper class to verify a mdoc - """ - _states = { True: "valid", False: "failed", } def __init__(self, docType: str, issuerSigned: dict, deviceSigned: dict = {}): - """ - Create a new MobileDocument instance - - :param docType: the document type - :type docType: str - :param issuerSigned: the issuer signed data - :type issuerSigned: dict - :param deviceSigned: the device signed data - :type deviceSigned: dict - - :raises NoDocumentTypeProvided: if no document type is provided - :raises NoSignedDocumentProvided: if no signed document is provided - """ - - if not docType: - raise NoDocumentTypeProvided("You must provide a document type") - - if not issuerSigned: - raise NoSignedDocumentProvided("You must provide a signed document") - self.doctype: str = docType # eg: 'org.iso.18013.5.1.mDL' - self.issuersigned: IssuerSigned = IssuerSigned(**issuerSigned) + self.issuersigned: List[IssuerSigned] = IssuerSigned(**issuerSigned) self.is_valid = False # TODO self.devicesigned: dict = deviceSigned def dump(self) -> dict: - """ - Returns a dict representation of the document - - :return: the document as dict - :rtype: dict - """ - return { 'docType': self.doctype, 'issuerSigned': self.issuersigned.dump() @@ -64,35 +32,23 @@ def dump(self) -> dict: def dumps(self) -> str: """ - Returns an AF binary repr of the document - - :return: the document as AF binary - :rtype: str + returns an AF binary repr of the document """ return binascii.hexlify(self.dump()) def dump(self) -> bytes: """ - Returns a CBOR repr of the document - - :return: the document as CBOR - :rtype: bytes + returns bytes """ return cbor2.dumps( cbor2.CBORTag(24, value={ 'docType': self.doctype, 'issuerSigned': self.issuersigned.dumps() - }) + } + ) ) def verify(self) -> bool: - """ - Verify the document signature - - :return: True if valid, False otherwise - :rtype: bool - """ - self.is_valid = self.issuersigned.issuer_auth.verify_signature() return self.is_valid @@ -109,14 +65,13 @@ def __init__(self): self.documents: List[MobileDocument] = [] self.documents_invalid: list = [] - def load(self, data: bytes): - data = binascii.hexlify(data) - return self.loads(data) - def loads(self, data: str): """ data is a AF BINARY """ + if isinstance(data, bytes): + data = binascii.hexlify(data) + self.data_as_bytes = binascii.unhexlify(data) self.data_as_cbor_dict = cbor2.loads(self.data_as_bytes) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 94afe2e..1e639f5 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -1,179 +1,220 @@ +# Modifications have been made to the original file (available at https://github.com/IdentityPython/pyMDOC-CBOR) +# All modifications Copyright (c) 2023 European Commission + +# All modifications licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import cbor2 import datetime import hashlib import secrets import uuid -from pycose.headers import Algorithm, KID -from pycose.keys import CoseKey, EC2Key +from pycose.headers import Algorithm +from pycose.keys import CoseKey from pycose.messages import Sign1Message from typing import Union -from pymdoccbor.exceptions import ( - MsoPrivateKeyRequired -) + +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 cbor_diag import * class MsoIssuer(MsoX509Fabric): - """ - MsoIssuer helper class to create a new mso - """ + """ """ def __init__( self, data: dict, - private_key: Union[dict, EC2Key, CoseKey], - digest_alg: str = settings.PYMDOC_HASHALG + validity: str, + cert_path: str = None, + key_label: str = None, + user_pin: str = None, + lib_path: str = None, + slot_id: int = None, + kid: str = None, + alg: str = None, + hsm: bool = False, + private_key: Union[dict, CoseKey] = None, + digest_alg: str = settings.PYMDOC_HASHALG, ): - """ - Create a new MsoIssuer instance - - :param data: the data to sign - :type data: dict - :param private_key: the private key to sign the mso - :type private_key: dict | CoseKey - :param digest_alg: the digest algorithm to use - :type digest_alg: str - - :raises MsoPrivateKeyRequired: if no private key is provided - """ - - if private_key and isinstance(private_key, dict): - self.private_key = CoseKey.from_dict(private_key) - if not self.private_key.kid: - self.private_key.kid = str(uuid.uuid4()) - elif private_key and isinstance(private_key, CoseKey): - self.private_key = private_key - elif private_key and isinstance(private_key, EC2Key): - ec2_encoded = private_key.encode() - ec2_decoded = CoseKey.decode(ec2_encoded) - self.private_key = ec2_decoded - else: - raise MsoPrivateKeyRequired( - "MSO Writer requires a valid private key" - ) - - self.public_key = EC2Key( - crv=self.private_key.crv, - x=self.private_key.x, - y=self.private_key.y - ) + if not hsm: + if private_key and isinstance(private_key, dict): + self.private_key = CoseKey.from_dict(private_key) + if not self.private_key.kid: + self.private_key.kid = str(uuid.uuid4()) + elif private_key and isinstance(private_key, CoseKey): + self.private_key = private_key + else: + raise MsoPrivateKeyRequired("MSO Writer requires a valid private key") self.data: dict = data self.hash_map: dict = {} + self.cert_path = cert_path self.disclosure_map: dict = {} self.digest_alg: str = digest_alg + self.key_label = key_label + self.user_pin = user_pin + self.lib_path = lib_path + self.slot_id = slot_id + self.hsm = hsm + self.alg = alg + self.kid = kid + self.validity = validity - hashfunc = getattr( - hashlib, - settings.HASHALG_MAP[settings.PYMDOC_HASHALG] - ) + alg_map = {"ES256": "sha256", "ES384": "sha384", "ES512": "sha512"} + + hashfunc = getattr(hashlib, alg_map.get(self.alg)) digest_cnt = 0 for ns, values in data.items(): self.disclosure_map[ns] = {} self.hash_map[ns] = {} for k, v in shuffle_dict(values).items(): - _rnd_salt = secrets.token_bytes(settings.DIGEST_SALT_LENGTH) - + _value_cbortag = settings.CBORTAGS_ATTR_MAP.get(k, None) - + if _value_cbortag: v = cbor2.CBORTag(_value_cbortag, value=v) - - self.disclosure_map[ns][digest_cnt] = { - 'digestID': digest_cnt, - 'random': _rnd_salt, - 'elementIdentifier': k, - 'elementValue': v - } + # print("\n-----\n K,V ", k, "\n", v) + + if k == "driving_privileges": + for item in v: + for k2, v2 in item.items(): + _value_cbortag = settings.CBORTAGS_ATTR_MAP.get(k2, None) + if _value_cbortag: + item[k2] = cbor2.CBORTag(_value_cbortag, value=v2) + + self.disclosure_map[ns][digest_cnt] = cbor2.CBORTag( + 24, + value=cbor2.dumps( + { + "digestID": digest_cnt, + "random": _rnd_salt, + "elementIdentifier": k, + "elementValue": v, + }, + canonical=True, + ), + ) self.hash_map[ns][digest_cnt] = hashfunc( - cbor2.dumps( - cbor2.CBORTag( - 24, - value=cbor2.dumps( - self.disclosure_map[ns][digest_cnt] - ) - ) - ) + cbor2.dumps(self.disclosure_map[ns][digest_cnt], canonical=True) ).digest() digest_cnt += 1 - def format_datetime_repr(self, dt: datetime.datetime) -> str: - """ - Format a datetime object to a string representation - - :param dt: the datetime object - :type dt: datetime.datetime - - :return: the string representation - :rtype: str - """ - return dt.isoformat().split('.')[0] + 'Z' + def format_datetime_repr(self, dt: datetime.datetime): + return dt.isoformat().split(".")[0] + "Z" def sign( self, device_key: Union[dict, None] = None, valid_from: Union[None, datetime.datetime] = None, - doctype: str | None = None + doctype: str = None, ) -> Sign1Message: """ - Sign a mso and returns it - - :param device_key: the device key info - :type device_key: dict | None - :param valid_from: the validity start date - :type valid_from: datetime.datetime | None - :param doctype: the document type - :type doctype: str - - :return: the signed mso - :rtype: Sign1Message + sign a mso and returns itprivate_key """ utcnow = datetime.datetime.utcnow() + valid_from = datetime.datetime.strptime( + self.validity["issuance_date"], "%Y-%m-%d" + ) if settings.PYMDOC_EXP_DELTA_HOURS: - exp = utcnow + datetime.timedelta( - hours=settings.PYMDOC_EXP_DELTA_HOURS - ) + exp = utcnow + datetime.timedelta(hours=settings.PYMDOC_EXP_DELTA_HOURS) else: # five years - exp = utcnow + datetime.timedelta(hours=(24 * 365) * 5) + exp = datetime.datetime.strptime(self.validity["expiry_date"], "%Y-%m-%d") + # exp = utcnow + datetime.timedelta(hours=(24 * 365) * 5) + + if utcnow > valid_from: + valid_from = utcnow + + alg_map = {"ES256": "SHA-256", "ES384": "SHA-384", "ES512": "SHA-512"} payload = { - 'version': '1.0', - 'digestAlgorithm': settings.HASHALG_MAP[settings.PYMDOC_HASHALG], - 'valueDigests': self.hash_map, - 'deviceKeyInfo': { - 'deviceKey': device_key + "docType": doctype or list(self.hash_map)[0], + "version": "1.0", + "validityInfo": { + "signed": cbor2.CBORTag(0, self.format_datetime_repr(utcnow)), + "validFrom": cbor2.CBORTag( + 0, self.format_datetime_repr(valid_from or utcnow) + ), + "validUntil": cbor2.CBORTag(0, self.format_datetime_repr(exp)), }, - 'docType': doctype or list(self.hash_map)[0], - 'validityInfo': { - 'signed': cbor2.dumps(cbor2.CBORTag(0, self.format_datetime_repr(utcnow))), - 'validFrom': cbor2.dumps(cbor2.CBORTag(0, self.format_datetime_repr(valid_from or utcnow))), - 'validUntil': cbor2.dumps(cbor2.CBORTag(0, self.format_datetime_repr(exp))) - } - } - - _cert = settings.X509_DER_CERT or self.selfsigned_x509cert() - - mso = Sign1Message( - phdr={ - Algorithm: self.private_key.alg, - KID: self.private_key.kid, - 33: self.selfsigned_x509cert() + "valueDigests": self.hash_map, + "deviceKeyInfo": { + "deviceKey": device_key, }, - # TODO: x509 (cbor2.CBORTag(33)) and federation trust_chain support (cbor2.CBORTag(27?)) here - # 33 means x509chain standing to rfc9360 - # in both protected and unprotected for interop purpose .. for now. - uhdr={33: _cert}, - payload=cbor2.dumps(payload) - ) - mso.key = self.private_key + "digestAlgorithm": alg_map.get(self.alg), + } + + if self.cert_path: + # Load the DER 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")) + else: + _cert = self.selfsigned_x509cert() + + if self.hsm: + # print("payload diganostic notation: \n",cbor2diag(cbor2.dumps(cbor2.CBORTag(24, cbor2.dumps(payload))))) + + mso = Sign1Message( + phdr={ + Algorithm: self.alg, + # 33: _cert + }, + # TODO: x509 (cbor2.CBORTag(33)) and federation trust_chain support (cbor2.CBORTag(27?)) here + # 33 means x509chain standing to rfc9360 + # in both protected and unprotected for interop purpose .. for now. + uhdr={33: _cert}, + payload=cbor2.dumps( + cbor2.CBORTag(24, cbor2.dumps(payload, canonical=True)), + canonical=True, + ), + ) + + else: + # print("payload diganostic notation: \n", cbor2diag(cbor2.dumps(cbor2.CBORTag(24,cbor2.dumps(payload))))) + + mso = Sign1Message( + phdr={ + Algorithm: self.private_key.alg, + # KID: self.private_key.kid, + # 33: _cert + }, + # TODO: x509 (cbor2.CBORTag(33)) and federation trust_chain support (cbor2.CBORTag(27?)) here + # 33 means x509chain standing to rfc9360 + # in both protected and unprotected for interop purpose .. for now. + uhdr={33: _cert}, + payload=cbor2.dumps( + cbor2.CBORTag(24, cbor2.dumps(payload, canonical=True)), + canonical=True, + ), + ) + + mso.key = self.private_key + return mso diff --git a/pymdoccbor/mso/verifier.py b/pymdoccbor/mso/verifier.py index a314491..cd6284f 100644 --- a/pymdoccbor/mso/verifier.py +++ b/pymdoccbor/mso/verifier.py @@ -2,9 +2,11 @@ import cryptography import logging -from pycose.keys import EC2Key +from pycose.keys import CoseKey, EC2Key from pycose.messages import Sign1Message +from typing import Optional + from pymdoccbor.exceptions import ( MsoX509ChainNotFound, UnsupportedMsoDataFormat @@ -18,8 +20,6 @@ class MsoVerifier: """ - MsoVerifier helper class to verify a mso - Parameters data: CBOR TAG 24 @@ -31,15 +31,7 @@ class MsoVerifier: structure as defined in RFC 8152. """ - def __init__(self, data: cbor2.CBORTag) -> None: - """ - Create a new MsoParser instance - - :param data: the data to verify - :type data: cbor2.CBORTag - - :raises UnsupportedMsoDataFormat: if the data format is not supported - """ + def __init__(self, data: cbor2.CBORTag): self._data = data # not used if isinstance(self._data, bytes): @@ -52,35 +44,23 @@ def __init__(self, data: cbor2.CBORTag) -> None: f"MsoParser only supports raw bytes and list, a {type(data)} was provided" ) - self.object.key = None + self.object.key: Optional[CoseKey, None] = None self.public_key: cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey = None self.x509_certificates: list = [] @property - def payload_as_cbor(self) -> dict: + def payload_as_cbor(self): """ - Return the decoded payload - - :return: the decoded payload - :rtype: dict + return the decoded payload """ return cbor2.loads(self.object.payload) @property - def payload_as_raw(self) -> bytes: - """ - Return the raw payload - - :return: the raw payload - :rtype: bytes - """ + def payload_as_raw(self): return self.object.payload @property - def payload_as_dict(self) -> dict: - """ - Return the payload as dict - """ + def payload_as_dict(self): return cbor2.loads( cbor2.loads(self.object.payload).value ) @@ -88,13 +68,8 @@ def payload_as_dict(self) -> dict: @property def raw_public_keys(self) -> bytes: """ - It returns the public key extract from x509 certificates - looking to both phdr and uhdr - - :raises MsoX509ChainNotFound: if no valid x509 certificate is found - - :return: the raw public key - :rtype: bytes + it returns the public key extract from x509 certificates + looking to both phdr and uhdr """ _mixed_heads = self.object.phdr.items() | self.object.uhdr.items() for h, v in _mixed_heads: @@ -106,7 +81,7 @@ def raw_public_keys(self) -> bytes: "in this MSO." ) - def attest_public_key(self) -> None: + def attest_public_key(self): logger.warning( "TODO: in next releases. " "The certificate is to be considered as untrusted, this release " @@ -114,10 +89,7 @@ def attest_public_key(self) -> None: "python certvalidator or cryptography for that." ) - def load_public_key(self) -> None: - """ - Load the public key from the x509 certificate - """ + def load_public_key(self): self.attest_public_key() @@ -130,18 +102,14 @@ def load_public_key(self) -> None: key = EC2Key( 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') + x=self.public_key.public_numbers().x.to_bytes( + settings.CRV_LEN_MAP[self.public_key.curve.name], 'big' + ) ) self.object.key = key def verify_signature(self) -> bool: - """ - Verify the signature - :return: True if valid, False otherwise - :rtype: bool - """ if not self.object.key: self.load_public_key() diff --git a/pymdoccbor/settings.py b/pymdoccbor/settings.py index 8ba2777..6527225 100644 --- a/pymdoccbor/settings.py +++ b/pymdoccbor/settings.py @@ -1,55 +1,47 @@ import datetime import os -COSEKEY_HAZMAT_CRV_MAP = { - "secp256r1": "P_256", - "secp384r1": "P_384", - "secp521r1": "P_521" -} +COSEKEY_HAZMAT_CRV_MAP = {"secp256r1": "P_256"} CRV_LEN_MAP = { "secp256r1": 32, - "secp384r1": 48, - "secp521r1": 66 } -PYMDOC_HASHALG: str = os.getenv('PYMDOC_HASHALG', "SHA-256") -PYMDOC_EXP_DELTA_HOURS: int = os.getenv('PYMDOC_EXP_DELTA_HOURS', 0) +PYMDOC_HASHALG: str = os.getenv("PYMDOC_HASHALG", "SHA-256") +PYMDOC_EXP_DELTA_HOURS: int = os.getenv("PYMDOC_EXP_DELTA_HOURS", 0) HASHALG_MAP = { "SHA-256": "sha256", "SHA-512": "sha512", - } DIGEST_SALT_LENGTH = 32 -X509_DER_CERT = os.getenv('X509_DER_CERT', None) + +X509_DER_CERT = os.getenv("X509_DER_CERT", None) # OR -X509_COUNTRY_NAME = os.getenv('X509_COUNTRY_NAME', u"US") -X509_STATE_OR_PROVINCE_NAME = os.getenv('X509_STATE_OR_PROVINCE_NAME', u"California") -X509_LOCALITY_NAME = os.getenv('X509_LOCALITY_NAME', u"San Francisco") -X509_ORGANIZATION_NAME = os.getenv('X509_ORGANIZATION_NAME', u"My Company") -X509_COMMON_NAME = os.getenv('X509_COMMON_NAME', u"mysite.com") - -X509_NOT_VALID_BEFORE = os.getenv('X509_NOT_VALID_BEFORE', datetime.datetime.utcnow()) -X509_NOT_VALID_AFTER_DAYS = os.getenv('X509_NOT_VALID_AFTER_DAYS', 10) -X509_NOT_VALID_AFTER = os.getenv( - 'X509_NOT_VALID_AFTER', - datetime.datetime.utcnow() + datetime.timedelta( - days=X509_NOT_VALID_AFTER_DAYS - ) +X509_COUNTRY_NAME = os.getenv("X509_COUNTRY_NAME", "US") +X509_STATE_OR_PROVINCE_NAME = os.getenv("X509_STATE_OR_PROVINCE_NAME", "California") +X509_LOCALITY_NAME = os.getenv("X509_LOCALITY_NAME", "San Francisco") +X509_ORGANIZATION_NAME = os.getenv("X509_ORGANIZATION_NAME", "My Company") +X509_COMMON_NAME = os.getenv("X509_COMMON_NAME", "mysite.com") + +X509_NOT_VALID_BEFORE = os.getenv("X509_NOT_VALID_BEFORE", datetime.datetime.utcnow()) +X509_NOT_VALID_AFTER_DAYS = os.getenv("X509_NOT_VALID_AFTER_DAYS", 10) +X509_NOT_VALID_AFTER = os.getenv( + "X509_NOT_VALID_AFTER", + datetime.datetime.utcnow() + datetime.timedelta(days=X509_NOT_VALID_AFTER_DAYS), ) X509_SAN_URL = os.getenv( - 'X509_SAN_URL', u"https://credential-issuer.oidc-federation.online" + "X509_SAN_URL", "https://credential-issuer.oidc-federation.online" ) CBORTAGS_ATTR_MAP = { "birth_date": 1004, "expiry_date": 1004, - "issue_date": 1004 + "issue_date": 1004, + "issuance_date": 1004, } - diff --git a/pymdoccbor/tests/micov_data.py b/pymdoccbor/tests/micov_data.py deleted file mode 100644 index c219f74..0000000 --- a/pymdoccbor/tests/micov_data.py +++ /dev/null @@ -1,23 +0,0 @@ -MICOV_DATA = { - "org.micov.medical.1":{ - "last_name": "Rossi", - "given_name": "Mario", - "birth_date": "1922-03-13", - "PersonId_nic": { - "PersonIdNumber": "1234567890", - "PersonIdType": "nic", - "PersonIdIS": "IT", - }, - "sex": 1, - "VPInfo_COVID-19_1": { - "VaccineProphylaxis": "", - "VaccMedicinalProd": "Moderna", - "VaccMktAuthHolder": "Moderna", - "VaccDoseNumber": "2/2", - "VaccAdmDate": "2021-01-01", - "VaccCountry": "IT", - }, - "CertIssuer": "Italian Ministry of Health", - "CertId": "1234567890", - } -} \ No newline at end of file diff --git a/pymdoccbor/tests/pkey.py b/pymdoccbor/tests/pkey.py deleted file mode 100644 index 65a001a..0000000 --- a/pymdoccbor/tests/pkey.py +++ /dev/null @@ -1,5 +0,0 @@ -from pycose.keys import EC2Key - -encoded_pkey = b'\xa6\x01\x02\x03& \x01!X \x8d%C\x91\xe8\x17A\xe1\xc2\xc1\'J\xa7\x1e\xe6J\x03\xc4\xc9\x8a\x91 hV\xcd\x10yb\x9f\xf7\xbe\x9a"X H\x8a\xc3\xd4\xc2\xea\x9bX\x9d\x9d\xf1~\x0c!\x92\xda\xfd\x02s\x0ci\xee\x190i\x88J\xddt\x14\x03\x95#X \xcd\xe1^\x92\xc8z\xd9&&\x0f\x0c\xbd\x8f4r}z\x03\x83\xe0\xf2\x8e\xcc\x04\x13M\xe1\xafXH\xcbT' - -PKEY = EC2Key.decode(encoded_pkey) \ No newline at end of file diff --git a/pymdoccbor/tests/test_01_mdoc_parser.py b/pymdoccbor/tests/test_01_mdoc_parser.py index 5d89b70..bb52ce8 100644 --- a/pymdoccbor/tests/test_01_mdoc_parser.py +++ b/pymdoccbor/tests/test_01_mdoc_parser.py @@ -23,7 +23,7 @@ def test_parse_mdoc_af_binary(): # testing from export re-import mdoc2 = MdocCbor() - mdoc2.load(mdoc.data_as_bytes) + mdoc2.loads(mdoc.data_as_bytes) mdoc2.verify() for i in mdoc.documents: diff --git a/pymdoccbor/tests/test_02_mdoc_issuer.py b/pymdoccbor/tests/test_02_mdoc_issuer.py index 7ac300b..8c232f4 100644 --- a/pymdoccbor/tests/test_02_mdoc_issuer.py +++ b/pymdoccbor/tests/test_02_mdoc_issuer.py @@ -2,13 +2,20 @@ import os from pycose.messages import Sign1Message -from pycose.keys import EC2Key + from pymdoccbor.mdoc.issuer import MdocCborIssuer from pymdoccbor.mdoc.verifier import MdocCbor from pymdoccbor.mso.issuer import MsoIssuer -from pymdoccbor.tests.pid_data import PID_DATA -from pymdoccbor.tests.pkey import PKEY +from . pid_data import PID_DATA + +PKEY = { + 'KTY': 'EC2', + 'CURVE': 'P_256', + 'ALG': 'ES256', + 'D': os.urandom(32), + 'KID': b"demo-kid" +} def test_mso_writer(): @@ -40,10 +47,9 @@ def test_mdoc_issuer(): mdocp = MdocCbor() aa = cbor2.dumps(mdoc) - mdocp.load(aa) + mdocp.loads(aa) mdocp.verify() mdoci.dump() mdoci.dumps() - diff --git a/pymdoccbor/tests/test_03_mdoc_issuer.py b/pymdoccbor/tests/test_03_mdoc_issuer.py deleted file mode 100644 index 77dbf73..0000000 --- a/pymdoccbor/tests/test_03_mdoc_issuer.py +++ /dev/null @@ -1,57 +0,0 @@ -from pycose.keys import EC2Key -from pymdoccbor.mdoc.issuer import MdocCborIssuer -from pymdoccbor.tests.micov_data import MICOV_DATA -from pymdoccbor.tests.pid_data import PID_DATA -from pymdoccbor.tests.pkey import PKEY - -mdoc = MdocCborIssuer(PKEY) - -def test_MdocCborIssuer_creation(): - assert mdoc.version == '1.0' - assert mdoc.status == 0 - -def test_mdoc_without_private_key_must_fail(): - try: - MdocCborIssuer(None) - except Exception as e: - assert str(e) == "You must provide a private key" - -def test_MdocCborIssuer_new_single(): - mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" - ) - assert mdoc.signed['version'] == '1.0' - assert mdoc.signed['status'] == 0 - assert mdoc.signed['documents'][0]['docType'] == 'org.micov.medical.1' - assert mdoc.signed['documents'][0]['issuerSigned']['nameSpaces']['org.micov.medical.1'][0].tag == 24 - -def test_MdocCborIssuer_new_multiple(): - micov_data = {"doctype": "org.micov.medical.1", "data": MICOV_DATA} - pid_data = {"doctype": "eu.europa.ec.eudiw.pid.1", "data": PID_DATA} - - mdoc.new( - data=[micov_data, pid_data], - devicekeyinfo=PKEY # TODO - ) - assert mdoc.signed['version'] == '1.0' - assert mdoc.signed['status'] == 0 - assert mdoc.signed['documents'][0]['docType'] == 'org.micov.medical.1' - assert mdoc.signed['documents'][0]['issuerSigned']['nameSpaces']['org.micov.medical.1'][0].tag == 24 - assert mdoc.signed['documents'][1]['docType'] == 'eu.europa.ec.eudiw.pid.1' - assert mdoc.signed['documents'][1]['issuerSigned']['nameSpaces']['eu.europa.ec.eudiw.pid.1'][0].tag == 24 - -def test_MdocCborIssuer_dump(): - dump = mdoc.dump() - - assert dump - assert isinstance(dump, bytes) - assert len(dump) > 0 - -def test_MdocCborIssuer_dumps(): - dumps = mdoc.dumps() - - assert dumps - assert isinstance(dumps, bytes) - assert len(dumps) > 0 \ No newline at end of file diff --git a/pymdoccbor/tests/test_04_issuer_signed.py b/pymdoccbor/tests/test_04_issuer_signed.py deleted file mode 100644 index 51abbdf..0000000 --- a/pymdoccbor/tests/test_04_issuer_signed.py +++ /dev/null @@ -1,46 +0,0 @@ -from pycose.keys import EC2Key -from pymdoccbor.mdoc.issuersigned import IssuerSigned -from pymdoccbor.mdoc.issuer import MdocCborIssuer -from pymdoccbor.tests.micov_data import MICOV_DATA -from pymdoccbor.tests.test_03_mdoc_issuer import mdoc -from pymdoccbor.tests.pkey import PKEY - - -mdoc = MdocCborIssuer(PKEY) -mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" -) -issuerAuth = mdoc.signed["documents"][0]["issuerSigned"] -issuer_signed = IssuerSigned(**issuerAuth) - -def test_issuer_signed_fail(): - try: - IssuerSigned(None, None) - except Exception as e: - assert str(e) == "issuerAuth must be provided" - -def test_issuer_signed_creation(): - assert issuer_signed.namespaces - assert issuer_signed.issuer_auth - -def test_issuer_signed_dump(): - issuerAuth = mdoc.signed["documents"][0]["issuerSigned"] - - issuer_signed = IssuerSigned(**issuerAuth) - - dump = issuer_signed.dump() - assert dump - assert dump["nameSpaces"] == issuer_signed.namespaces - assert dump["issuerAuth"] == issuer_signed.issuer_auth - -def test_issuer_signed_dumps(): - issuerAuth = mdoc.signed["documents"][0]["issuerSigned"] - - issuer_signed = IssuerSigned(**issuerAuth) - - dumps = issuer_signed.dumps() - assert dumps - assert isinstance(dumps, bytes) - assert len(dumps) > 0 \ No newline at end of file diff --git a/pymdoccbor/tests/test_05_mdoc_verifier.py b/pymdoccbor/tests/test_05_mdoc_verifier.py deleted file mode 100644 index 4a7ff63..0000000 --- a/pymdoccbor/tests/test_05_mdoc_verifier.py +++ /dev/null @@ -1,79 +0,0 @@ -from pycose.keys import EC2Key -from pymdoccbor.mdoc.verifier import MobileDocument -from pymdoccbor.mdoc.issuer import MdocCborIssuer -from pymdoccbor.tests.micov_data import MICOV_DATA -from pymdoccbor.tests.pkey import PKEY - -def test_verifier_must_fail_document_type(): - try: - MobileDocument(None, None) - except Exception as e: - assert str(e) == "You must provide a document type" - -def test_verifier_must_fail_issuer_signed(): - try: - MobileDocument("org.micov.medical.1", None) - except Exception as e: - assert str(e) == "You must provide a signed document" - -def test_mobile_document(): - mdoc = MdocCborIssuer(PKEY) - mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" - ) - - - document = mdoc.signed["documents"][0] - doc = MobileDocument(**document) - - assert doc.doctype == "org.micov.medical.1" - assert doc.issuersigned - -def test_mobile_document_dump(): - mdoc = MdocCborIssuer(PKEY) - mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" - ) - - - document = mdoc.signed["documents"][0] - doc = MobileDocument(**document) - - dump = doc.dump() - assert dump - assert isinstance(dump, bytes) - assert len(dump) > 0 - -def test_mobile_document_dumps(): - mdoc = MdocCborIssuer(PKEY) - mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" - ) - - - document = mdoc.signed["documents"][0] - doc = MobileDocument(**document) - - dumps = doc.dumps() - assert dumps - assert isinstance(dumps, bytes) - assert len(dumps) > 0 - -def test_mobile_document_verify(): - mdoc = MdocCborIssuer(PKEY) - mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" - ) - - document = mdoc.signed["documents"][0] - doc = MobileDocument(**document) - - assert doc.verify() \ No newline at end of file diff --git a/pymdoccbor/tests/test_06_mso_issuer.py b/pymdoccbor/tests/test_06_mso_issuer.py deleted file mode 100644 index fbe2a7f..0000000 --- a/pymdoccbor/tests/test_06_mso_issuer.py +++ /dev/null @@ -1,34 +0,0 @@ -from pycose.keys import EC2Key -from pycose.messages import CoseMessage -from pymdoccbor.mso.issuer import MsoIssuer -from pymdoccbor.tests.micov_data import MICOV_DATA -from pymdoccbor.tests.pkey import PKEY - - -def test_mso_issuer_fail(): - try: - MsoIssuer(None, None) - except Exception as e: - assert str(e) == "MSO Writer requires a valid private key" - -def test_mso_issuer_creation(): - msoi = MsoIssuer( - data=MICOV_DATA, - private_key=PKEY - ) - - assert msoi.private_key - assert msoi.public_key - assert msoi.data - assert msoi.hash_map - assert list(msoi.hash_map.keys())[0] == 'org.micov.medical.1' - assert msoi.disclosure_map['org.micov.medical.1'] - -def test_mso_issuer_sign(): - msoi = MsoIssuer( - data=MICOV_DATA, - private_key=PKEY - ) - - mso = msoi.sign() - assert isinstance(mso, CoseMessage) diff --git a/pymdoccbor/tests/test_07_mso_verifier.py b/pymdoccbor/tests/test_07_mso_verifier.py deleted file mode 100644 index d6b1aae..0000000 --- a/pymdoccbor/tests/test_07_mso_verifier.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -from pycose.keys import CoseKey, EC2Key -from pymdoccbor.mso.verifier import MsoVerifier -from pymdoccbor.mdoc.issuer import MdocCborIssuer -from pymdoccbor.tests.micov_data import MICOV_DATA -from pycose.messages import CoseMessage -from pymdoccbor.tests.pkey import PKEY - - -mdoc = MdocCborIssuer(PKEY) -mdoc.new( - data=MICOV_DATA, - devicekeyinfo=PKEY, # TODO - doctype="org.micov.medical.1" -) - -def test_mso_verifier_fail(): - try: - MsoVerifier(None) - except Exception as e: - assert str(e) == "MsoParser only supports raw bytes and list, a was provided" - -def test_mso_verifier_creation(): - issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] - - msov = MsoVerifier(issuerAuth) - - assert isinstance(msov.object, CoseMessage) - -def test_mso_verifier_verify_signatures(): - issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] - - msov = MsoVerifier(issuerAuth) - - assert msov.verify_signature() - -def test_mso_verifier_payload_as_cbor(): - issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] - - msov = MsoVerifier(issuerAuth) - - cbor = msov.payload_as_cbor - - assert cbor - assert cbor["version"] == "1.0" - assert cbor["digestAlgorithm"] == "sha256" - assert cbor["valueDigests"]["org.micov.medical.1"] - -def test_payload_as_raw(): - issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] - - msov = MsoVerifier(issuerAuth) - - raw = msov.payload_as_raw - - assert raw - assert isinstance(raw, bytes) - assert len(raw) > 0 \ No newline at end of file diff --git a/setup.py b/setup.py index bc1e583..bf0e902 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,42 @@ +# Modifications have been made to the original file (available at https://github.com/IdentityPython/pyMDOC-CBOR) +# All modifications Copyright (c) 2023 European Commission + +# All modifications licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import re from glob import glob from setuptools import setup + def readme(): - with open('README.md') as f: + with open("README.md") as f: return f.read() -_pkg_name = 'pymdoccbor' -with open(f'{_pkg_name}/__init__.py', 'r') as fd: - VERSION = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', - fd.read(), re.MULTILINE).group(1) +_pkg_name = "pymdoccbor" + +with open(f"{_pkg_name}/__init__.py", "r") as fd: + VERSION = re.search( + r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE + ).group(1) setup( name=_pkg_name, version=VERSION, description="Python parser and writer for Mobile Driving License and EUDI Wallet MDOC CBOR.", long_description=readme(), - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: Apache Software License", @@ -26,23 +44,24 @@ def readme(): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Topic :: Software Development :: Libraries :: Python Modules" + "Topic :: Software Development :: Libraries :: Python Modules", ], - url='https://github.com/peppelinux/pyMDL-MDOC', - author='Giuseppe De Marco', - author_email='demarcog83@gmail.com', - license='License :: OSI Approved :: Apache Software License', + url="https://github.com/peppelinux/pyMDL-MDOC", + author="Giuseppe De Marco", + author_email="demarcog83@gmail.com", + license="License :: OSI Approved :: Apache Software License", # scripts=[f'{_pkg_name}/bin/{_pkg_name}'], packages=[f"{_pkg_name}"], package_dir={f"{_pkg_name}": f"{_pkg_name}"}, - package_data={f"{_pkg_name}": [ - i.replace(f'{_pkg_name}/', '') - for i in glob(f'{_pkg_name}/**', recursive=True) + package_data={ + f"{_pkg_name}": [ + i.replace(f"{_pkg_name}/", "") + for i in glob(f"{_pkg_name}/**", recursive=True) ] }, install_requires=[ - 'cbor2>=5.4.0,<5.5.0', - 'cwt>=2.3.0,<2.4', - 'pycose>=1.0.1,<1.1.0' + "cbor2>=5.4.0,<5.5.0", + "cwt>=2.3.0,<2.4", + "pycose @ git+https://github.com/devisefutures/pycose.git@hsm", ], ) From ce8e898f4256f49f927a8c0a1ea8d23caae592d4 Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Mon, 14 Oct 2024 15:07:51 +0100 Subject: [PATCH 02/31] initial update for revocation status lists --- pymdoccbor/mdoc/issuer.py | 3 +++ pymdoccbor/mso/issuer.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index fc008c5..7503da8 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -62,6 +62,7 @@ def new( validity: dict = None, devicekeyinfo: Union[dict, CoseKey, str] = None, cert_path: str = None, + revocation: dict = None, ): """ create a new mdoc with signed mso @@ -118,6 +119,7 @@ def new( alg=self.alg, kid=self.kid, validity=validity, + revocation=revocation, ) else: @@ -127,6 +129,7 @@ def new( alg=self.alg, cert_path=cert_path, validity=validity, + revocation=revocation, ) mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 1e639f5..d7b4ce4 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -44,6 +44,7 @@ def __init__( self, data: dict, validity: str, + revocation: str = None, cert_path: str = None, key_label: str = None, user_pin: str = None, @@ -78,6 +79,7 @@ def __init__( self.alg = alg self.kid = kid self.validity = validity + self.revocation = revocation alg_map = {"ES256": "sha256", "ES384": "sha384", "ES512": "sha512"} @@ -167,6 +169,9 @@ def sign( "digestAlgorithm": alg_map.get(self.alg), } + if self.revocation is not None: + payload.update({"status": {"StatusListInfo": self.revocation}}) + if self.cert_path: # Load the DER certificate file with open(self.cert_path, "rb") as file: From 963cc8423c5a88fce64b9046b5f4596b986966ae Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Thu, 23 Jan 2025 10:51:55 +0000 Subject: [PATCH 03/31] update status struct --- pymdoccbor/mso/issuer.py | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index d7b4ce4..1286d0d 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -170,7 +170,7 @@ def sign( } if self.revocation is not None: - payload.update({"status": {"StatusListInfo": self.revocation}}) + payload.update({"status": self.revocation}) if self.cert_path: # Load the DER certificate file diff --git a/setup.py b/setup.py index bf0e902..305caea 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def readme(): install_requires=[ "cbor2>=5.4.0,<5.5.0", "cwt>=2.3.0,<2.4", + #'pycose>=1.0.1,<1.1.0' "pycose @ git+https://github.com/devisefutures/pycose.git@hsm", ], ) From 142d55635286413a35e2c1e39d848211b49082a2 Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Thu, 23 Jan 2025 10:55:23 +0000 Subject: [PATCH 04/31] add new attestations dc4eu support test --- pymdoccbor/mso/issuer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 1286d0d..fed2a70 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -98,7 +98,12 @@ def __init__( v = cbor2.CBORTag(_value_cbortag, value=v) # print("\n-----\n K,V ", k, "\n", v) - if k == "driving_privileges": + if ( + k == "driving_privileges" + or k == "places_of_work" + or k == "legislation" + or k == "employment_details" + ): for item in v: for k2, v2 in item.items(): _value_cbortag = settings.CBORTAGS_ATTR_MAP.get(k2, None) From 4e12bcfbeab378480f3ed3d85e365d5ae84ae9fb Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Tue, 28 Jan 2025 12:30:31 +0000 Subject: [PATCH 05/31] update dict and list encode tag --- pymdoccbor/mso/issuer.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index fed2a70..d5e89c8 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -98,12 +98,13 @@ def __init__( v = cbor2.CBORTag(_value_cbortag, value=v) # print("\n-----\n K,V ", k, "\n", v) - if ( - k == "driving_privileges" - or k == "places_of_work" - or k == "legislation" - or k == "employment_details" - ): + if isinstance(v, dict): + for k2, v2 in v.items(): + _value_cbortag = settings.CBORTAGS_ATTR_MAP.get(k2, None) + if _value_cbortag: + v[k2] = cbor2.CBORTag(_value_cbortag, value=v2) + + if isinstance(v, list): for item in v: for k2, v2 in item.items(): _value_cbortag = settings.CBORTAGS_ATTR_MAP.get(k2, None) From fe83479f1432b23187cb419c4aa583a44714210f Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Thu, 20 Feb 2025 15:16:46 +0000 Subject: [PATCH 06/31] update PID 1.5 nationality --- pymdoccbor/mso/issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index d5e89c8..9551540 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -104,7 +104,7 @@ def __init__( if _value_cbortag: v[k2] = cbor2.CBORTag(_value_cbortag, value=v2) - if isinstance(v, list): + if isinstance(v, list) and k != "nationality": for item in v: for k2, v2 in item.items(): _value_cbortag = settings.CBORTAGS_ATTR_MAP.get(k2, None) From c613407029424f34f3ce1d83fbccaac8a1aa381d Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:05:24 +0100 Subject: [PATCH 07/31] fix: ci restore --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 9910dfa..a3e640c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -30,7 +30,7 @@ jobs: - name: Install system package run: | sudo apt update - sudo apt install python3-dev libssl-dev make automake gcc g++ + sudo apt install python3-dev libssl-dev make automake gcc g++ libssl-dev - name: Install dependencies run: | python -m pip install --upgrade pip From 670bd33e3be8a41f9f692f1114ae20969b035b03 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:05:38 +0100 Subject: [PATCH 08/31] fix: restore code --- pymdoccbor/mdoc/issuer.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index 7503da8..e2f1b48 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -143,23 +143,19 @@ def new( slot_id=self.slot_id, ) - # TODO: for now just a single document, it would be trivial having - # also multiple but for now I don't have use cases for this res = { - # "version": self.version, - # "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, + "version": self.version, + "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, } # print("mso diganostic notation: \n", cbor2diag(mso_cbor)) From 8a37f526d8533dbfe305a2b1ed4ca9fee9a16d6f Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:06:18 +0100 Subject: [PATCH 09/31] tests: adapted tests --- pymdoccbor/tests/micov_data.py | 23 ++++++++++++++++ pymdoccbor/tests/test_02_mdoc_issuer.py | 24 +++++++++++++---- pymdoccbor/tests/test_08_mdoc_cbor.py | 36 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 pymdoccbor/tests/micov_data.py create mode 100644 pymdoccbor/tests/test_08_mdoc_cbor.py diff --git a/pymdoccbor/tests/micov_data.py b/pymdoccbor/tests/micov_data.py new file mode 100644 index 0000000..c219f74 --- /dev/null +++ b/pymdoccbor/tests/micov_data.py @@ -0,0 +1,23 @@ +MICOV_DATA = { + "org.micov.medical.1":{ + "last_name": "Rossi", + "given_name": "Mario", + "birth_date": "1922-03-13", + "PersonId_nic": { + "PersonIdNumber": "1234567890", + "PersonIdType": "nic", + "PersonIdIS": "IT", + }, + "sex": 1, + "VPInfo_COVID-19_1": { + "VaccineProphylaxis": "", + "VaccMedicinalProd": "Moderna", + "VaccMktAuthHolder": "Moderna", + "VaccDoseNumber": "2/2", + "VaccAdmDate": "2021-01-01", + "VaccCountry": "IT", + }, + "CertIssuer": "Italian Ministry of Health", + "CertId": "1234567890", + } +} \ No newline at end of file diff --git a/pymdoccbor/tests/test_02_mdoc_issuer.py b/pymdoccbor/tests/test_02_mdoc_issuer.py index 8c232f4..7975c15 100644 --- a/pymdoccbor/tests/test_02_mdoc_issuer.py +++ b/pymdoccbor/tests/test_02_mdoc_issuer.py @@ -1,6 +1,5 @@ import cbor2 import os - from pycose.messages import Sign1Message from pymdoccbor.mdoc.issuer import MdocCborIssuer @@ -21,10 +20,20 @@ def test_mso_writer(): msoi = MsoIssuer( data=PID_DATA, - private_key=PKEY + private_key=PKEY, + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + alg="ES256" ) - # TODO: assertion here about msow.hash_map and msow.disclosure_map + assert "eu.europa.ec.eudiw.pid.1" in msoi.hash_map + assert msoi.hash_map["eu.europa.ec.eudiw.pid.1"] + + assert "eu.europa.ec.eudiw.pid.1" in msoi.disclosure_map + assert msoi.disclosure_map["eu.europa.ec.eudiw.pid.1"] + assert msoi.disclosure_map["eu.europa.ec.eudiw.pid.1"].values().__len__() == PID_DATA["eu.europa.ec.eudiw.pid.1"].values().__len__() mso = msoi.sign() @@ -36,13 +45,18 @@ def test_mso_writer(): def test_mdoc_issuer(): mdoci = MdocCborIssuer( - private_key=PKEY + private_key=PKEY, + alg="ES256", ) mdoc = mdoci.new( doctype="eu.europa.ec.eudiw.pid.1", data=PID_DATA, - devicekeyinfo=PKEY # TODO + #devicekeyinfo=PKEY, TODO + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, ) mdocp = MdocCbor() diff --git a/pymdoccbor/tests/test_08_mdoc_cbor.py b/pymdoccbor/tests/test_08_mdoc_cbor.py new file mode 100644 index 0000000..bff835d --- /dev/null +++ b/pymdoccbor/tests/test_08_mdoc_cbor.py @@ -0,0 +1,36 @@ +import os +import cbor2 +from pymdoccbor.mdoc.issuer import MdocCborIssuer +from pymdoccbor.tests.micov_data import MICOV_DATA +from pymdoccbor.mdoc.verifier import MdocCbor + +PKEY = { + 'KTY': 'EC2', + 'CURVE': 'P_256', + 'ALG': 'ES256', + 'D': os.urandom(32), + 'KID': b"demo-kid" +} + +def test_mdoc_cbor_creation(): + mdoci = MdocCborIssuer( + private_key=PKEY, + alg="ES256", + ) + mdoc = mdoci.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + ) + + data = cbor2.dumps(mdoc) + + mdocp = MdocCbor() + mdocp.loads(data) + mdocp.verify() + + assert mdoc \ No newline at end of file From 86e7f7c3eace6f9dd3a62cbb4cb162ba3b784881 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:20:02 +0100 Subject: [PATCH 10/31] fix: install --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a3e640c..3dea6c1 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -30,7 +30,7 @@ jobs: - name: Install system package run: | sudo apt update - sudo apt install python3-dev libssl-dev make automake gcc g++ libssl-dev + sudo apt install python3-dev libssl-dev libffi-dev make automake gcc g++ - name: Install dependencies run: | python -m pip install --upgrade pip From f2149e6710f1448c3b2f35d0baf6e91923834699 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:24:10 +0100 Subject: [PATCH 11/31] fix: try ubuntu 22 --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 3dea6c1..44bbe0e 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -12,7 +12,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false From c768740cb7435214d1bfbad2ec919d097ac292a1 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:27:27 +0100 Subject: [PATCH 12/31] fix: dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 305caea..99c55c9 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def readme(): install_requires=[ "cbor2>=5.4.0,<5.5.0", "cwt>=2.3.0,<2.4", + "cbor-diag>=1.1.0<1.2", #'pycose>=1.0.1,<1.1.0' "pycose @ git+https://github.com/devisefutures/pycose.git@hsm", ], From 6439edb012f1594c1c66a86758dad2efd0b24907 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:28:42 +0100 Subject: [PATCH 13/31] fix: typo --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 99c55c9..1f875b3 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ def readme(): install_requires=[ "cbor2>=5.4.0,<5.5.0", "cwt>=2.3.0,<2.4", - "cbor-diag>=1.1.0<1.2", + "cbor-diag>=1.1.0,<1.2", #'pycose>=1.0.1,<1.1.0' "pycose @ git+https://github.com/devisefutures/pycose.git@hsm", ], From db5b9c951d0653cb718a3223f31d8f6eff3d2059 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Thu, 13 Mar 2025 16:38:09 +0100 Subject: [PATCH 14/31] fix: updated python versions --- .github/workflows/python-app.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 44bbe0e..bd013ba 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -18,8 +18,9 @@ jobs: fail-fast: false matrix: python-version: - - '3.9' - '3.10' + - "3.11" + - "3.12" steps: - uses: actions/checkout@v2 From 3104d19470f857cd73c9f56999fb9421f8fb50f6 Mon Sep 17 00:00:00 2001 From: Pasquale De Rose Date: Fri, 14 Mar 2025 14:18:18 +0100 Subject: [PATCH 15/31] Update pymdoccbor/mdoc/issuer.py Co-authored-by: Giuseppe De Marco --- 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 e2f1b48..0319eb8 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -69,7 +69,7 @@ def new( """ if isinstance(devicekeyinfo, dict): devicekeyinfo = CoseKey.from_dict(devicekeyinfo) - if isinstance(devicekeyinfo, str): + elif isinstance(devicekeyinfo, str): device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8")) public_key = serialization.load_pem_public_key(device_key_bytes) curve_name = public_key.curve.name From 8f10944973601d5021045a8ca937dd5bd07be8fd Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:23:45 +0100 Subject: [PATCH 16/31] fix: private key handling --- pymdoccbor/mso/issuer.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 9551540..568b89f 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -57,12 +57,15 @@ def __init__( digest_alg: str = settings.PYMDOC_HASHALG, ): if not hsm: - if private_key and isinstance(private_key, dict): - self.private_key = CoseKey.from_dict(private_key) - if not self.private_key.kid: - self.private_key.kid = str(uuid.uuid4()) - elif private_key and isinstance(private_key, CoseKey): - self.private_key = private_key + if private_key: + if isinstance(private_key, dict): + self.private_key = CoseKey.from_dict(private_key) + if not self.private_key.kid: + self.private_key.kid = str(uuid.uuid4()) + elif isinstance(private_key, CoseKey): + self.private_key = private_key + else: + raise ValueError("private_key must be a dict or CoseKey object") else: raise MsoPrivateKeyRequired("MSO Writer requires a valid private key") From bd2d133bdefe61c39139a612377267f817195e60 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:26:00 +0100 Subject: [PATCH 17/31] fix: version --- pymdoccbor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymdoccbor/__init__.py b/pymdoccbor/__init__.py index 6b27eee..49e0fc1 100644 --- a/pymdoccbor/__init__.py +++ b/pymdoccbor/__init__.py @@ -1 +1 @@ -__version__ = "0.5.4" +__version__ = "0.7.0" From 719e3bf4fbd305002f11c01787517921f0dc2f1e Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:27:30 +0100 Subject: [PATCH 18/31] fix: private key handling --- pymdoccbor/mdoc/issuer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index 0319eb8..cde432f 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -43,8 +43,14 @@ def __init__( ): self.version: str = "1.0" self.status: int = 0 - if private_key and isinstance(private_key, dict): - self.private_key = CoseKey.from_dict(private_key) + + if private_key: + if isinstance(private_key, dict): + self.private_key = CoseKey.from_dict(private_key) + elif isinstance(private_key, CoseKey): + self.private_key = private_key + else: + raise ValueError("private_key must be a dict or CoseKey object") self.signed: dict = {} self.key_label = key_label @@ -69,7 +75,7 @@ def new( """ if isinstance(devicekeyinfo, dict): devicekeyinfo = CoseKey.from_dict(devicekeyinfo) - elif isinstance(devicekeyinfo, str): + if isinstance(devicekeyinfo, str): device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8")) public_key = serialization.load_pem_public_key(device_key_bytes) curve_name = public_key.curve.name From 750f7b236d6ed17298dc1bee05cfdcfb7c76d158 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:31:09 +0100 Subject: [PATCH 19/31] fix: readme --- README.md | 55 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5d471bf..d807566 100644 --- a/README.md +++ b/README.md @@ -9,26 +9,26 @@ Python parser and writer for EUDI Wallet MDOC CBOR for credential Type 1 and als - [ISO 18013-5 - ISO-compliant driving licence — Mobile driving licence (mDL) application](https://www.iso.org/standard/69084.html) - [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) - [RFC 9052 - CBOR Object Signing and Encryption (COSE): Structures and Process](https://www.rfc-editor.org/rfc/rfc9052.html) - - deprecates [RFC 8152 - CBOR Object Signing and Encryption (COSE)](https://datatracker.ietf.org/doc/html/rfc8152) + - deprecates [RFC 8152 - CBOR Object Signing and Encryption (COSE)](https://datatracker.ietf.org/doc/html/rfc8152) - [IANA Registry - Concise Binary Object Representation (CBOR) Tags](https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml) COSE label 33 (x5chain) in MSO: - [RFC 9360 CBOR Object Signing and Encryption (COSE) - Header Parameters for Carrying and Referencing X.509 Certificates](https://www.rfc-editor.org/rfc/rfc9360.html) -## Scope +## Scope -pyMDOC-CBOR is a -[cbor2](https://github.com/agronholm/cbor2) -and -[pycose](https://github.com/TimothyClaeys/pycose) -wrapper that parses, creates and validates MDOC CBOR encoded binaries +pyMDOC-CBOR is a +[cbor2](https://github.com/agronholm/cbor2) +and +[pycose](https://github.com/TimothyClaeys/pycose) +wrapper that parses, creates and validates MDOC CBOR encoded binaries according to ISO 18013-5. ## Setup ```` -pip install pymdlmdoc +pip install pymdoccbor ```` or @@ -122,7 +122,7 @@ MsoIssuer is a class that handles private keys, data processing, digests and sig The `disclosure_map` is used in the Mdoc `nameSpaces` object for issuance and presentations, it's carried in the mdoc but outside of the MSO, even if it is produced by `MsoIssuer`. -that's why `MsoIssuer.sign()` returns a pure MSO, while `disclosure_map` +that's why `MsoIssuer.sign()` returns a pure MSO, while `disclosure_map` is an attribute of the `MsoIssuer` instance. ```` @@ -150,15 +150,16 @@ mso = msoi.sign() ```` API usage: - - `msoi.data`, user attributes to be encoded - - `msoi.private_key`, COSEKey - - `msoi.public_key`, COSEKey without `d` (for EC2Key) - - `msoi.selfsigned_x509cert`, using the private and the public keys returns a self-signed x509 certificate - - `msoi.hash_map`, digests that will be signed in the MSO - - `msoi.disclosure_map`, disclosure objects grouped by namespaces - - `msoi.sign`, signs the MSO and returns it -### Parse a binary Mdoc +- `msoi.data`, user attributes to be encoded +- `msoi.private_key`, COSEKey +- `msoi.public_key`, COSEKey without `d` (for EC2Key) +- `msoi.selfsigned_x509cert`, using the private and the public keys returns a self-signed x509 certificate +- `msoi.hash_map`, digests that will be signed in the MSO +- `msoi.disclosure_map`, disclosure objects grouped by namespaces +- `msoi.sign`, signs the MSO and returns it + +### Parse a binary Mdoc ```` from pymdoccbor.mdoc.verifier import MdocCbor @@ -192,13 +193,14 @@ msop.verify_signature() ```` API usage: - - `msop.payload_as_dict`: returns the MSO as Python dictionary. - - `msop.payload_as_raw`: returns the MSO as bytes in its original format. - - `msop.payload_as_cbor`: returns the MSO as CBOR encoded object. - - `msop.object`: returns a pycose COSE_Sign1 object. - - `msop.raw_public_keys`: returns the list of the public keys from the unprotected COSE header - - `msop.public_key`: returns `cryptography.hazmat` key. - - `msop.x509_certificates`: returns a list of `cryptography.x509` certificate objects + +- `msop.payload_as_dict`: returns the MSO as Python dictionary. +- `msop.payload_as_raw`: returns the MSO as bytes in its original format. +- `msop.payload_as_cbor`: returns the MSO as CBOR encoded object. +- `msop.object`: returns a pycose COSE_Sign1 object. +- `msop.raw_public_keys`: returns the list of the public keys from the unprotected COSE header +- `msop.public_key`: returns `cryptography.hazmat` key. +- `msop.x509_certificates`: returns a list of `cryptography.x509` certificate objects ## Tests @@ -210,6 +212,7 @@ pytest --cov-report term-missing --cov-report term:skip-covered --cov ## Other examples Quick preview in bash using AF binary + ```` # D.4.1.2 mdoc response export ISSUED_MDOC="a36776657273696f6e63312e3069646f63756d656e747381a367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564a26a6e616d65537061636573a1716f72672e69736f2e31383031332e352e3186d8185863a4686469676573744944006672616e646f6d58208798645b20ea200e19ffabac92624bee6aec63aceedecfb1b80077d22bfc20e971656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756563446f65d818586ca4686469676573744944036672616e646f6d5820b23f627e8999c706df0c0a4ed98ad74af988af619b4bb078b89058553f44615d71656c656d656e744964656e7469666965726a69737375655f646174656c656c656d656e7456616c7565d903ec6a323031392d31302d3230d818586da4686469676573744944046672616e646f6d5820c7ffa307e5de921e67ba5878094787e8807ac8e7b5b3932d2ce80f00f3e9abaf71656c656d656e744964656e7469666965726b6578706972795f646174656c656c656d656e7456616c7565d903ec6a323032342d31302d3230d818586da4686469676573744944076672616e646f6d582026052a42e5880557a806c1459af3fb7eb505d3781566329d0b604b845b5f9e6871656c656d656e744964656e7469666965726f646f63756d656e745f6e756d6265726c656c656d656e7456616c756569313233343536373839d818590471a4686469676573744944086672616e646f6d5820d094dad764a2eb9deb5210e9d899643efbd1d069cc311d3295516ca0b024412d71656c656d656e744964656e74696669657268706f7274726169746c656c656d656e7456616c7565590412ffd8ffe000104a46494600010101009000900000ffdb004300130d0e110e0c13110f11151413171d301f1d1a1a1d3a2a2c2330453d4947443d43414c566d5d4c51685241435f82606871757b7c7b4a5c869085778f6d787b76ffdb0043011415151d191d381f1f38764f434f7676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676ffc00011080018006403012200021101031101ffc4001b00000301000301000000000000000000000005060401020307ffc400321000010303030205020309000000000000010203040005110612211331141551617122410781a1163542527391b2c1f1ffc4001501010100000000000000000000000000000001ffc4001a110101010003010000000000000000000000014111213161ffda000c03010002110311003f00a5bbde22da2329c7d692bc7d0d03f52cfb0ff75e7a7ef3e7709723a1d0dae146ddfbb3c039ce07ad2bd47a7e32dbb8dd1d52d6ef4b284f64a480067dfb51f87ffb95ff00eb9ff14d215de66af089ce44b7dbde9cb6890a2838eddf18078f7add62d411ef4db9b10a65d6b95a147381ea0d495b933275fe6bba75c114104a8ba410413e983dff004f5af5d34b4b4cde632d0bf1fd1592bdd91c6411f3934c2fa6af6b54975d106dcf4a65ae56e856001ebc03c7ce29dd9eef1ef10fc447dc9da76ad2aee93537a1ba7e4f70dd8eff0057c6dffb5e1a19854a83758e54528750946ec6704850cd037bceb08b6d7d2cc76d3317fc7b5cc04fb6707269c5c6e0c5b60ae549242123b0e493f602a075559e359970d98db89525456b51c951c8afa13ea8e98e3c596836783d5c63f5a61a99fdb7290875db4be88ab384bbbbbfc7183fdeaa633e8951db7da396dc48524fb1a8bd611a5aa2a2432f30ab420a7a6d3240c718cf031fa9ef4c9ad550205aa02951df4a1d6c8421b015b769db8c9229837ea2be8b1b0d39d0eba9c51484efdb8c0efd8d258daf3c449699f2edbd4584e7af9c64e3f96b9beb28d4ac40931e6478c8e76a24a825449501d867d2b1dcdebae99b9c752ae4ecd6dde4a179c1c1e460938f9149ef655e515c03919a289cb3dca278fb7bf177f4faa829dd8ce3f2ac9a7ecde490971fafd7dce15eed9b71c018c64fa514514b24e8e4f8c5c9b75c1e82579dc1233dfec08238f6add62d391acc1c5256a79e706d52d431c7a0145140b9fd149eb3a60dc5e88cbbc2da092411e9dc71f39a7766b447b344e847dcac9dcb5abba8d145061d43a6fcf1e65cf15d0e90231d3dd9cfe62995c6dcc5ca12a2c904a15f71dd27d451453e09d1a21450961cbb3ea8a956433b781f1ce33dfed54f0e2b50a2b71d84ed6db18028a28175f74fc6bda105c529a791c25c4f3c7a11f71586268f4a66b726e33de9ea6f1b52b181c760724e47b514520a5a28a283ffd9d81858ffa4686469676573744944096672616e646f6d58204599f81beaa2b20bd0ffcc9aa03a6f985befab3f6beaffa41e6354cdb2ab2ce471656c656d656e744964656e7469666965727264726976696e675f70726976696c656765736c656c656d656e7456616c756582a37576656869636c655f63617465676f72795f636f646561416a69737375655f64617465d903ec6a323031382d30382d30396b6578706972795f64617465d903ec6a323032342d31302d3230a37576656869636c655f63617465676f72795f636f646561426a69737375655f64617465d903ec6a323031372d30322d32336b6578706972795f64617465d903ec6a323032342d31302d32306a697373756572417574688443a10126a118215901d2308201ce30820174a003020102021401ec51916031e6898e8fc7864af5e6d5f86602b6300a06082a8648ce3d04030230233114301206035504030c0b75746f7069612069616361310b3009060355040613025553301e170d3230313030313030303030305a170d3231313030313030303030305a30213112301006035504030c0975746f706961206473310b30090603550406130255533059301306072a8648ce3d020106082a8648ce3d03010703420004ace7ab7340e5d9648c5a72a9a6f56745c7aad436a03a43efea77b5fa7b88f0197d57d8983e1b37d3a539f4d588365e38cbbf5b94d68c547b5bc8731dcd2f146ba38187308184301e0603551d120417301581136578616d706c65406578616d706c652e636f6d301c0603551d1f041530133011a00fa00d820b6578616d706c652e636f6d301d0603551d0e0416041414e29017a6c35621ffc7a686b7b72db06cd12351300e0603551d0f0101ff04040302078030150603551d250101ff040b3009060728818c5d050102300a06082a8648ce3d0403020348003045022100bac6f93a8bacf0fc9aeac1c89a5c9293af2076942e9e972882a113640330702702207b7b73c0444371a4c94c9c888ddfe553ffde84ca492fd64dfbf02ad46a31cbc85903a2d81859039da66776657273696f6e63312e306f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473a2716f72672e69736f2e31383031332e352e31ad00582075167333b47b6c2bfb86eccc1f438cf57af055371ac55e1e359e20f254adcebf01582067e539d6139ebd131aef441b445645dd831b2b375b390ca5ef6279b205ed45710258203394372ddb78053f36d5d869780e61eda313d44a392092ad8e0527a2fbfe55ae0358202e35ad3c4e514bb67b1a9db51ce74e4cb9b7146e41ac52dac9ce86b8613db555045820ea5c3304bb7c4a8dcb51c4c13b65264f845541341342093cca786e058fac2d59055820fae487f68b7a0e87a749774e56e9e1dc3a8ec7b77e490d21f0e1d3475661aa1d0658207d83e507ae77db815de4d803b88555d0511d894c897439f5774056416a1c7533075820f0549a145f1cf75cbeeffa881d4857dd438d627cf32174b1731c4c38e12ca936085820b68c8afcb2aaf7c581411d2877def155be2eb121a42bc9ba5b7312377e068f660958200b3587d1dd0c2a07a35bfb120d99a0abfb5df56865bb7fa15cc8b56a66df6e0c0a5820c98a170cf36e11abb724e98a75a5343dfa2b6ed3df2ecfbb8ef2ee55dd41c8810b5820b57dd036782f7b14c6a30faaaae6ccd5054ce88bdfa51a016ba75eda1edea9480c5820651f8736b18480fe252a03224ea087b5d10ca5485146c67c74ac4ec3112d4c3a746f72672e69736f2e31383031332e352e312e5553a4005820d80b83d25173c484c5640610ff1a31c949c1d934bf4cf7f18d5223b15dd4f21c0158204d80e1e2e4fb246d97895427ce7000bb59bb24c8cd003ecf94bf35bbd2917e340258208b331f3b685bca372e85351a25c9484ab7afcdf0d2233105511f778d98c2f544035820c343af1bd1690715439161aba73702c474abf992b20c9fb55c36a336ebe01a876d6465766963654b6579496e666fa1696465766963654b6579a40102200121582096313d6c63e24e3372742bfdb1a33ba2c897dcd68ab8c753e4fbd48dca6b7f9a2258201fb3269edd418857de1b39a4e4a44b92fa484caa722c228288f01d0c03a2c3d667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e666fa3667369676e6564c074323032302d31302d30315431333a33303a30325a6976616c696446726f6dc074323032302d31302d30315431333a33303a30325a6a76616c6964556e74696cc074323032312d31302d30315431333a33303a30325a5840cff12c17d4739aba806035a9cb2b34ae8a830cef4f329289f9a3ebd302dd6b99c584068257569397b92ba9aa5128554eb05d1273dafea313da4aff6b01a5fb3f6c6465766963655369676e6564a26a6e616d65537061636573d81841a06a64657669636541757468a1696465766963654d61638443a10105a0f65820200d73ded787c64652dc8ee743ea83a5260d5a3283fddc919b7b9cfb486addb26673746174757300" @@ -220,11 +223,13 @@ echo $ISSUED_MDOC | xxd -r -ps | python3 -m cbor2.tool --pretty ### using cbor-diag Install cbor-diag + ```` pip install cbor-diag ```` Print a cbor diagnostic representation + ```` from cbor_diag import * @@ -246,4 +251,4 @@ Other examples at [cbor official documentation](https://github.com/agronholm/cbo ## Authors and contributors -- Giuseppe De Marco +- Giuseppe De Marco From 07f692a53c3dee3ea1c7d93ccd3fdbd57eae46c2 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:32:55 +0100 Subject: [PATCH 20/31] fix: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d807566..f09e565 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ pip install pymdoccbor or ```` -pip install git+https://github.com/devisefutures/pyMDOC-CBOR.git@cert_arg +pip install git+https://github.com/peppelinux/pyMDOC-CBOR.git ```` ## Usage From 4aff49a18e78e888ccd4c78e3ff50cbfdf415e0c Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:37:05 +0100 Subject: [PATCH 21/31] fix: readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f09e565..1365d5e 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,8 @@ Other examples at [cbor official documentation](https://github.com/agronholm/cbo #### CBOR Diagnostic representation - [CBOR-DIAG-PY](https://github.com/chrysn/cbor-diag-py) +- [Authlete's CBOR diagnostic tools](https://nextdev-api.authlete.net/api/cbor) +- [Auth0 CBOR diagnostic tool](https://www.mdl.me/) #### X.509 certificates and chains From 7694fe9a7363d3a304060a85555b675c1f6a8863 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:41:25 +0100 Subject: [PATCH 22/31] fix: readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1365d5e..c5d518d 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,9 @@ PID_DATA = { "eu.europa.ec.eudiw.pid.1": { "family_name": "Raffaello", "given_name": "Mascetti", - "birth_date": "1922-03-13" + "birth_date": "1922-03-13", + "birth_place": "Rome", + "birth_country": "IT" } } From 4f17a86dbcf8014582d06409be07dd999932f63b Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 14:45:46 +0100 Subject: [PATCH 23/31] fix: restored example --- examples/it_data_model.py | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 examples/it_data_model.py diff --git a/examples/it_data_model.py b/examples/it_data_model.py new file mode 100644 index 0000000..a429145 --- /dev/null +++ b/examples/it_data_model.py @@ -0,0 +1,71 @@ +import cbor2 +import os + +from pymdoccbor.mdoc.issuer import MdocCborIssuer + +PKEY = { + 'KTY': 'EC2', + 'CURVE': 'P_256', + 'ALG': 'ES256', + 'D': os.urandom(32), + 'KID': b"demo-kid" +} + +PID_DATA = { + "org.iso.18013.5.1": { + "expiry_date": "2024-02-22", + "issue_date": "2023-11-14", + "issuing_country": "IT", + "issuing_authority": "Gli amici della Salaria", + "family_name": "Rossi", + "given_name": "Mario", + "birth_date": "1956-01-12", + "document_number": "XX1234567", + "portrait": b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00\x90\x00\x90\x00\x00\xff\xdb\x00C\x00\x13\r\x0e\x11\x0e\x0c\x13\x11\x0f\x11\x15\x14\x13\x17\x1d0\x1f\x1d\x1a\x1a\x1d:*,#0E=IGD=CALVm]LQhRAC_\x82`hqu{|{J\\\x86\x90\x85w\x8fmx{v\xff\xdb\x00C\x01\x14\x15\x15\x1d\x19\x1d8\x1f\x1f8vOCOvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\xff\xc0\x00\x11\x08\x00\x18\x00d\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1b\x00\x00\x03\x01\x00\x03\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x06\x04\x01\x02\x03\x07\xff\xc4\x002\x10\x00\x01\x03\x03\x03\x02\x05\x02\x03\t\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x00\x05\x11\x06\x12!\x131\x14\x15Qaq"A\x07\x81\xa1\x165BRs\x91\xb2\xc1\xf1\xff\xc4\x00\x15\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xc4\x00\x1a\x11\x01\x01\x01\x00\x03\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01A\x11!1a\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xa5\xbb\xde"\xda#)\xc7\xd6\x92\xbc}\r\x03\xf5,\xfb\x0f\xf7^z~\xf3\xe7p\x97#\xa1\xd0\xda\xe1F\xdd\xfb\xb3\xc09\xce\x07\xad+\xd4z~2\xdb\xb8\xdd\x1dR\xd6\xefK(Od\xa4\x80\x06}\xfbQ\xf8\x7f\xfb\x95\xff\x00\xeb\x9f\xf1M!]\xe6j\xf0\x89\xceD\xb7\xdb\xde\x9c\xb6\x89\n(8\xed\xdf\x18\x07\x8fz\xddb\xd4\x11\xefM\xb9\xb1\ne\xd6\xb9Z\x14s\x81\xea\rI[\x932u\xfek\xbau\xc1\x14\x10J\x8b\xa4\x10A>\x98=\xff\x00OZ\xf5\xd3KKL\xdec-\x0b\xf1\xfd\x15\x92\xbd\xd9\x1cd\x11\xf3\x93L/\xa6\xafkT\x97]\x10m\xcfJe\xaeV\xe8V\x00\x1e\xbc\x03\xc7\xce)\xdd\x9e\xef\x1e\xf1\x0f\xc4G\xdc\x9d\xa7j\xd2\xae\xe957\xa1\xba~Op\xdd\x8e\xff\x00W\xc6\xdf\xfb^\x1a\x19\x85J\x83u\x8eTR\x87P\x94n\xc6pHP\xcd\x03{\xce\xb0\x8bm},\xc7m3\x17\xfc{\\\xc0O\xb6pri\xc5\xc6\xe0\xc5\xb6\n\xe5I$!#\xb0\xe4\x93\xf6\x02\xa0uU\x9e5\x99p\xd9\x8d\xb8\x95%EkQ\xc9Q\xc8\xaf\xa1>\xa8\xe9\x8e\x89Q\xdb}\xa3\x96\xdcHRO\xb1\xa8\xbda\x1aZ\xa2\xa2C/0\xabB\nzm2@\xc7\x18\xcf\x03\x1f\xa9\xefL\x9a\xd5P Z\xa0)Q\xdfJ\x1dl\x84!\xb0\x15\xb7i\xdb\x8c\x92)\x83~\xa2\xbe\x8b\x1b\r9\xd0\xeb\xa9\xc5\x14\x84\xef\xdb\x8c\x0e\xfd\x8d%\x8d\xaf\t\xd1\xa2\x14P\x96\x1c\xbb>\xa8\xa9VC;x\x1f\x1c\xe3=\xfe\xd5O\x0e+P\xa2\xb7\x1d\x84\xedm\xb1\x80(\xa2\x81u\xf7O\xc6\xbd\xa1\x05\xc5)\xa7\x91\xc2\\O Date: Fri, 14 Mar 2025 15:21:09 +0100 Subject: [PATCH 24/31] fix: restored docstring and typing --- pymdoccbor/mdoc/issuer.py | 51 +++++++++++++-------- pymdoccbor/mdoc/issuersigned.py | 23 +++++++++- pymdoccbor/mdoc/verifier.py | 78 +++++++++++++++++++++++++-------- pymdoccbor/mso/issuer.py | 57 +++++++++++++++--------- pymdoccbor/mso/verifier.py | 29 +++++++++--- pymdoccbor/tools.py | 23 ++++++++-- pymdoccbor/x509.py | 16 +++++-- 7 files changed, 205 insertions(+), 72 deletions(-) diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index cde432f..8a3c0b1 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -1,18 +1,3 @@ -# Modifications have been made to the original file (available at https://github.com/IdentityPython/pyMDOC-CBOR) -# All modifications Copyright (c) 2023 European Commission - -# All modifications licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import base64 import binascii import cbor2 @@ -30,6 +15,9 @@ class MdocCborIssuer: + """ + MdocCborIssuer helper class to create a new mdoc + """ def __init__( self, key_label: str = None, @@ -41,6 +29,18 @@ def __init__( kid: str = None, private_key: Union[dict, CoseKey] = {}, ): + """ + Initialize a new MdocCborIssuer + + :param key_label: str: key label + :param user_pin: str: user pin + :param lib_path: str: path to the library cryptographic library + :param slot_id: int: slot id + :param hsm: bool: hardware security module + :param alg: str: hashig algorithm + :param kid: str: key id + :param private_key: Union[dict, CoseKey]: private key + """ self.version: str = "1.0" self.status: int = 0 @@ -72,6 +72,15 @@ def new( ): """ create a new mdoc with signed mso + + :param data: dict: data to be signed + :param doctype: str: document type + :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 + + :return: dict: signed mdoc """ if isinstance(devicekeyinfo, dict): devicekeyinfo = CoseKey.from_dict(devicekeyinfo) @@ -169,14 +178,18 @@ def new( self.signed = res return self.signed - def dump(self): + def dump(self) -> bytes: """ - returns bytes + Returns the CBOR representation of the signed mdoc + + :return: bytes: CBOR representation of the signed mdoc """ return cbor2.dumps(self.signed, canonical=True) - def dumps(self): + def dumps(self) -> bytes: """ - returns AF binary repr + Returns the AF binary representation of the signed mdoc + + :return: bytes: AF binary representation of the signed mdoc """ return binascii.hexlify(cbor2.dumps(self.signed, canonical=True)) diff --git a/pymdoccbor/mdoc/issuersigned.py b/pymdoccbor/mdoc/issuersigned.py index 1c39b84..866e974 100644 --- a/pymdoccbor/mdoc/issuersigned.py +++ b/pymdoccbor/mdoc/issuersigned.py @@ -22,19 +22,38 @@ class IssuerSigned: ] """ - def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]): + def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]) -> None: + """ + Initialize the IssuerSigned object + + :param nameSpaces: dict: the nameSpaces of the document + :param issuerAuth: Union[dict, bytes]: the issuerAuth info of the document + """ + self.namespaces: dict = nameSpaces # if isinstance(ia, dict): self.issuer_auth = MsoVerifier(issuerAuth) def dump(self) -> dict: + """ + It returns the issuerSigned as a dict + + :return: dict: the issuerSigned as a dict + """ + return { 'nameSpaces': self.namespaces, 'issuerAuth': self.issuer_auth } - def dumps(self) -> dict: + def dumps(self) -> bytes: + """ + It returns the issuerSigned as bytes + + :return: dict: the issuerSigned as bytes + """ + return cbor2.dumps( { 'nameSpaces': self.namespaces, diff --git a/pymdoccbor/mdoc/verifier.py b/pymdoccbor/mdoc/verifier.py index 4eea8df..0bbb7cf 100644 --- a/pymdoccbor/mdoc/verifier.py +++ b/pymdoccbor/mdoc/verifier.py @@ -11,63 +11,97 @@ class MobileDocument: + """ + MobileDocument class to handle the Mobile Document + """ + _states = { True: "valid", False: "failed", } - def __init__(self, docType: str, issuerSigned: dict, deviceSigned: dict = {}): + def __init__(self, docType: str, issuerSigned: dict, deviceSigned: dict = {}) -> None: + """ + Initialize the MobileDocument object + + :param docType: str: the document type + :param issuerSigned: dict: the issuerSigned info + :param deviceSigned: dict: the deviceSigned info + """ + self.doctype: str = docType # eg: 'org.iso.18013.5.1.mDL' self.issuersigned: List[IssuerSigned] = IssuerSigned(**issuerSigned) self.is_valid = False - - # TODO self.devicesigned: dict = deviceSigned def dump(self) -> dict: + """ + It returns the document as a dict + + :return: dict: the document as a dict + """ return { 'docType': self.doctype, 'issuerSigned': self.issuersigned.dump() } - def dumps(self) -> str: + def dumps(self) -> bytes: """ - returns an AF binary repr of the document + It returns the AF binary repr as bytes + + :return: bytes: the document as bytes """ return binascii.hexlify(self.dump()) def dump(self) -> bytes: """ - returns bytes + It returns the document as bytes + + :return: dict: the document as bytes """ return cbor2.dumps( - cbor2.CBORTag(24, value={ - 'docType': self.doctype, - 'issuerSigned': self.issuersigned.dumps() - } + cbor2.CBORTag( + 24, + value={ + 'docType': self.doctype, + 'issuerSigned': self.issuersigned.dumps() + } ) ) def verify(self) -> bool: + """ + Verify the document signature + + :return: bool: True if the signature is valid, False otherwise + """ self.is_valid = self.issuersigned.issuer_auth.verify_signature() return self.is_valid - def __repr__(self): + def __repr__(self) -> str: return f"{self.__module__}.{self.__class__.__name__} [{self._states[self.is_valid]}]" class MdocCbor: + """ + MdocCbor class to handle the Mobile Document + """ - def __init__(self): + def __init__(self) -> None: + """ + Initialize the MdocCbor object + """ self.data_as_bytes: bytes = b"" self.data_as_cbor_dict: dict = {} self.documents: List[MobileDocument] = [] self.documents_invalid: list = [] - def loads(self, data: str): + def loads(self, data: str) -> None: """ - data is a AF BINARY + Load the data from a AF Binary string + + :param data: str: the AF binary string """ if isinstance(data, bytes): data = binascii.hexlify(data) @@ -77,13 +111,15 @@ def loads(self, data: str): def dump(self) -> bytes: """ - returns bytes + Returns the CBOR representation of the mdoc as bytes """ return self.data_as_bytes - def dumps(self) -> str: + def dumps(self) -> bytes: """ - returns AF binary string representation + Returns the AF binary representation of the mdoc as bytes + + :return: bytes: the AF binary representation of the mdoc """ return binascii.hexlify(self.data_as_bytes) @@ -92,6 +128,12 @@ def data_as_string(self) -> str: return self.dumps().decode() def verify(self) -> bool: + """" + Verify signatures of all documents contained in the mdoc + + :return: bool: True if all signatures are valid, False otherwise + """ + cdict = self.data_as_cbor_dict for i in ('version', 'documents'): @@ -121,7 +163,7 @@ def verify(self) -> bool: return False if self.documents_invalid else True - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__module__}.{self.__class__.__name__} " f"[{len(self.documents)} valid documents]" diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 568b89f..da988d0 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -1,18 +1,3 @@ -# Modifications have been made to the original file (available at https://github.com/IdentityPython/pyMDOC-CBOR) -# All modifications Copyright (c) 2023 European Commission - -# All modifications licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import cbor2 import datetime import hashlib @@ -38,12 +23,14 @@ class MsoIssuer(MsoX509Fabric): - """ """ + """ + MsoIssuer helper class to create a new mso + """ def __init__( self, data: dict, - validity: str, + validity: dict, revocation: str = None, cert_path: str = None, key_label: str = None, @@ -55,7 +42,25 @@ def __init__( hsm: bool = False, private_key: Union[dict, CoseKey] = None, digest_alg: str = settings.PYMDOC_HASHALG, - ): + ) -> 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 + :param lib_path: str: path to the library cryptographic library + :param slot_id: int: slot id + :param kid: str: key id + :param alg: str: hashig algorithm + :param hsm: bool: hardware security module + :param private_key: Union[dict, CoseKey]: the signing key + :param digest_alg: str: the digest algorithm + """ + if not hsm: if private_key: if isinstance(private_key, dict): @@ -133,7 +138,13 @@ def __init__( digest_cnt += 1 - def format_datetime_repr(self, dt: datetime.datetime): + def format_datetime_repr(self, dt: datetime.datetime) -> str: + """ + Format a datetime object to a string + + :param dt: datetime.datetime: the datetime object + :return: str: the formatted string + """ return dt.isoformat().split(".")[0] + "Z" def sign( @@ -143,7 +154,13 @@ def sign( doctype: str = None, ) -> Sign1Message: """ - sign a mso and returns itprivate_key + Sign a mso and returns itprivate_key + + :param device_key: Union[dict, None]: the device key + :param valid_from: Union[None, datetime.datetime]: the valid from date + :param doctype: str: the document type + + :return: Sign1Message: the signed mso """ utcnow = datetime.datetime.utcnow() valid_from = datetime.datetime.strptime( diff --git a/pymdoccbor/mso/verifier.py b/pymdoccbor/mso/verifier.py index cd6284f..20c4602 100644 --- a/pymdoccbor/mso/verifier.py +++ b/pymdoccbor/mso/verifier.py @@ -5,7 +5,7 @@ from pycose.keys import CoseKey, EC2Key from pycose.messages import Sign1Message -from typing import Optional +from typing import Union from pymdoccbor.exceptions import ( MsoX509ChainNotFound, @@ -31,8 +31,15 @@ class MsoVerifier: structure as defined in RFC 8152. """ - def __init__(self, data: cbor2.CBORTag): + def __init__(self, data: Union[cbor2.CBORTag, bytes, list]) -> None: + """ + Initialize the MsoParser object + + :param data: Union[cbor2.CBORTag, bytes, list]: the data to parse + """ + self._data = data + # not used if isinstance(self._data, bytes): self.object: Sign1Message = bytes2CoseSign1( @@ -44,14 +51,16 @@ def __init__(self, data: cbor2.CBORTag): f"MsoParser only supports raw bytes and list, a {type(data)} was provided" ) - self.object.key: Optional[CoseKey, None] = None + self.object.key = None self.public_key: cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey = None self.x509_certificates: list = [] @property - def payload_as_cbor(self): + def payload_as_cbor(self) -> dict: """ - return the decoded payload + It returns the payload as a CBOR TAG + + :return: dict: the payload as a CBOR TAG 24 """ return cbor2.loads(self.object.payload) @@ -89,8 +98,12 @@ def attest_public_key(self): "python certvalidator or cryptography for that." ) - def load_public_key(self): + def load_public_key(self) -> None: + """ + Load the public key from the x509 certificate + :return: None + """ self.attest_public_key() for i in self.raw_public_keys: @@ -109,7 +122,11 @@ def load_public_key(self): self.object.key = key def verify_signature(self) -> bool: + """" + Verify the signature of the MSO + :return: bool: True if the signature is valid, False otherwise + """ if not self.object.key: self.load_public_key() diff --git a/pymdoccbor/tools.py b/pymdoccbor/tools.py index 50cbcf5..dc781c0 100644 --- a/pymdoccbor/tools.py +++ b/pymdoccbor/tools.py @@ -13,6 +13,9 @@ def bytes2CoseSign1(data: bytes) -> Sign1Message: """ Gets bytes and return a COSE_Sign1 object + + :param data: bytes: the COSE Sign1 as bytes + :return: Sign1Message: the COSE Sign1 object """ decoded = Sign1Message.decode(cbor2.loads(data).value) @@ -21,7 +24,10 @@ def bytes2CoseSign1(data: bytes) -> Sign1Message: def cborlist2CoseSign1(data: list) -> Sign1Message: """ - Gets cbor2 decoded COSE Sign1 as a list and return a COSE_Sign1 object + Gets cbor2 decoded COSE Sign1 as a list and return a COSE_Sign1 object + + :param data: list: the COSE Sign1 as a list + :return: Sign1Message: the COSE Sign1 object """ decoded = Sign1Message.decode( cbor2.dumps( @@ -32,7 +38,12 @@ def cborlist2CoseSign1(data: list) -> Sign1Message: return decoded -def pretty_print(cbor_loaded: dict): +def pretty_print(cbor_loaded: dict) -> None: + """" + Pretty print a CBOR object + + :param cbor_loaded: dict: the CBOR object + """ _obj = key_to_str(cbor_loaded) res = json.dumps( _obj, @@ -42,7 +53,13 @@ def pretty_print(cbor_loaded: dict): print(res) -def shuffle_dict(d: dict): +def shuffle_dict(d: dict) -> dict: + """ + Shuffle a dictionary + + :param d: dict: the dictionary to shuffle + :return: dict: the shuffled dictionary + """ keys = list(d.keys()) diff --git a/pymdoccbor/x509.py b/pymdoccbor/x509.py index 0c65bef..a2f8f12 100644 --- a/pymdoccbor/x509.py +++ b/pymdoccbor/x509.py @@ -2,19 +2,27 @@ import os from cwt import COSEKey +from typing import Union from cryptography import x509 from cryptography.x509.oid import NameOID +from cryptography.x509 import Certificate from cryptography.hazmat.primitives import hashes, serialization -from . import settings - +from pymdoccbor import settings class MsoX509Fabric: + """ + MsoX509Fabric helper class to create a new mso + """ - def selfsigned_x509cert(self, encoding: str = "DER"): + def selfsigned_x509cert(self, encoding: str = "DER") -> Union[Certificate, bytes]: """ - returns an X.509 certificate derived from the private key of the MSO Issuer + Returns an X.509 certificate derived from the private key of the MSO Issuer + + :param encoding: str: the encoding to use, default is DER + + :return: Union[Certificate, bytes]: the X.509 certificate """ ckey = COSEKey.from_bytes(self.private_key.encode()) From e66161146eb3d8d290cc5b305b8b0fe0dec05e07 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 16:05:50 +0100 Subject: [PATCH 25/31] fix: error handling --- pymdoccbor/mdoc/issuersigned.py | 6 ++++-- pymdoccbor/mdoc/verifier.py | 7 +++++++ pymdoccbor/mso/issuer.py | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pymdoccbor/mdoc/issuersigned.py b/pymdoccbor/mdoc/issuersigned.py index 866e974..f8207be 100644 --- a/pymdoccbor/mdoc/issuersigned.py +++ b/pymdoccbor/mdoc/issuersigned.py @@ -3,7 +3,6 @@ from pymdoccbor.mso.verifier import MsoVerifier - class IssuerSigned: """ nameSpaces provides the definition within which the data elements of @@ -22,7 +21,7 @@ class IssuerSigned: ] """ - def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]) -> None: + def __init__(self, nameSpaces: dict, issuerAuth: Union[cbor2.CBORTag, dict, bytes]) -> None: """ Initialize the IssuerSigned object @@ -32,6 +31,9 @@ def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]) -> None: self.namespaces: dict = nameSpaces + if not issuerAuth: + raise ValueError("issuerAuth must be provided") + # if isinstance(ia, dict): self.issuer_auth = MsoVerifier(issuerAuth) diff --git a/pymdoccbor/mdoc/verifier.py b/pymdoccbor/mdoc/verifier.py index 0bbb7cf..03f2be5 100644 --- a/pymdoccbor/mdoc/verifier.py +++ b/pymdoccbor/mdoc/verifier.py @@ -29,7 +29,14 @@ def __init__(self, docType: str, issuerSigned: dict, deviceSigned: dict = {}) -> :param deviceSigned: dict: the deviceSigned info """ + if not docType: + raise ValueError("You must provide a document type") + self.doctype: str = docType # eg: 'org.iso.18013.5.1.mDL' + + if not issuerSigned: + raise ValueError("You must provide a signed document") + self.issuersigned: List[IssuerSigned] = IssuerSigned(**issuerSigned) self.is_valid = False self.devicesigned: dict = deviceSigned diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index da988d0..c8c979f 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -73,6 +73,12 @@ 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 = {} From 23e41c274c0294f5243cb89ec4d4b98465c41265 Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 16:06:06 +0100 Subject: [PATCH 26/31] tests: restored tests --- pymdoccbor/tests/pkey.py | 9 ++ pymdoccbor/tests/test_03_mdoc_issuer.py | 73 +++++++++++++++ pymdoccbor/tests/test_04_issuer_signed.py | 52 +++++++++++ pymdoccbor/tests/test_05_mdoc_verifier.py | 107 ++++++++++++++++++++++ pymdoccbor/tests/test_06_mso_issuer.py | 43 +++++++++ pymdoccbor/tests/test_07_mso_verifier.py | 67 ++++++++++++++ pymdoccbor/tests/test_08_mdoc_cbor.py | 9 +- 7 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 pymdoccbor/tests/pkey.py create mode 100644 pymdoccbor/tests/test_03_mdoc_issuer.py create mode 100644 pymdoccbor/tests/test_04_issuer_signed.py create mode 100644 pymdoccbor/tests/test_05_mdoc_verifier.py create mode 100644 pymdoccbor/tests/test_06_mso_issuer.py create mode 100644 pymdoccbor/tests/test_07_mso_verifier.py diff --git a/pymdoccbor/tests/pkey.py b/pymdoccbor/tests/pkey.py new file mode 100644 index 0000000..97e3210 --- /dev/null +++ b/pymdoccbor/tests/pkey.py @@ -0,0 +1,9 @@ +import os + +PKEY = { + 'KTY': 'EC2', + 'CURVE': 'P_256', + 'ALG': 'ES256', + 'D': b"<\xe5\xbc;\x08\xadF\x1d\xc5\x0czR'T&\xbb\x91\xac\x84\xdc\x9ce\xbf\x0b,\x00\xcb\xdd\xbf\xec\xa2\xa5", + 'KID': b"demo-kid" +} \ No newline at end of file diff --git a/pymdoccbor/tests/test_03_mdoc_issuer.py b/pymdoccbor/tests/test_03_mdoc_issuer.py new file mode 100644 index 0000000..34deb89 --- /dev/null +++ b/pymdoccbor/tests/test_03_mdoc_issuer.py @@ -0,0 +1,73 @@ +from pycose.keys import EC2Key +from pymdoccbor.mdoc.issuer import MdocCborIssuer +from pymdoccbor.tests.micov_data import MICOV_DATA +from pymdoccbor.tests.pid_data import PID_DATA +from pymdoccbor.tests.pkey import PKEY + +mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256", +) + +def test_MdocCborIssuer_creation(): + assert mdoc.version == '1.0' + assert mdoc.status == 0 + +def test_mdoc_without_private_key_must_fail(): + try: + MdocCborIssuer(None) + except Exception as e: + assert str(e) == "You must provide a private key" + +def test_MdocCborIssuer_new_single(): + mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + ) + assert mdoc.signed['version'] == '1.0' + assert mdoc.signed['status'] == 0 + assert mdoc.signed['documents'][0]['docType'] == 'org.micov.medical.1' + assert mdoc.signed['documents'][0]['issuerSigned']['nameSpaces']['org.micov.medical.1'][0].tag == 24 + +# TODO: restore multiple documents support +""" +def test_MdocCborIssuer_new_multiple(): + micov_data = {"doctype": "org.micov.medical.1", "data": MICOV_DATA} + pid_data = {"doctype": "eu.europa.ec.eudiw.pid.1", "data": PID_DATA} + + mdoc.new( + #TODO: fix the doctype handling + doctype="org.micov.medical.1", + data=[micov_data, pid_data], + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + #devicekeyinfo=PKEY # TODO + ) + assert mdoc.signed['version'] == '1.0' + assert mdoc.signed['status'] == 0 + assert mdoc.signed['documents'][0]['docType'] == 'org.micov.medical.1' + assert mdoc.signed['documents'][0]['issuerSigned']['nameSpaces']['org.micov.medical.1'][0].tag == 24 + assert mdoc.signed['documents'][1]['docType'] == 'eu.europa.ec.eudiw.pid.1' + assert mdoc.signed['documents'][1]['issuerSigned']['nameSpaces']['eu.europa.ec.eudiw.pid.1'][0].tag == 24 +""" + +def test_MdocCborIssuer_dump(): + dump = mdoc.dump() + + assert dump + assert isinstance(dump, bytes) + assert len(dump) > 0 + +def test_MdocCborIssuer_dumps(): + dumps = mdoc.dumps() + + assert dumps + assert isinstance(dumps, bytes) + assert len(dumps) > 0 \ No newline at end of file diff --git a/pymdoccbor/tests/test_04_issuer_signed.py b/pymdoccbor/tests/test_04_issuer_signed.py new file mode 100644 index 0000000..efd6969 --- /dev/null +++ b/pymdoccbor/tests/test_04_issuer_signed.py @@ -0,0 +1,52 @@ +from pycose.keys import EC2Key +from pymdoccbor.mdoc.issuersigned import IssuerSigned +from pymdoccbor.mdoc.issuer import MdocCborIssuer +from pymdoccbor.tests.micov_data import MICOV_DATA +from pymdoccbor.tests.pkey import PKEY + + +mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256", +) +mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, +) +issuerAuth = mdoc.signed["documents"][0]["issuerSigned"] +issuer_signed = IssuerSigned(**issuerAuth) + +def test_issuer_signed_fail(): + try: + IssuerSigned(None, None) + except Exception as e: + assert str(e) == "issuerAuth must be provided" + +def test_issuer_signed_creation(): + assert issuer_signed.namespaces + assert issuer_signed.issuer_auth + +def test_issuer_signed_dump(): + issuerAuth = mdoc.signed["documents"][0]["issuerSigned"] + + issuer_signed = IssuerSigned(**issuerAuth) + + dump = issuer_signed.dump() + assert dump + assert dump["nameSpaces"] == issuer_signed.namespaces + assert dump["issuerAuth"] == issuer_signed.issuer_auth + +def test_issuer_signed_dumps(): + issuerAuth = mdoc.signed["documents"][0]["issuerSigned"] + + issuer_signed = IssuerSigned(**issuerAuth) + + dumps = issuer_signed.dumps() + assert dumps + assert isinstance(dumps, bytes) + assert len(dumps) > 0 \ No newline at end of file diff --git a/pymdoccbor/tests/test_05_mdoc_verifier.py b/pymdoccbor/tests/test_05_mdoc_verifier.py new file mode 100644 index 0000000..58fe29b --- /dev/null +++ b/pymdoccbor/tests/test_05_mdoc_verifier.py @@ -0,0 +1,107 @@ +from pycose.keys import EC2Key +from pymdoccbor.mdoc.verifier import MobileDocument +from pymdoccbor.mdoc.issuer import MdocCborIssuer +from pymdoccbor.tests.micov_data import MICOV_DATA +from pymdoccbor.tests.pkey import PKEY + +def test_verifier_must_fail_document_type(): + try: + MobileDocument(None, None) + except Exception as e: + assert str(e) == "You must provide a document type" + +def test_verifier_must_fail_issuer_signed(): + try: + MobileDocument("org.micov.medical.1", None) + except Exception as e: + assert str(e) == "You must provide a signed document" + +def test_mobile_document(): + mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256", + ) + mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + ) + + + document = mdoc.signed["documents"][0] + doc = MobileDocument(**document) + + assert doc.doctype == "org.micov.medical.1" + assert doc.issuersigned + +def test_mobile_document_dump(): + mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256" + ) + mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + ) + + + document = mdoc.signed["documents"][0] + doc = MobileDocument(**document) + + dump = doc.dump() + assert dump + assert isinstance(dump, bytes) + assert len(dump) > 0 + +def test_mobile_document_dumps(): + mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256" + ) + mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + ) + + + document = mdoc.signed["documents"][0] + doc = MobileDocument(**document) + + dumps = doc.dumps() + assert dumps + assert isinstance(dumps, bytes) + assert len(dumps) > 0 + +def test_mobile_document_verify(): + mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256" + ) + mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + ) + + document = mdoc.signed["documents"][0] + doc = MobileDocument(**document) + + assert doc.verify() diff --git a/pymdoccbor/tests/test_06_mso_issuer.py b/pymdoccbor/tests/test_06_mso_issuer.py new file mode 100644 index 0000000..ec30cce --- /dev/null +++ b/pymdoccbor/tests/test_06_mso_issuer.py @@ -0,0 +1,43 @@ +from pycose.keys import EC2Key +from pycose.messages import CoseMessage +from pymdoccbor.mso.issuer import MsoIssuer +from pymdoccbor.tests.micov_data import MICOV_DATA +from pymdoccbor.tests.pkey import PKEY + + +def test_mso_issuer_fail(): + try: + MsoIssuer(None, None) + except Exception as e: + assert str(e) == "MSO Writer requires a valid private key" + +def test_mso_issuer_creation(): + msoi = MsoIssuer( + data=MICOV_DATA, + private_key=PKEY, + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + alg="ES256" + ) + + assert msoi.private_key + assert msoi.data + assert msoi.hash_map + assert list(msoi.hash_map.keys())[0] == 'org.micov.medical.1' + assert msoi.disclosure_map['org.micov.medical.1'] + +def test_mso_issuer_sign(): + msoi = MsoIssuer( + data=MICOV_DATA, + private_key=PKEY, + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, + alg="ES256" + ) + + mso = msoi.sign() + assert isinstance(mso, CoseMessage) \ No newline at end of file diff --git a/pymdoccbor/tests/test_07_mso_verifier.py b/pymdoccbor/tests/test_07_mso_verifier.py new file mode 100644 index 0000000..a385bc3 --- /dev/null +++ b/pymdoccbor/tests/test_07_mso_verifier.py @@ -0,0 +1,67 @@ + +import os +from pycose.keys import CoseKey, EC2Key +from pymdoccbor.mso.verifier import MsoVerifier +from pymdoccbor.mdoc.issuer import MdocCborIssuer +from pymdoccbor.tests.micov_data import MICOV_DATA +from pycose.messages import CoseMessage +from pymdoccbor.tests.pkey import PKEY + + +mdoc = MdocCborIssuer( + private_key=PKEY, + alg="ES256", +) + +mdoc.new( + data=MICOV_DATA, + #devicekeyinfo=PKEY, # TODO + doctype="org.micov.medical.1", + validity={ + "issuance_date": "2024-12-31", + "expiry_date": "2050-12-31" + }, +) + +def test_mso_verifier_fail(): + try: + MsoVerifier(None) + except Exception as e: + assert str(e) == "MsoParser only supports raw bytes and list, a was provided" + +def test_mso_verifier_creation(): + issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] + + msov = MsoVerifier(issuerAuth) + + assert isinstance(msov.object, CoseMessage) + +def test_mso_verifier_verify_signatures(): + issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] + + msov = MsoVerifier(issuerAuth) + + assert msov.verify_signature() + +def test_mso_verifier_payload_as_cbor(): + issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] + + msov = MsoVerifier(issuerAuth) + + cbor = msov.payload_as_dict + + assert cbor + assert cbor["version"] == "1.0" + assert cbor["digestAlgorithm"] == "SHA-256" + assert cbor["valueDigests"]["org.micov.medical.1"] + +def test_payload_as_raw(): + issuerAuth = mdoc.signed["documents"][0]["issuerSigned"]["issuerAuth"] + + msov = MsoVerifier(issuerAuth) + + raw = msov.payload_as_raw + + assert raw + assert isinstance(raw, bytes) + assert len(raw) > 0 \ No newline at end of file diff --git a/pymdoccbor/tests/test_08_mdoc_cbor.py b/pymdoccbor/tests/test_08_mdoc_cbor.py index bff835d..eab9e88 100644 --- a/pymdoccbor/tests/test_08_mdoc_cbor.py +++ b/pymdoccbor/tests/test_08_mdoc_cbor.py @@ -3,14 +3,7 @@ from pymdoccbor.mdoc.issuer import MdocCborIssuer from pymdoccbor.tests.micov_data import MICOV_DATA from pymdoccbor.mdoc.verifier import MdocCbor - -PKEY = { - 'KTY': 'EC2', - 'CURVE': 'P_256', - 'ALG': 'ES256', - 'D': os.urandom(32), - 'KID': b"demo-kid" -} +from pymdoccbor.tests.pkey import PKEY def test_mdoc_cbor_creation(): mdoci = MdocCborIssuer( From 6a432df5ab06b8ac9a9daf1275aa8ce44d67a19e Mon Sep 17 00:00:00 2001 From: PascalDR Date: Fri, 14 Mar 2025 16:10:34 +0100 Subject: [PATCH 27/31] fix: restored specialized exceptions --- pymdoccbor/mdoc/exceptions.py | 11 +++++++++++ pymdoccbor/mdoc/verifier.py | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 pymdoccbor/mdoc/exceptions.py diff --git a/pymdoccbor/mdoc/exceptions.py b/pymdoccbor/mdoc/exceptions.py new file mode 100644 index 0000000..1cb70ad --- /dev/null +++ b/pymdoccbor/mdoc/exceptions.py @@ -0,0 +1,11 @@ +class MissingPrivateKey(Exception): + pass + +class NoDocumentTypeProvided(Exception): + pass + +class NoSignedDocumentProvided(Exception): + pass + +class MissingIssuerAuth(Exception): + pass diff --git a/pymdoccbor/mdoc/verifier.py b/pymdoccbor/mdoc/verifier.py index 03f2be5..51ae9b7 100644 --- a/pymdoccbor/mdoc/verifier.py +++ b/pymdoccbor/mdoc/verifier.py @@ -6,6 +6,7 @@ from pymdoccbor.exceptions import InvalidMdoc from pymdoccbor.mdoc.issuersigned import IssuerSigned +from pymdoccbor.mdoc.exceptions import NoDocumentTypeProvided, NoSignedDocumentProvided logger = logging.getLogger('pymdoccbor') @@ -30,12 +31,12 @@ def __init__(self, docType: str, issuerSigned: dict, deviceSigned: dict = {}) -> """ if not docType: - raise ValueError("You must provide a document type") + raise NoDocumentTypeProvided("You must provide a document type") self.doctype: str = docType # eg: 'org.iso.18013.5.1.mDL' if not issuerSigned: - raise ValueError("You must provide a signed document") + raise NoSignedDocumentProvided("You must provide a signed document") self.issuersigned: List[IssuerSigned] = IssuerSigned(**issuerSigned) self.is_valid = False From 7e4737ff77d5d2430c19522647a7feb86654e2b3 Mon Sep 17 00:00:00 2001 From: Pasquale De Rose Date: Mon, 17 Mar 2025 16:38:47 +0100 Subject: [PATCH 28/31] Update README.md Co-authored-by: Giuseppe De Marco --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c5d518d..54ec39e 100644 --- a/README.md +++ b/README.md @@ -256,3 +256,4 @@ Other examples at [cbor official documentation](https://github.com/agronholm/cbo ## Authors and contributors - Giuseppe De Marco +- Pasquale De Rose From 58985b6af3ba4a4c9d467a44b2a3af6fff05f1e4 Mon Sep 17 00:00:00 2001 From: Giuseppe De Marco Date: Tue, 18 Mar 2025 10:55:12 +0100 Subject: [PATCH 29/31] Apply suggestions from code review --- README.md | 4 ++-- pymdoccbor/mso/issuer.py | 5 ++--- pymdoccbor/settings.py | 2 +- setup.py | 19 +------------------ 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 54ec39e..0ec049c 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ mdoci = MdocCborIssuer( mdoc = mdoci.new( doctype="eu.europa.ec.eudiw.pid.1", data=PID_DATA, - cert_path="app/keys/IACAmDLRoot01.der" # DS certificate + # cert_path="app/keys/IACAmDLRoot01.der" # DS certificate ) ```` @@ -246,7 +246,7 @@ Other examples at [cbor official documentation](https://github.com/agronholm/cbo #### CBOR Diagnostic representation - [CBOR-DIAG-PY](https://github.com/chrysn/cbor-diag-py) -- [Authlete's CBOR diagnostic tools](https://nextdev-api.authlete.net/api/cbor) +- [Authlete's CBOR diagnostic tools](https://nextdev-api.authlete.net/api/cbor) - [Auth0 CBOR diagnostic tool](https://www.mdl.me/) #### X.509 certificates and chains diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index c8c979f..eb10a2d 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -110,7 +110,6 @@ def __init__( if _value_cbortag: v = cbor2.CBORTag(_value_cbortag, value=v) - # print("\n-----\n K,V ", k, "\n", v) if isinstance(v, dict): for k2, v2 in v.items(): @@ -160,7 +159,7 @@ def sign( doctype: str = None, ) -> Sign1Message: """ - Sign a mso and returns itprivate_key + Sign a mso and returns it :param device_key: Union[dict, None]: the device key :param valid_from: Union[None, datetime.datetime]: the valid from date @@ -234,7 +233,7 @@ def sign( ) else: - # print("payload diganostic notation: \n", cbor2diag(cbor2.dumps(cbor2.CBORTag(24,cbor2.dumps(payload))))) + logger.debug("payload diagnostic notation: {cbor2diag(cbor2.dumps(cbor2.CBORTag(24,cbor2.dumps(payload))))}") mso = Sign1Message( phdr={ diff --git a/pymdoccbor/settings.py b/pymdoccbor/settings.py index 6527225..cef5181 100644 --- a/pymdoccbor/settings.py +++ b/pymdoccbor/settings.py @@ -36,7 +36,7 @@ ) X509_SAN_URL = os.getenv( - "X509_SAN_URL", "https://credential-issuer.oidc-federation.online" + "X509_SAN_URL", "https://credential-issuer.example.org" ) CBORTAGS_ATTR_MAP = { diff --git a/setup.py b/setup.py index 1f875b3..af16d41 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,3 @@ -# Modifications have been made to the original file (available at https://github.com/IdentityPython/pyMDOC-CBOR) -# All modifications Copyright (c) 2023 European Commission - -# All modifications licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import re from glob import glob @@ -40,13 +25,11 @@ def readme(): classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries :: Python Modules", ], - url="https://github.com/peppelinux/pyMDL-MDOC", + url="https://github.com/IdentityPython/pyMDL-MDOC", author="Giuseppe De Marco", author_email="demarcog83@gmail.com", license="License :: OSI Approved :: Apache Software License", From 18270866c94b34277813bb8c3d2c52f269a8f51f Mon Sep 17 00:00:00 2001 From: Giuseppe De Marco Date: Tue, 18 Mar 2025 11:04:03 +0100 Subject: [PATCH 30/31] Apply suggestions from code review --- README.md | 2 +- pymdoccbor/mdoc/issuer.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0ec049c..429ea90 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ mdoc = mdoci.new( doctype="eu.europa.ec.eudiw.pid.1", data=PID_DATA, devicekeyinfo=PKEY # TODO - cert_path="/path/" + # cert_path="/path/" ) mdoc diff --git a/pymdoccbor/mdoc/issuer.py b/pymdoccbor/mdoc/issuer.py index 8a3c0b1..beca979 100644 --- a/pymdoccbor/mdoc/issuer.py +++ b/pymdoccbor/mdoc/issuer.py @@ -47,6 +47,10 @@ def __init__( if private_key: if isinstance(private_key, dict): self.private_key = CoseKey.from_dict(private_key) + elif isinstance(private_key, EC2Key): + ec2_encoded = private_key.encode() + ec2_decoded = CoseKey.decode(ec2_encoded) + self.private_key = ec2_decoded elif isinstance(private_key, CoseKey): self.private_key = private_key else: @@ -173,7 +177,7 @@ def new( "status": self.status, } - # print("mso diganostic notation: \n", cbor2diag(mso_cbor)) + logger.debug(f"MSO diagnostic notation: {cbor2diag(mso_cbor)}") self.signed = res return self.signed From 8d3d2be8ff330a66de04c310b00085e5ea433ecd Mon Sep 17 00:00:00 2001 From: Giuseppe De Marco Date: Tue, 18 Mar 2025 11:08:49 +0100 Subject: [PATCH 31/31] Apply suggestions from code review --- pymdoccbor/mso/issuer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pymdoccbor/mso/issuer.py b/pymdoccbor/mso/issuer.py index 0305dad..3d943a3 100644 --- a/pymdoccbor/mso/issuer.py +++ b/pymdoccbor/mso/issuer.py @@ -3,7 +3,9 @@ import hashlib import secrets import uuid +import logging +logger = logging.getLogger("pymdoccbor") from pycose.headers import Algorithm from pycose.keys import CoseKey