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
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 26 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,8 @@ def new(
validity: dict = None,
devicekeyinfo: Union[dict, CoseKey, str] = None,
cert_path: str = None,
revocation: dict = None,
pem_cert_path: str = None,
status_list: dict = {},
):
"""
create a new mdoc with signed mso
Expand All @@ -82,15 +85,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 status_list: dict: The status list to include in the mso of the mdoc

: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 @@ -130,6 +139,7 @@ def new(
msoi = MsoIssuer(
data=data,
cert_path=cert_path,
pem_cert_path=pem_cert_path,
hsm=self.hsm,
key_label=self.key_label,
user_pin=self.user_pin,
Expand All @@ -138,7 +148,7 @@ def new(
alg=self.alg,
kid=self.kid,
validity=validity,
revocation=revocation,
status_list=status_list
)

else:
Expand All @@ -147,11 +157,12 @@ def new(
private_key=self.private_key,
alg=self.alg,
cert_path=cert_path,
pem_cert_path=pem_cert_path,
validity=validity,
revocation=revocation,
status_list=status_list
)

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

mso_cbor = mso.encode(
tag=False,
Expand All @@ -162,18 +173,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
31 changes: 16 additions & 15 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,8 +33,8 @@ def __init__(
self,
data: dict,
validity: dict,
revocation: str = None,
cert_path: str = None,
pem_cert_path: str = None,
key_label: str = None,
user_pin: str = None,
lib_path: str = None,
Expand All @@ -51,13 +44,13 @@ def __init__(
hsm: bool = False,
private_key: Union[dict, CoseKey] = None,
digest_alg: str = settings.PYMDOC_HASHALG,
status_list: dict = {}
) -> 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 +61,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 status_list: dict: the status list to include in the mso
"""

if not hsm:
Expand All @@ -82,16 +76,17 @@ 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")

self.data: dict = data
self.hash_map: dict = {}
self.cert_path = cert_path
self.pem_cert_path = pem_cert_path
self.disclosure_map: dict = {}
self.digest_alg: str = digest_alg
self.key_label = key_label
Expand All @@ -102,7 +97,7 @@ def __init__(
self.alg = alg
self.kid = kid
self.validity = validity
self.revocation = revocation
self.status_list = status_list

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

Expand Down Expand Up @@ -209,18 +204,24 @@ def sign(
"deviceKey": device_key,
},
"digestAlgorithm": alg_map.get(self.alg),
"status": self.status_list
}

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

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

cert = x509.load_der_x509_certificate(certificate)

_cert = cert.public_bytes(getattr(serialization.Encoding, "DER"))
elif self.pem_cert_path:
# Load the PEM certificate file
with open(self.pem_cert_path, "rb") as file:
certificate = file.read()

cert = x509.load_pem_x509_certificate(certificate)

_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-----
63 changes: 45 additions & 18 deletions pymdoccbor/tests/test_02_mdoc_issuer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
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
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,
status_list=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"
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ isort
autoflake
bandit
autopep8
pycose~=1.0.1