Skip to content

Fix mso.verify Add test + add status list variable to integrate status list ref in mso #12

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

Merged
merged 9 commits into from
Mar 20, 2025
35 changes: 23 additions & 12 deletions pymdoccbor/mdoc/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import binascii
import cbor2
import logging
import datetime
from cryptography.hazmat.primitives import serialization
from pycose.keys import CoseKey
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from pycose.keys import CoseKey, EC2Key
from typing import Union

from pymdoccbor.mso.issuer import MsoIssuer
Expand Down Expand Up @@ -72,7 +74,7 @@ def new(
validity: dict = None,
devicekeyinfo: Union[dict, CoseKey, str] = None,
cert_path: str = None,
revocation: dict = None,
revocation: dict = None
):
"""
create a new mdoc with signed mso
Expand All @@ -82,15 +84,21 @@ def new(
:param validity: dict: validity info
:param devicekeyinfo: Union[dict, CoseKey, str]: device key info
:param cert_path: str: path to the certificate
:param revocation: dict: revocation info
:param revocation: dict: revocation status dict it may include status_list and identifier_list keys

:return: dict: signed mdoc
"""
if isinstance(devicekeyinfo, dict):
devicekeyinfo = CoseKey.from_dict(devicekeyinfo)
devicekeyinfoCoseKeyObject = CoseKey.from_dict(devicekeyinfo)
devicekeyinfo = {
1: devicekeyinfoCoseKeyObject.kty.identifier,
-1: devicekeyinfoCoseKeyObject.crv.identifier,
-2: devicekeyinfoCoseKeyObject.x,
-3: devicekeyinfoCoseKeyObject.y,
}
if isinstance(devicekeyinfo, str):
device_key_bytes = base64.urlsafe_b64decode(devicekeyinfo.encode("utf-8"))
public_key = serialization.load_pem_public_key(device_key_bytes)
public_key:EllipticCurvePublicKey = serialization.load_pem_public_key(device_key_bytes)
curve_name = public_key.curve.name
curve_map = {
"secp256r1": 1, # NIST P-256
Expand Down Expand Up @@ -138,7 +146,7 @@ def new(
alg=self.alg,
kid=self.kid,
validity=validity,
revocation=revocation,
revocation=revocation
)

else:
Expand All @@ -148,10 +156,10 @@ def new(
alg=self.alg,
cert_path=cert_path,
validity=validity,
revocation=revocation,
revocation=revocation
)

mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo)
mso = msoi.sign(doctype=doctype, device_key=devicekeyinfo,valid_from=datetime.datetime.now(datetime.UTC))

mso_cbor = mso.encode(
tag=False,
Expand All @@ -162,18 +170,21 @@ def new(
slot_id=self.slot_id,
)


res = {
"version": self.version,
"documents": [{
"documents": [
{
"docType": doctype, # 'org.iso.18013.5.1.mDL'
"issuerSigned": {
"nameSpaces": {
ns: [v for k, v in dgst.items()]
for ns, dgst in msoi.disclosure_map.items()
},
},
"issuerAuth": cbor2.decoder.loads(mso_cbor),
},
}],
},
}
],
"status": self.status,
}

Expand Down
40 changes: 23 additions & 17 deletions pymdoccbor/mso/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,12 @@

logger = logging.getLogger("pymdoccbor")

from pycose.headers import Algorithm
from pycose.keys import CoseKey

from datetime import timezone

from pycose.headers import Algorithm #, KID
from pycose.keys import CoseKey, EC2Key

from pycose.messages import Sign1Message

from typing import Union


from pymdoccbor.exceptions import MsoPrivateKeyRequired
from pymdoccbor import settings
from pymdoccbor.x509 import MsoX509Fabric
Expand All @@ -40,7 +33,6 @@ def __init__(
self,
data: dict,
validity: dict,
revocation: str = None,
cert_path: str = None,
key_label: str = None,
user_pin: str = None,
Expand All @@ -51,13 +43,13 @@ def __init__(
hsm: bool = False,
private_key: Union[dict, CoseKey] = None,
digest_alg: str = settings.PYMDOC_HASHALG,
revocation: dict = None
) -> None:
"""
Initialize a new MsoIssuer

:param data: dict: the data to sign
:param validity: validity: the validity info of the mso
:param revocation: str: the revocation status
:param cert_path: str: the path to the certificate
:param key_label: str: key label
:param user_pin: str: user pin
Expand All @@ -68,6 +60,7 @@ def __init__(
:param hsm: bool: hardware security module
:param private_key: Union[dict, CoseKey]: the signing key
:param digest_alg: str: the digest algorithm
:param revocation: dict: revocation status dict to include in the mso, it may include status_list and identifier_list keys
"""

if not hsm:
Expand All @@ -82,10 +75,10 @@ def __init__(
raise ValueError("private_key must be a dict or CoseKey object")
else:
raise MsoPrivateKeyRequired("MSO Writer requires a valid private key")

if not validity:
raise ValueError("validity must be present")

if not alg:
raise ValueError("alg must be present")

Expand Down Expand Up @@ -208,19 +201,32 @@ def sign(
"deviceKeyInfo": {
"deviceKey": device_key,
},
"digestAlgorithm": alg_map.get(self.alg),
"digestAlgorithm": alg_map.get(self.alg)
}

if self.revocation is not None:
payload.update({"status": self.revocation})

if self.cert_path:
# Load the DER certificate file
# Try to load the certificate file
with open(self.cert_path, "rb") as file:
certificate = file.read()

cert = x509.load_der_x509_certificate(certificate)

_parsed_cert: Union[Certificate, None] = None
try:
_parsed_cert = x509.load_pem_x509_certificate(certificate)
except Exception as e:
logger.error(f"Certificate at {self.cert_path} could not be loaded as PEM, trying DER")

if not _parsed_cert:
try:
_parsed_cert = x509.load_der_x509_certificate(certificate)
except Exception as e:
_err_msg = f"Certificate at {self.cert_path} could not be loaded as DER"
logger.error(_err_msg)

if _parsed_cert:
cert = _parsed_cert
else:
raise Exception(f"Certificate at {self.cert_path} failed parse")
_cert = cert.public_bytes(getattr(serialization.Encoding, "DER"))
else:
_cert = self.selfsigned_x509cert()
Expand Down
3 changes: 2 additions & 1 deletion pymdoccbor/mso/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ def load_public_key(self) -> None:
crv=settings.COSEKEY_HAZMAT_CRV_MAP[self.public_key.curve.name],
x=self.public_key.public_numbers().x.to_bytes(
settings.CRV_LEN_MAP[self.public_key.curve.name], 'big'
)
),
y=self.public_key.public_numbers().y.to_bytes( settings.CRV_LEN_MAP[self.public_key.curve.name], 'big')
)
self.object.key = key

Expand Down
6 changes: 6 additions & 0 deletions pymdoccbor/tests/certs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Procedure to create fake certificate fake-cert.pem
```
openssl ecparam -name prime256v1 -genkey -noout -out fake-private-key.pem
openssl x509 -req -in fake-request.csr -out leaf-asl.pem -days 3650 -sha256
openssl x509 -req -in fake-request.csr -key fake-private-key.pem -out fake-cert.pem -days 3650 -sha256
```
31 changes: 31 additions & 0 deletions pymdoccbor/tests/certs/fake-cert.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[ req ]
distinguished_name = req_distinguished_name
attributes = req_attributes

# Stop confirmation prompts. All information is contained below.
prompt= no


# The extensions to add to a certificate request - see [ v3_req ]
req_extensions = v3_req

[ req_distinguished_name ]
# Describe the Subject (ie the origanisation).
# The first 6 below could be shortened to: C ST L O OU CN
# The short names are what are shown when the certificate is displayed.
# Eg the details below would be shown as:
# Subject: C=UK, ST=Hertfordshire, L=My Town, O=Some Organisation, OU=Some Department, CN=www.example.com/emailAddress=bofh@example.com

countryName= BE
stateOrProvinceName= Brussels Region
localityName= Brussels
organizationName= Test
organizationalUnitName= Test-Unit
commonName= Test ASL Issuer
emailAddress= fake@fake.com

[ req_attributes ]
# None. Could put Challenge Passwords, don't want them, leave empty

[ v3_req ]
# None.
15 changes: 15 additions & 0 deletions pymdoccbor/tests/certs/fake-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICTzCCAfWgAwIBAgIUN+rPlhGdCIIWrQaKxFcdzJGyL0YwCgYIKoZIzj0EAwIw
gZUxCzAJBgNVBAYTAkJFMRgwFgYDVQQIDA9CcnVzc2VscyBSZWdpb24xETAPBgNV
BAcMCEJydXNzZWxzMQ0wCwYDVQQKDARUZXN0MRIwEAYDVQQLDAlUZXN0LVVuaXQx
GDAWBgNVBAMMD1Rlc3QgQVNMIElzc3VlcjEcMBoGCSqGSIb3DQEJARYNZmFrZUBm
YWtlLmNvbTAeFw0yNTAzMTcxMTA3MTZaFw0zNTAzMTUxMTA3MTZaMIGVMQswCQYD
VQQGEwJCRTEYMBYGA1UECAwPQnJ1c3NlbHMgUmVnaW9uMREwDwYDVQQHDAhCcnVz
c2VsczENMAsGA1UECgwEVGVzdDESMBAGA1UECwwJVGVzdC1Vbml0MRgwFgYDVQQD
DA9UZXN0IEFTTCBJc3N1ZXIxHDAaBgkqhkiG9w0BCQEWDWZha2VAZmFrZS5jb20w
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASgs+CiDRy2Fh1lPA6mtIb/c1fBBIA3
Qz77kpnxsOid5/2bbUFYOI02djof6hsq7lWuCGwdWThDeiUQV1hISCPyoyEwHzAd
BgNVHQ4EFgQU+jJ/exJHH3gawahlcnWTrlxbw3UwCgYIKoZIzj0EAwIDSAAwRQIg
JJ3N2I7VyCFzN8CVktrs6IylXlDiSC+vsjt1POLnrHYCIQDKkU1XOfQiBGFzeLav
vvqxhGIU/iOVlrLM3JOF9pGKCA==
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions pymdoccbor/tests/certs/fake-private-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEWpyV6wCzKqJhcvRWg2olReRXLLcUwyL2IZzKNLiR6koAoGCCqGSM49
AwEHoUQDQgAEoLPgog0cthYdZTwOprSG/3NXwQSAN0M++5KZ8bDonef9m21BWDiN
NnY6H+obKu5VrghsHVk4Q3olEFdYSEgj8g==
-----END EC PRIVATE KEY-----
10 changes: 10 additions & 0 deletions pymdoccbor/tests/certs/fake-request.csr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBUDCB+AIBADCBlTELMAkGA1UEBhMCQkUxGDAWBgNVBAgMD0JydXNzZWxzIFJl
Z2lvbjERMA8GA1UEBwwIQnJ1c3NlbHMxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAsM
CVRlc3QtVW5pdDEYMBYGA1UEAwwPVGVzdCBBU0wgSXNzdWVyMRwwGgYJKoZIhvcN
AQkBFg1mYWtlQGZha2UuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoLPg
og0cthYdZTwOprSG/3NXwQSAN0M++5KZ8bDonef9m21BWDiNNnY6H+obKu5Vrghs
HVk4Q3olEFdYSEgj8qAAMAoGCCqGSM49BAMCA0cAMEQCICtw2VqH3Jg03Ycme7UW
0aQbBll8eQiBDPLCui+yekAMAiBfLqO9P7mgEWPMoSWfGYBiOVDEVUO8vERTZY1e
HKpaRg==
-----END CERTIFICATE REQUEST-----
65 changes: 46 additions & 19 deletions pymdoccbor/tests/test_02_mdoc_issuer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import cbor2
import os

from asn1crypto.x509 import Certificate
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import load_der_x509_certificate
from pycose.messages import Sign1Message

from pymdoccbor.mdoc.issuer import MdocCborIssuer
from pymdoccbor.mdoc.verifier import MdocCbor
from pymdoccbor.mso.issuer import MsoIssuer
from . pid_data import PID_DATA
from pymdoccbor.tests.pid_data import PID_DATA


PKEY = {
Expand All @@ -17,15 +22,20 @@
}


def extract_mso(mdoc:dict):
mso_data = mdoc["documents"][0]["issuerSigned"]["issuerAuth"][2]
mso_cbortag = cbor2.loads(mso_data)
mso = cbor2.loads(mso_cbortag.value)
return mso


def test_mso_writer():
validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" }
msoi = MsoIssuer(
data=PID_DATA,
private_key=PKEY,
validity={
"issuance_date": "2024-12-31",
"expiry_date": "2050-12-31"
},
alg="ES256"
validity=validity,
alg = "ES256"
)

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


def test_mdoc_issuer():
validity = {"issuance_date": "2025-01-17", "expiry_date": "2025-11-13" }
mdoci = MdocCborIssuer(
private_key=PKEY,
alg="ES256",
)

mdoc = mdoci.new(
doctype="eu.europa.ec.eudiw.pid.1",
data=PID_DATA,
#devicekeyinfo=PKEY, TODO
validity={
"issuance_date": "2024-12-31",
"expiry_date": "2050-12-31"
},
alg = "ES256"
)
with open("pymdoccbor/tests/certs/fake-cert.pem", "rb") as file:
fake_cert_file = file.read()
asl_signing_cert = x509.load_pem_x509_certificate(fake_cert_file)
_asl_signing_cert = asl_signing_cert.public_bytes(getattr(serialization.Encoding, "DER"))
status_list = {
"status_list": {
"idx": 0,
"uri": "https://issuer.com/statuslists",
"certificate": _asl_signing_cert,
}
}
mdoc = mdoci.new(
doctype="eu.europa.ec.eudiw.pid.1",
data=PID_DATA,
devicekeyinfo=PKEY,
validity=validity,
revocation=status_list
)

mdocp = MdocCbor()
aa = cbor2.dumps(mdoc)
mdocp.loads(aa)
mdocp.verify()
assert mdocp.verify() is True

mdoci.dump()
mdoci.dumps()


# check mso content for status list
mso = extract_mso(mdoc)
status_list = mso["status"]["status_list"]
assert status_list["idx"] == 0
assert status_list["uri"] == "https://issuer.com/statuslists"
cert_bytes = status_list["certificate"]
cert:Certificate = load_der_x509_certificate(cert_bytes)
assert "Test ASL Issuer" in cert.subject.rfc4514_string(), "ASL is not signed with the expected certificate"