Skip to content

Commit

Permalink
Merge pull request #2 from PascalDR/feat/tests
Browse files Browse the repository at this point in the history
[Feat/tests] Unit tests
  • Loading branch information
peppelinux authored Feb 8, 2024
2 parents 74a3a6b + 1aedd54 commit c57434f
Show file tree
Hide file tree
Showing 15 changed files with 561 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
fail-fast: false
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11'

steps:
- uses: actions/checkout@v2
Expand Down
11 changes: 11 additions & 0 deletions pymdoccbor/mdoc/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class MissingPrivateKey(Exception):
pass

class NoDocumentTypeProvided(Exception):
pass

class NoSignedDocumentProvided(Exception):
pass

class MissingIssuerAuth(Exception):
pass
113 changes: 80 additions & 33 deletions pymdoccbor/mdoc/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,126 @@
import cbor2
import logging

from pycose.keys import CoseKey
from pycose.keys import CoseKey, EC2Key
from typing import Union

from pymdoccbor.mso.issuer import MsoIssuer
from pymdoccbor.mdoc.exceptions import MissingPrivateKey

logger = logging.getLogger('pymdoccbor')


class MdocCborIssuer:
"""
MdocCborIssuer helper class to create a new mdoc
"""

def __init__(self, private_key: Union[dict, CoseKey] = {}):
def __init__(self, private_key: Union[dict, EC2Key, CoseKey]):
"""
Create a new MdocCborIssuer instance
:param private_key: the private key to sign the mdoc
:type private_key: dict | CoseKey
:raises MissingPrivateKey: if no private key is provided
"""
self.version: str = '1.0'
self.status: int = 0
if private_key and isinstance(private_key, dict):

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:
raise MissingPrivateKey("You must provide a private key")


self.signed :dict = {}

def new(
self,
data: dict,
data: dict | list[dict],
devicekeyinfo: Union[dict, CoseKey],
doctype: str
):
doctype: str | None = None
) -> dict:
"""
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)
else:
devicekeyinfo: CoseKey = devicekeyinfo

msoi = MsoIssuer(
data=data,
private_key=self.private_key
)
if isinstance(data, dict):
data = [{"doctype": doctype, "data": data}]

mso = msoi.sign()
documents = []

for doc in data:
msoi = MsoIssuer(
data=doc["data"],
private_key=self.private_key
)

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
# }
}

documents.append(document)

# 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
self.signed = {
'version': self.version,
'documents': [
{
'docType': 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
# }
}
],
'documents': documents,
'status': self.status
}
return self.signed

def dump(self):
"""
returns bytes
Returns the signed mdoc in CBOR format
:return: the signed mdoc in CBOR format
:rtype: bytes
"""
return cbor2.dumps(self.signed)

def dumps(self):
"""
returns AF binary repr
Returns the signed mdoc in AF binary repr
:return: the signed mdoc in AF binary repr
:rtype: bytes
"""
return binascii.hexlify(cbor2.dumps(self.signed))
33 changes: 30 additions & 3 deletions pymdoccbor/mdoc/issuersigned.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
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.
Expand All @@ -22,19 +25,43 @@ class IssuerSigned:
]
"""

def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]):
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
"""
self.namespaces: dict = nameSpaces

# if isinstance(ia, dict):
if not issuerAuth:
raise MissingIssuerAuth("issuerAuth must be provided")

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) -> dict:
def dumps(self) -> bytes:
"""
Returns a CBOR representation of the issuer signed data
:return: the issuer signed data as CBOR
:rtype: bytes
"""
return cbor2.dumps(
{
'nameSpaces': self.namespaces,
Expand Down
54 changes: 49 additions & 5 deletions pymdoccbor/mdoc/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,93 @@

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: List[IssuerSigned] = IssuerSigned(**issuerSigned)
self.issuersigned: 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()
}

def dumps(self) -> str:
"""
returns an AF binary repr of the document
Returns an AF binary repr of the document
:return: the document as AF binary
:rtype: str
"""
return binascii.hexlify(self.dump())

def dump(self) -> bytes:
"""
returns bytes
Returns a CBOR repr of the document
:return: the document as CBOR
:rtype: 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

Expand Down
Loading

0 comments on commit c57434f

Please sign in to comment.