Skip to content

Commit c57434f

Browse files
authored
Merge pull request #2 from PascalDR/feat/tests
[Feat/tests] Unit tests
2 parents 74a3a6b + 1aedd54 commit c57434f

15 files changed

+561
-69
lines changed

.github/workflows/python-app.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
python-version:
21-
- '3.9'
2221
- '3.10'
22+
- '3.11'
2323

2424
steps:
2525
- uses: actions/checkout@v2

pymdoccbor/mdoc/exceptions.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class MissingPrivateKey(Exception):
2+
pass
3+
4+
class NoDocumentTypeProvided(Exception):
5+
pass
6+
7+
class NoSignedDocumentProvided(Exception):
8+
pass
9+
10+
class MissingIssuerAuth(Exception):
11+
pass

pymdoccbor/mdoc/issuer.py

+80-33
Original file line numberDiff line numberDiff line change
@@ -2,79 +2,126 @@
22
import cbor2
33
import logging
44

5-
from pycose.keys import CoseKey
5+
from pycose.keys import CoseKey, EC2Key
66
from typing import Union
77

88
from pymdoccbor.mso.issuer import MsoIssuer
9+
from pymdoccbor.mdoc.exceptions import MissingPrivateKey
910

1011
logger = logging.getLogger('pymdoccbor')
1112

1213

1314
class MdocCborIssuer:
15+
"""
16+
MdocCborIssuer helper class to create a new mdoc
17+
"""
1418

15-
def __init__(self, private_key: Union[dict, CoseKey] = {}):
19+
def __init__(self, private_key: Union[dict, EC2Key, CoseKey]):
20+
"""
21+
Create a new MdocCborIssuer instance
22+
23+
:param private_key: the private key to sign the mdoc
24+
:type private_key: dict | CoseKey
25+
26+
:raises MissingPrivateKey: if no private key is provided
27+
"""
1628
self.version: str = '1.0'
1729
self.status: int = 0
18-
if private_key and isinstance(private_key, dict):
30+
31+
if isinstance(private_key, dict):
1932
self.private_key = CoseKey.from_dict(private_key)
33+
elif isinstance(private_key, EC2Key):
34+
ec2_encoded = private_key.encode()
35+
ec2_decoded = CoseKey.decode(ec2_encoded)
36+
self.private_key = ec2_decoded
37+
elif isinstance(private_key, CoseKey):
38+
self.private_key = private_key
39+
else:
40+
raise MissingPrivateKey("You must provide a private key")
41+
2042

2143
self.signed :dict = {}
2244

2345
def new(
2446
self,
25-
data: dict,
47+
data: dict | list[dict],
2648
devicekeyinfo: Union[dict, CoseKey],
27-
doctype: str
28-
):
49+
doctype: str | None = None
50+
) -> dict:
2951
"""
3052
create a new mdoc with signed mso
53+
54+
:param data: the data to sign
55+
Can be a dict, representing the single document, or a list of dicts containg the doctype and the data
56+
Example:
57+
{doctype: "org.iso.18013.5.1.mDL", data: {...}}
58+
:type data: dict | list[dict]
59+
:param devicekeyinfo: the device key info
60+
:type devicekeyinfo: dict | CoseKey
61+
:param doctype: the document type (optional if data is a list)
62+
:type doctype: str | None
63+
64+
:return: the signed mdoc
65+
:rtype: dict
3166
"""
3267
if isinstance(devicekeyinfo, dict):
3368
devicekeyinfo = CoseKey.from_dict(devicekeyinfo)
3469
else:
3570
devicekeyinfo: CoseKey = devicekeyinfo
3671

37-
msoi = MsoIssuer(
38-
data=data,
39-
private_key=self.private_key
40-
)
72+
if isinstance(data, dict):
73+
data = [{"doctype": doctype, "data": data}]
4174

42-
mso = msoi.sign()
75+
documents = []
76+
77+
for doc in data:
78+
msoi = MsoIssuer(
79+
data=doc["data"],
80+
private_key=self.private_key
81+
)
82+
83+
mso = msoi.sign()
84+
85+
document = {
86+
'docType': doc["doctype"], # 'org.iso.18013.5.1.mDL'
87+
'issuerSigned': {
88+
"nameSpaces": {
89+
ns: [
90+
cbor2.CBORTag(24, value={k: v}) for k, v in dgst.items()
91+
]
92+
for ns, dgst in msoi.disclosure_map.items()
93+
},
94+
"issuerAuth": mso.encode()
95+
},
96+
# this is required during the presentation.
97+
# 'deviceSigned': {
98+
# # TODO
99+
# }
100+
}
101+
102+
documents.append(document)
43103

44-
# TODO: for now just a single document, it would be trivial having
45-
# also multiple but for now I don't have use cases for this
46104
self.signed = {
47105
'version': self.version,
48-
'documents': [
49-
{
50-
'docType': doctype, # 'org.iso.18013.5.1.mDL'
51-
'issuerSigned': {
52-
"nameSpaces": {
53-
ns: [
54-
cbor2.CBORTag(24, value={k: v}) for k, v in dgst.items()
55-
]
56-
for ns, dgst in msoi.disclosure_map.items()
57-
},
58-
"issuerAuth": mso.encode()
59-
},
60-
# this is required during the presentation.
61-
# 'deviceSigned': {
62-
# # TODO
63-
# }
64-
}
65-
],
106+
'documents': documents,
66107
'status': self.status
67108
}
68109
return self.signed
69110

70111
def dump(self):
71112
"""
72-
returns bytes
113+
Returns the signed mdoc in CBOR format
114+
115+
:return: the signed mdoc in CBOR format
116+
:rtype: bytes
73117
"""
74118
return cbor2.dumps(self.signed)
75119

76120
def dumps(self):
77121
"""
78-
returns AF binary repr
122+
Returns the signed mdoc in AF binary repr
123+
124+
:return: the signed mdoc in AF binary repr
125+
:rtype: bytes
79126
"""
80127
return binascii.hexlify(cbor2.dumps(self.signed))

pymdoccbor/mdoc/issuersigned.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from typing import Union
33

44
from pymdoccbor.mso.verifier import MsoVerifier
5+
from pymdoccbor.mdoc.exceptions import MissingIssuerAuth
56

67

78
class IssuerSigned:
89
"""
10+
IssuerSigned helper class to handle issuer signed data
11+
912
nameSpaces provides the definition within which the data elements of
1013
the document are defined.
1114
A document may have multiple nameSpaces.
@@ -22,19 +25,43 @@ class IssuerSigned:
2225
]
2326
"""
2427

25-
def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]):
28+
def __init__(self, nameSpaces: dict, issuerAuth: Union[dict, bytes]) -> None:
29+
"""
30+
Create a new IssuerSigned instance
31+
32+
:param nameSpaces: the namespaces
33+
:type nameSpaces: dict
34+
:param issuerAuth: the issuer auth
35+
:type issuerAuth: dict | bytes
36+
37+
:raises MissingIssuerAuth: if no issuer auth is provided
38+
"""
2639
self.namespaces: dict = nameSpaces
2740

28-
# if isinstance(ia, dict):
41+
if not issuerAuth:
42+
raise MissingIssuerAuth("issuerAuth must be provided")
43+
2944
self.issuer_auth = MsoVerifier(issuerAuth)
3045

3146
def dump(self) -> dict:
47+
"""
48+
Returns a dict representation of the issuer signed data
49+
50+
:return: the issuer signed data as dict
51+
:rtype: dict
52+
"""
3253
return {
3354
'nameSpaces': self.namespaces,
3455
'issuerAuth': self.issuer_auth
3556
}
3657

37-
def dumps(self) -> dict:
58+
def dumps(self) -> bytes:
59+
"""
60+
Returns a CBOR representation of the issuer signed data
61+
62+
:return: the issuer signed data as CBOR
63+
:rtype: bytes
64+
"""
3865
return cbor2.dumps(
3966
{
4067
'nameSpaces': self.namespaces,

pymdoccbor/mdoc/verifier.py

+49-5
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,93 @@
66

77
from pymdoccbor.exceptions import InvalidMdoc
88
from pymdoccbor.mdoc.issuersigned import IssuerSigned
9+
from pymdoccbor.mdoc.exceptions import NoDocumentTypeProvided, NoSignedDocumentProvided
910

1011
logger = logging.getLogger('pymdoccbor')
1112

1213

1314
class MobileDocument:
15+
"""
16+
MobileDocument helper class to verify a mdoc
17+
"""
18+
1419
_states = {
1520
True: "valid",
1621
False: "failed",
1722
}
1823

1924
def __init__(self, docType: str, issuerSigned: dict, deviceSigned: dict = {}):
25+
"""
26+
Create a new MobileDocument instance
27+
28+
:param docType: the document type
29+
:type docType: str
30+
:param issuerSigned: the issuer signed data
31+
:type issuerSigned: dict
32+
:param deviceSigned: the device signed data
33+
:type deviceSigned: dict
34+
35+
:raises NoDocumentTypeProvided: if no document type is provided
36+
:raises NoSignedDocumentProvided: if no signed document is provided
37+
"""
38+
39+
if not docType:
40+
raise NoDocumentTypeProvided("You must provide a document type")
41+
42+
if not issuerSigned:
43+
raise NoSignedDocumentProvided("You must provide a signed document")
44+
2045
self.doctype: str = docType # eg: 'org.iso.18013.5.1.mDL'
21-
self.issuersigned: List[IssuerSigned] = IssuerSigned(**issuerSigned)
46+
self.issuersigned: IssuerSigned = IssuerSigned(**issuerSigned)
2247
self.is_valid = False
2348

2449
# TODO
2550
self.devicesigned: dict = deviceSigned
2651

2752
def dump(self) -> dict:
53+
"""
54+
Returns a dict representation of the document
55+
56+
:return: the document as dict
57+
:rtype: dict
58+
"""
59+
2860
return {
2961
'docType': self.doctype,
3062
'issuerSigned': self.issuersigned.dump()
3163
}
3264

3365
def dumps(self) -> str:
3466
"""
35-
returns an AF binary repr of the document
67+
Returns an AF binary repr of the document
68+
69+
:return: the document as AF binary
70+
:rtype: str
3671
"""
3772
return binascii.hexlify(self.dump())
3873

3974
def dump(self) -> bytes:
4075
"""
41-
returns bytes
76+
Returns a CBOR repr of the document
77+
78+
:return: the document as CBOR
79+
:rtype: bytes
4280
"""
4381
return cbor2.dumps(
4482
cbor2.CBORTag(24, value={
4583
'docType': self.doctype,
4684
'issuerSigned': self.issuersigned.dumps()
47-
}
48-
)
85+
})
4986
)
5087

5188
def verify(self) -> bool:
89+
"""
90+
Verify the document signature
91+
92+
:return: True if valid, False otherwise
93+
:rtype: bool
94+
"""
95+
5296
self.is_valid = self.issuersigned.issuer_auth.verify_signature()
5397
return self.is_valid
5498

0 commit comments

Comments
 (0)