From 39ab4fe946c9faff97959c25a0575d3b70b06f0c Mon Sep 17 00:00:00 2001 From: Luis Pereira Date: Mon, 1 Jul 2024 10:59:47 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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)