Skip to content

Commit 2c14741

Browse files
committed
Add minimal support to integrate status list reference data in the mso/issuer.py - tests update (replaced revocation to status list for the sake of clarity)
Load also Y coordinate from DER certificate instead of calculating it + enhanced some "unit" tests Allow multiple doc support in IS0 18013-5 format Add support to load PEM certificate format (and not only DER ones)
1 parent 085f8c3 commit 2c14741

File tree

10 files changed

+157
-46
lines changed

10 files changed

+157
-46
lines changed

pymdoccbor/mdoc/issuer.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import binascii
33
import cbor2
44
import logging
5+
import datetime
56
from cryptography.hazmat.primitives import serialization
6-
from pycose.keys import CoseKey
7+
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
8+
from pycose.keys import CoseKey, EC2Key
79
from typing import Union
810

911
from pymdoccbor.mso.issuer import MsoIssuer
@@ -72,7 +74,8 @@ def new(
7274
validity: dict = None,
7375
devicekeyinfo: Union[dict, CoseKey, str] = None,
7476
cert_path: str = None,
75-
revocation: dict = None,
77+
pem_cert_path: str = None,
78+
status_list: dict = {},
7679
):
7780
"""
7881
create a new mdoc with signed mso
@@ -82,15 +85,21 @@ def new(
8285
:param validity: dict: validity info
8386
:param devicekeyinfo: Union[dict, CoseKey, str]: device key info
8487
:param cert_path: str: path to the certificate
85-
:param revocation: dict: revocation info
88+
:param status_list: dict: The status list to include in the mso of the mdoc
8689
8790
:return: dict: signed mdoc
8891
"""
8992
if isinstance(devicekeyinfo, dict):
90-
devicekeyinfo = CoseKey.from_dict(devicekeyinfo)
93+
devicekeyinfoCoseKeyObject = CoseKey.from_dict(devicekeyinfo)
94+
devicekeyinfo = {
95+
1: devicekeyinfoCoseKeyObject.kty.identifier,
96+
-1: devicekeyinfoCoseKeyObject.crv.identifier,
97+
-2: devicekeyinfoCoseKeyObject.x,
98+
-3: devicekeyinfoCoseKeyObject.y,
99+
}
91100
if isinstance(devicekeyinfo, str):
92101
device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8"))
93-
public_key = serialization.load_pem_public_key(device_key_bytes)
102+
public_key:EllipticCurvePublicKey = serialization.load_pem_public_key(device_key_bytes)
94103
curve_name = public_key.curve.name
95104
curve_map = {
96105
"secp256r1": 1, # NIST P-256
@@ -130,6 +139,7 @@ def new(
130139
msoi = MsoIssuer(
131140
data=data,
132141
cert_path=cert_path,
142+
pem_cert_path=pem_cert_path,
133143
hsm=self.hsm,
134144
key_label=self.key_label,
135145
user_pin=self.user_pin,
@@ -138,7 +148,7 @@ def new(
138148
alg=self.alg,
139149
kid=self.kid,
140150
validity=validity,
141-
revocation=revocation,
151+
status_list=status_list
142152
)
143153

144154
else:
@@ -147,11 +157,12 @@ def new(
147157
private_key=self.private_key,
148158
alg=self.alg,
149159
cert_path=cert_path,
160+
pem_cert_path=pem_cert_path,
150161
validity=validity,
151-
revocation=revocation,
162+
status_list=status_list
152163
)
153164

154-
mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo)
165+
mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now())
155166

156167
mso_cbor = mso.encode(
157168
tag=False,
@@ -162,18 +173,21 @@ def new(
162173
slot_id=self.slot_id,
163174
)
164175

176+
165177
res = {
166178
"version": self.version,
167-
"documents": [{
179+
"documents": [
180+
{
168181
"docType": doctype, # 'org.iso.18013.5.1.mDL'
169182
"issuerSigned": {
170183
"nameSpaces": {
171184
ns: [v for k, v in dgst.items()]
172185
for ns, dgst in msoi.disclosure_map.items()
173-
},
186+
},
174187
"issuerAuth": cbor2.decoder.loads(mso_cbor),
175-
},
176-
}],
188+
},
189+
}
190+
],
177191
"status": self.status,
178192
}
179193

pymdoccbor/mso/issuer.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,12 @@
77

88
logger = logging.getLogger("pymdoccbor")
99

10-
from pycose.headers import Algorithm
11-
from pycose.keys import CoseKey
12-
13-
from datetime import timezone
14-
1510
from pycose.headers import Algorithm #, KID
1611
from pycose.keys import CoseKey, EC2Key
17-
1812
from pycose.messages import Sign1Message
1913

2014
from typing import Union
2115

22-
2316
from pymdoccbor.exceptions import MsoPrivateKeyRequired
2417
from pymdoccbor import settings
2518
from pymdoccbor.x509 import MsoX509Fabric
@@ -40,8 +33,8 @@ def __init__(
4033
self,
4134
data: dict,
4235
validity: dict,
43-
revocation: str = None,
4436
cert_path: str = None,
37+
pem_cert_path: str = None,
4538
key_label: str = None,
4639
user_pin: str = None,
4740
lib_path: str = None,
@@ -51,13 +44,13 @@ def __init__(
5144
hsm: bool = False,
5245
private_key: Union[dict, CoseKey] = None,
5346
digest_alg: str = settings.PYMDOC_HASHALG,
47+
status_list: dict = {}
5448
) -> None:
5549
"""
5650
Initialize a new MsoIssuer
5751
5852
:param data: dict: the data to sign
5953
:param validity: validity: the validity info of the mso
60-
:param revocation: str: the revocation status
6154
:param cert_path: str: the path to the certificate
6255
:param key_label: str: key label
6356
:param user_pin: str: user pin
@@ -68,6 +61,7 @@ def __init__(
6861
:param hsm: bool: hardware security module
6962
:param private_key: Union[dict, CoseKey]: the signing key
7063
:param digest_alg: str: the digest algorithm
64+
:param status_list: dict: the status list to include in the mso
7165
"""
7266

7367
if not hsm:
@@ -82,16 +76,17 @@ def __init__(
8276
raise ValueError("private_key must be a dict or CoseKey object")
8377
else:
8478
raise MsoPrivateKeyRequired("MSO Writer requires a valid private key")
85-
79+
8680
if not validity:
8781
raise ValueError("validity must be present")
88-
82+
8983
if not alg:
9084
raise ValueError("alg must be present")
9185

9286
self.data: dict = data
9387
self.hash_map: dict = {}
9488
self.cert_path = cert_path
89+
self.pem_cert_path = pem_cert_path
9590
self.disclosure_map: dict = {}
9691
self.digest_alg: str = digest_alg
9792
self.key_label = key_label
@@ -102,7 +97,7 @@ def __init__(
10297
self.alg = alg
10398
self.kid = kid
10499
self.validity = validity
105-
self.revocation = revocation
100+
self.status_list = status_list
106101

107102
alg_map = {"ES256": "sha256", "ES384": "sha384", "ES512": "sha512"}
108103

@@ -209,18 +204,24 @@ def sign(
209204
"deviceKey": device_key,
210205
},
211206
"digestAlgorithm": alg_map.get(self.alg),
207+
"status": self.status_list
212208
}
213209

214-
if self.revocation is not None:
215-
payload.update({"status": self.revocation})
216-
217210
if self.cert_path:
218211
# Load the DER certificate file
219212
with open(self.cert_path, "rb") as file:
220213
certificate = file.read()
221214

222215
cert = x509.load_der_x509_certificate(certificate)
223216

217+
_cert = cert.public_bytes(getattr(serialization.Encoding, "DER"))
218+
elif self.pem_cert_path:
219+
# Load the PEM certificate file
220+
with open(self.pem_cert_path, "rb") as file:
221+
certificate = file.read()
222+
223+
cert = x509.load_pem_x509_certificate(certificate)
224+
224225
_cert = cert.public_bytes(getattr(serialization.Encoding, "DER"))
225226
else:
226227
_cert = self.selfsigned_x509cert()

pymdoccbor/mso/verifier.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ def load_public_key(self) -> None:
117117
crv=settings.COSEKEY_HAZMAT_CRV_MAP[self.public_key.curve.name],
118118
x=self.public_key.public_numbers().x.to_bytes(
119119
settings.CRV_LEN_MAP[self.public_key.curve.name], 'big'
120-
)
120+
),
121+
y=self.public_key.public_numbers().y.to_bytes( settings.CRV_LEN_MAP[self.public_key.curve.name], 'big')
121122
)
122123
self.object.key = key
123124

pymdoccbor/tests/certs/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
### Procedure to create fake certificate fake-cert.pem
2+
```
3+
openssl ecparam -name prime256v1 -genkey -noout -out fake-private-key.pem
4+
openssl x509 -req -in fake-request.csr -out leaf-asl.pem -days 3650 -sha256
5+
openssl x509 -req -in fake-request.csr -key fake-private-key.pem -out fake-cert.pem -days 3650 -sha256
6+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[ req ]
2+
distinguished_name = req_distinguished_name
3+
attributes = req_attributes
4+
5+
# Stop confirmation prompts. All information is contained below.
6+
prompt= no
7+
8+
9+
# The extensions to add to a certificate request - see [ v3_req ]
10+
req_extensions = v3_req
11+
12+
[ req_distinguished_name ]
13+
# Describe the Subject (ie the origanisation).
14+
# The first 6 below could be shortened to: C ST L O OU CN
15+
# The short names are what are shown when the certificate is displayed.
16+
# Eg the details below would be shown as:
17+
# Subject: C=UK, ST=Hertfordshire, L=My Town, O=Some Organisation, OU=Some Department, CN=www.example.com/[email protected]
18+
19+
countryName= BE
20+
stateOrProvinceName= Brussels Region
21+
localityName= Brussels
22+
organizationName= Test
23+
organizationalUnitName= Test-Unit
24+
commonName= Test ASL Issuer
25+
emailAddress= [email protected]
26+
27+
[ req_attributes ]
28+
# None. Could put Challenge Passwords, don't want them, leave empty
29+
30+
[ v3_req ]
31+
# None.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICTzCCAfWgAwIBAgIUN+rPlhGdCIIWrQaKxFcdzJGyL0YwCgYIKoZIzj0EAwIw
3+
gZUxCzAJBgNVBAYTAkJFMRgwFgYDVQQIDA9CcnVzc2VscyBSZWdpb24xETAPBgNV
4+
BAcMCEJydXNzZWxzMQ0wCwYDVQQKDARUZXN0MRIwEAYDVQQLDAlUZXN0LVVuaXQx
5+
GDAWBgNVBAMMD1Rlc3QgQVNMIElzc3VlcjEcMBoGCSqGSIb3DQEJARYNZmFrZUBm
6+
YWtlLmNvbTAeFw0yNTAzMTcxMTA3MTZaFw0zNTAzMTUxMTA3MTZaMIGVMQswCQYD
7+
VQQGEwJCRTEYMBYGA1UECAwPQnJ1c3NlbHMgUmVnaW9uMREwDwYDVQQHDAhCcnVz
8+
c2VsczENMAsGA1UECgwEVGVzdDESMBAGA1UECwwJVGVzdC1Vbml0MRgwFgYDVQQD
9+
DA9UZXN0IEFTTCBJc3N1ZXIxHDAaBgkqhkiG9w0BCQEWDWZha2VAZmFrZS5jb20w
10+
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASgs+CiDRy2Fh1lPA6mtIb/c1fBBIA3
11+
Qz77kpnxsOid5/2bbUFYOI02djof6hsq7lWuCGwdWThDeiUQV1hISCPyoyEwHzAd
12+
BgNVHQ4EFgQU+jJ/exJHH3gawahlcnWTrlxbw3UwCgYIKoZIzj0EAwIDSAAwRQIg
13+
JJ3N2I7VyCFzN8CVktrs6IylXlDiSC+vsjt1POLnrHYCIQDKkU1XOfQiBGFzeLav
14+
vvqxhGIU/iOVlrLM3JOF9pGKCA==
15+
-----END CERTIFICATE-----
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MHcCAQEEIEWpyV6wCzKqJhcvRWg2olReRXLLcUwyL2IZzKNLiR6koAoGCCqGSM49
3+
AwEHoUQDQgAEoLPgog0cthYdZTwOprSG/3NXwQSAN0M++5KZ8bDonef9m21BWDiN
4+
NnY6H+obKu5VrghsHVk4Q3olEFdYSEgj8g==
5+
-----END EC PRIVATE KEY-----
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-----BEGIN CERTIFICATE REQUEST-----
2+
MIIBUDCB+AIBADCBlTELMAkGA1UEBhMCQkUxGDAWBgNVBAgMD0JydXNzZWxzIFJl
3+
Z2lvbjERMA8GA1UEBwwIQnJ1c3NlbHMxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAsM
4+
CVRlc3QtVW5pdDEYMBYGA1UEAwwPVGVzdCBBU0wgSXNzdWVyMRwwGgYJKoZIhvcN
5+
AQkBFg1mYWtlQGZha2UuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoLPg
6+
og0cthYdZTwOprSG/3NXwQSAN0M++5KZ8bDonef9m21BWDiNNnY6H+obKu5Vrghs
7+
HVk4Q3olEFdYSEgj8qAAMAoGCCqGSM49BAMCA0cAMEQCICtw2VqH3Jg03Ycme7UW
8+
0aQbBll8eQiBDPLCui+yekAMAiBfLqO9P7mgEWPMoSWfGYBiOVDEVUO8vERTZY1e
9+
HKpaRg==
10+
-----END CERTIFICATE REQUEST-----

pymdoccbor/tests/test_02_mdoc_issuer.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import cbor2
22
import os
3+
4+
from asn1crypto.x509 import Certificate
5+
from cryptography import x509
6+
from cryptography.hazmat.primitives import serialization
7+
from cryptography.x509 import load_der_x509_certificate
38
from pycose.messages import Sign1Message
49

510
from pymdoccbor.mdoc.issuer import MdocCborIssuer
@@ -17,15 +22,20 @@
1722
}
1823

1924

25+
def extract_mso(mdoc:dict):
26+
mso_data = mdoc["documents"][0]["issuerSigned"]["issuerAuth"][2]
27+
mso_cbortag = cbor2.loads(mso_data)
28+
mso = cbor2.loads(mso_cbortag.value)
29+
return mso
30+
31+
2032
def test_mso_writer():
33+
validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" }
2134
msoi = MsoIssuer(
2235
data=PID_DATA,
2336
private_key=PKEY,
24-
validity={
25-
"issuance_date": "2024-12-31",
26-
"expiry_date": "2050-12-31"
27-
},
28-
alg="ES256"
37+
validity=validity,
38+
alg = "ES256"
2939
)
3040

3141
assert "eu.europa.ec.eudiw.pid.1" in msoi.hash_map
@@ -44,26 +54,43 @@ def test_mso_writer():
4454

4555

4656
def test_mdoc_issuer():
57+
validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" }
4758
mdoci = MdocCborIssuer(
4859
private_key=PKEY,
49-
alg="ES256",
50-
)
51-
52-
mdoc = mdoci.new(
53-
doctype="eu.europa.ec.eudiw.pid.1",
54-
data=PID_DATA,
55-
#devicekeyinfo=PKEY, TODO
56-
validity={
57-
"issuance_date": "2024-12-31",
58-
"expiry_date": "2050-12-31"
59-
},
60+
alg = "ES256"
6061
)
62+
with open("/Users/olivier/PycharmProjects/pymdoccbor_oli/pymdoccbor/tests/certs/fake-cert.pem", "rb") as file:
63+
fake_cert_file = file.read()
64+
asl_signing_cert = x509.load_pem_x509_certificate(fake_cert_file)
65+
_asl_signing_cert = asl_signing_cert.public_bytes(getattr(serialization.Encoding, "DER"))
66+
status_list = {
67+
"status_list": {
68+
"idx": 0,
69+
"uri": "https://issuer.com/statuslists",
70+
"certificate": _asl_signing_cert,
71+
}
72+
}
73+
mdoc = mdoci.new(
74+
doctype="eu.europa.ec.eudiw.pid.1",
75+
data=PID_DATA,
76+
devicekeyinfo=PKEY,
77+
validity=validity,
78+
status_list=status_list
79+
)
6180

6281
mdocp = MdocCbor()
6382
aa = cbor2.dumps(mdoc)
6483
mdocp.loads(aa)
65-
mdocp.verify()
84+
assert mdocp.verify() is True
6685

6786
mdoci.dump()
6887
mdoci.dumps()
69-
88+
89+
# check mso content for status list
90+
mso = extract_mso(mdoc)
91+
status_list = mso["status"]["status_list"]
92+
assert status_list["idx"] == 0
93+
assert status_list["uri"] == "https://issuer.com/statuslists"
94+
cert_bytes = status_list["certificate"]
95+
cert:Certificate = load_der_x509_certificate(cert_bytes)
96+
assert "Test ASL Issuer" in cert.subject.rfc4514_string(), "ASL is not signed with the expected certificate"

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ isort
66
autoflake
77
bandit
88
autopep8
9+
pycose~=1.0.1

0 commit comments

Comments
 (0)