Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/tests] Unit tests #2

Merged
merged 21 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading