Skip to content

Commit 6416235

Browse files
authored
Merge pull request #12 from oligatorr/identity_python_branch
Fix mso.verify Add test + add status list variable to integrate status list ref in mso
2 parents 085f8c3 + 5a9a26d commit 6416235

File tree

9 files changed

+162
-49
lines changed

9 files changed

+162
-49
lines changed

pymdoccbor/mdoc/issuer.py

+23-12
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,7 @@ def new(
7274
validity: dict = None,
7375
devicekeyinfo: Union[dict, CoseKey, str] = None,
7476
cert_path: str = None,
75-
revocation: dict = None,
77+
revocation: dict = None
7678
):
7779
"""
7880
create a new mdoc with signed mso
@@ -82,15 +84,21 @@ def new(
8284
:param validity: dict: validity info
8385
:param devicekeyinfo: Union[dict, CoseKey, str]: device key info
8486
:param cert_path: str: path to the certificate
85-
:param revocation: dict: revocation info
87+
:param revocation: dict: revocation status dict it may include status_list and identifier_list keys
8688
8789
:return: dict: signed mdoc
8890
"""
8991
if isinstance(devicekeyinfo, dict):
90-
devicekeyinfo = CoseKey.from_dict(devicekeyinfo)
92+
devicekeyinfoCoseKeyObject = CoseKey.from_dict(devicekeyinfo)
93+
devicekeyinfo = {
94+
1: devicekeyinfoCoseKeyObject.kty.identifier,
95+
-1: devicekeyinfoCoseKeyObject.crv.identifier,
96+
-2: devicekeyinfoCoseKeyObject.x,
97+
-3: devicekeyinfoCoseKeyObject.y,
98+
}
9199
if isinstance(devicekeyinfo, str):
92100
device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8"))
93-
public_key = serialization.load_pem_public_key(device_key_bytes)
101+
public_key:EllipticCurvePublicKey = serialization.load_pem_public_key(device_key_bytes)
94102
curve_name = public_key.curve.name
95103
curve_map = {
96104
"secp256r1": 1, # NIST P-256
@@ -138,7 +146,7 @@ def new(
138146
alg=self.alg,
139147
kid=self.kid,
140148
validity=validity,
141-
revocation=revocation,
149+
revocation=revocation
142150
)
143151

144152
else:
@@ -148,10 +156,10 @@ def new(
148156
alg=self.alg,
149157
cert_path=cert_path,
150158
validity=validity,
151-
revocation=revocation,
159+
revocation=revocation
152160
)
153161

154-
mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo)
162+
mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime.UTC))
155163

156164
mso_cbor = mso.encode(
157165
tag=False,
@@ -162,18 +170,21 @@ def new(
162170
slot_id=self.slot_id,
163171
)
164172

173+
165174
res = {
166175
"version": self.version,
167-
"documents": [{
176+
"documents": [
177+
{
168178
"docType": doctype, # 'org.iso.18013.5.1.mDL'
169179
"issuerSigned": {
170180
"nameSpaces": {
171181
ns: [v for k, v in dgst.items()]
172182
for ns, dgst in msoi.disclosure_map.items()
173-
},
183+
},
174184
"issuerAuth": cbor2.decoder.loads(mso_cbor),
175-
},
176-
}],
185+
},
186+
}
187+
],
177188
"status": self.status,
178189
}
179190

pymdoccbor/mso/issuer.py

+24-17
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,19 @@
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
2619
from pymdoccbor.tools import shuffle_dict
2720
from cryptography import x509
2821
from cryptography.hazmat.primitives import serialization
22+
from cryptography.x509 import Certificate
2923

3024

3125
from cbor_diag import *
@@ -40,7 +34,6 @@ def __init__(
4034
self,
4135
data: dict,
4236
validity: dict,
43-
revocation: str = None,
4437
cert_path: str = None,
4538
key_label: str = None,
4639
user_pin: 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+
revocation: dict = None
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 revocation: dict: revocation status dict to include in the mso, it may include status_list and identifier_list keys
7165
"""
7266

7367
if not hsm:
@@ -82,10 +76,10 @@ 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

@@ -208,19 +202,32 @@ def sign(
208202
"deviceKeyInfo": {
209203
"deviceKey": device_key,
210204
},
211-
"digestAlgorithm": alg_map.get(self.alg),
205+
"digestAlgorithm": alg_map.get(self.alg)
212206
}
213-
214207
if self.revocation is not None:
215208
payload.update({"status": self.revocation})
216209

217210
if self.cert_path:
218-
# Load the DER certificate file
211+
# Try to load the certificate file
219212
with open(self.cert_path, "rb") as file:
220213
certificate = file.read()
221-
222-
cert = x509.load_der_x509_certificate(certificate)
223-
214+
_parsed_cert: Union[Certificate, None] = None
215+
try:
216+
_parsed_cert = x509.load_pem_x509_certificate(certificate)
217+
except Exception as e:
218+
logger.error(f"Certificate at {self.cert_path} could not be loaded as PEM, trying DER")
219+
220+
if not _parsed_cert:
221+
try:
222+
_parsed_cert = x509.load_der_x509_certificate(certificate)
223+
except Exception as e:
224+
_err_msg = f"Certificate at {self.cert_path} could not be loaded as DER"
225+
logger.error(_err_msg)
226+
227+
if _parsed_cert:
228+
cert = _parsed_cert
229+
else:
230+
raise Exception(f"Certificate at {self.cert_path} failed parse")
224231
_cert = cert.public_bytes(getattr(serialization.Encoding, "DER"))
225232
else:
226233
_cert = self.selfsigned_x509cert()

pymdoccbor/mso/verifier.py

+2-1
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

+6
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+
```

pymdoccbor/tests/certs/fake-cert.cnf

+31
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.

pymdoccbor/tests/certs/fake-cert.pem

+15
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-----
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-----
+10
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-----
+46-19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
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
611
from pymdoccbor.mdoc.verifier import MdocCbor
712
from pymdoccbor.mso.issuer import MsoIssuer
8-
from . pid_data import PID_DATA
13+
from pymdoccbor.tests.pid_data import PID_DATA
914

1015

1116
PKEY = {
@@ -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("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+
revocation=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"

0 commit comments

Comments
 (0)