From 680f30dc6f0d080bf2f82c7616001a0f52c7a5b5 Mon Sep 17 00:00:00 2001 From: red Date: Thu, 4 Apr 2024 10:34:06 +0800 Subject: [PATCH 01/10] created module spp_encryption --- requirements.txt | 2 + spp_encryption/__init__.py | 1 + spp_encryption/__manifest__.py | 27 +++++++ spp_encryption/models/__init__.py | 1 + spp_encryption/models/encryption_provider.py | 85 ++++++++++++++++++++ spp_encryption/pyproject.toml | 3 + spp_encryption/views/encryption_provider.xml | 27 +++++++ 7 files changed, 146 insertions(+) create mode 100644 spp_encryption/__init__.py create mode 100644 spp_encryption/__manifest__.py create mode 100644 spp_encryption/models/__init__.py create mode 100644 spp_encryption/models/encryption_provider.py create mode 100644 spp_encryption/pyproject.toml create mode 100644 spp_encryption/views/encryption_provider.xml diff --git a/requirements.txt b/requirements.txt index 6714dc6a7..58cb42812 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,11 @@ bravado_core faker geojson jsonschema +jwcrypto pyjwt>=2.4.0 pyproj python-magic +qrcode shapely simplejson swagger_spec_validator diff --git a/spp_encryption/__init__.py b/spp_encryption/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/spp_encryption/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/spp_encryption/__manifest__.py b/spp_encryption/__manifest__.py new file mode 100644 index 000000000..c5c8c3c4e --- /dev/null +++ b/spp_encryption/__manifest__.py @@ -0,0 +1,27 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + +{ + "name": "SPP Encryption", + "category": "OpenSPP", + "version": "17.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/openspp-modules", + "license": "LGPL-3", + "development_status": "Beta", + "maintainers": ["jeremi", "gonzalesedwin1123"], + "depends": [ + "g2p_encryption", + ], + "external_dependencies": {"python": ["jwcrypto"]}, + "data": [ + "views/encryption_provider.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": False, +} diff --git a/spp_encryption/models/__init__.py b/spp_encryption/models/__init__.py new file mode 100644 index 000000000..56c044c0b --- /dev/null +++ b/spp_encryption/models/__init__.py @@ -0,0 +1 @@ +from . import encryption_provider diff --git a/spp_encryption/models/encryption_provider.py b/spp_encryption/models/encryption_provider.py new file mode 100644 index 000000000..1419b9a78 --- /dev/null +++ b/spp_encryption/models/encryption_provider.py @@ -0,0 +1,85 @@ +import json +import uuid + +from jwcrypto import jwe, jwk, jwt +from jwcrypto.common import json_decode, json_encode +from jwcrypto.jws import InvalidJWSSignature + +from odoo import fields, models + + +class JWCryptoEncryptionProvider(models.Model): + _inherit = "g2p.encryption.provider" + + type = fields.Selection(selection_add=[("jwcrypto", "JWCrypto")]) + + jwcrypto_key = fields.Char(help="JWK key in JSON format for encryption, decryption, signing, and verification") + + def _get_jwk_key(self): + self.ensure_one() + if not self.jwcrypto_key: + raise ValueError("JWCrypto key is not set.") + return jwk.JWK.from_json(self.jwcrypto_key) + + def encrypt_data_jwcrypto(self, data: bytes, **kwargs) -> bytes: + self.ensure_one() + key = self._get_jwk_key() + enc = jwe.JWE(data, json_encode({"alg": "RSA-OAEP", "enc": "A256GCM"})) + enc.add_recipient(key) + return enc.serialize(compact=True).encode("utf-8") + + def decrypt_data_jwcrypto(self, data: bytes, **kwargs) -> bytes: + self.ensure_one() + key = self._get_jwk_key() + enc = jwe.JWE() + enc.deserialize(data.decode("utf-8"), key=key) + return enc.payload + + def jwt_sign_jwcrypto(self, data, **kwargs) -> str: + self.ensure_one() + key = self._get_jwk_key() + token = jwt.JWT(header={"alg": "RS256"}, claims=data) + token.make_signed_token(key) + return token.serialize() + + def jwt_verify_jwcrypto(self, token: str, **kwargs): + self.ensure_one() + key = self._get_jwk_key() + try: + received_jwt = jwt.JWT(key=key, jwt=token) + verified = True + except InvalidJWSSignature: + received_jwt = None + verified = False + return verified, received_jwt + + def get_jwks_jwcrypto(self, **kwargs): + self.ensure_one() + key = self._get_jwk_key() + public_key = key.export_public() + jwks = {"keys": [json_decode(public_key)]} + return jwks + + def generate_and_store_jwcrypto_key(self, key_type="RSA", size=2048): + """ + Generates a new JWK (JSON Web Key) for the current record and stores it in the `jwcrypto_key` field. + :param key_type: The type of key to generate, e.g., 'RSA'. + :param size: The size of the key (applies to RSA keys). + :return: None + """ + if key_type != "RSA": + raise ValueError("Unsupported key type. Currently, only 'RSA' is supported.") + + key = jwk.JWK.generate(kty=key_type, size=size) + + kid = str(uuid.uuid4()) + + key_export = key.export() + + export_data = json.loads(key_export) + export_data["kid"] = kid + + key_export = json.dumps(export_data) + + # Assuming this method is called on a specific record, not on the model class itself + self.jwcrypto_key = key_export diff --git a/spp_encryption/pyproject.toml b/spp_encryption/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_encryption/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_encryption/views/encryption_provider.xml b/spp_encryption/views/encryption_provider.xml new file mode 100644 index 000000000..7d9e96e17 --- /dev/null +++ b/spp_encryption/views/encryption_provider.xml @@ -0,0 +1,27 @@ + + + + view_encryption_provider_form + g2p.encryption.provider + + + + + + + + + From 1f3e5f16667094def2eb06bb6141cc749118e686 Mon Sep 17 00:00:00 2001 From: red Date: Thu, 4 Apr 2024 10:35:14 +0800 Subject: [PATCH 04/10] created VCI Issuer module for Group Registry --- spp_openid_vci_group/__init__.py | 1 + spp_openid_vci_group/__manifest__.py | 28 ++++++ .../data/default_contexts.json | 91 +++++++++++++++++++ .../data/default_credential_format.jq | 21 +++++ .../data/default_issuer_metadata.jq | 89 ++++++++++++++++++ spp_openid_vci_group/models/__init__.py | 1 + spp_openid_vci_group/models/vci_issuer.py | 33 +++++++ spp_openid_vci_group/pyproject.toml | 3 + spp_openid_vci_group/views/group_view.xml | 20 ++++ 9 files changed, 287 insertions(+) create mode 100644 spp_openid_vci_group/__init__.py create mode 100644 spp_openid_vci_group/__manifest__.py create mode 100644 spp_openid_vci_group/data/default_contexts.json create mode 100644 spp_openid_vci_group/data/default_credential_format.jq create mode 100644 spp_openid_vci_group/data/default_issuer_metadata.jq create mode 100644 spp_openid_vci_group/models/__init__.py create mode 100644 spp_openid_vci_group/models/vci_issuer.py create mode 100644 spp_openid_vci_group/pyproject.toml create mode 100644 spp_openid_vci_group/views/group_view.xml diff --git a/spp_openid_vci_group/__init__.py b/spp_openid_vci_group/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/spp_openid_vci_group/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/spp_openid_vci_group/__manifest__.py b/spp_openid_vci_group/__manifest__.py new file mode 100644 index 000000000..1c3cbb5c0 --- /dev/null +++ b/spp_openid_vci_group/__manifest__.py @@ -0,0 +1,28 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + +{ + "name": "SPP Registry OpenID VCI: Group", + "category": "OpenSPP", + "version": "17.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/openspp-modules", + "license": "LGPL-3", + "development_status": "Beta", + "maintainers": ["jeremi", "gonzalesedwin1123"], + "depends": [ + "spp_openid_vci", + "g2p_registry_group", + ], + "external_dependencies": {"python": ["qrcode"]}, + "data": [ + "views/group_view.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": True, + "installable": True, + "auto_install": False, +} diff --git a/spp_openid_vci_group/data/default_contexts.json b/spp_openid_vci_group/data/default_contexts.json new file mode 100644 index 000000000..8505dcdc2 --- /dev/null +++ b/spp_openid_vci_group/data/default_contexts.json @@ -0,0 +1,91 @@ +{ + "@context": { + "OpenG2PRegistryVerifiableCredential": { + "@id": "https://openg2p.org/credential#OpenG2PRegistryVerifiableCredential", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "@vocab": "https://openg2p.org/credential#OpenG2PRegistryVerifiableCredential#", + "credentialSubject": { + "@id": "credentialSubject", + "@type": "@id", + "@context": { + "@vocab": "https://openg2p.org/credential#OpenG2PRegistryVerifiableCredential#credentialSubject#", + "name": { + "@id": "name", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "fullName": { + "@id": "fullName", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "gender": { + "@id": "gender", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "dateOfBirth": "dateOfBirth", + "email": "email", + "phone": "phone", + "addressLine1": { + "@id": "addressLine1", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "addressLine2": { + "@id": "addressLine2", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "addressLine3": { + "@id": "addressLine3", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "province": { + "@id": "province", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "region": { + "@id": "region", + "@type": "@id", + "@context": { + "value": "@value", + "language": "@language" + } + }, + "postalCode": "postalCode", + "face": "face", + "vcVer": "vcVer", + "UIN": "UIN" + } + } + } + ] + } + } +} diff --git a/spp_openid_vci_group/data/default_credential_format.jq b/spp_openid_vci_group/data/default_credential_format.jq new file mode 100644 index 000000000..49eb654ec --- /dev/null +++ b/spp_openid_vci_group/data/default_credential_format.jq @@ -0,0 +1,21 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + (.web_base_url + "/api/v1/vci/.well-known/contexts.json") + ], + "id": .vc_id, + "type": ["VerifiableCredential", .issuer.credential_type], + "issuer": .issuer.unique_issuer_id, + "issuanceDate": .curr_datetime, + "credentialSubject": { + "vcVer": "VC-V1", + "id": (.partner.id | tostring), + "name": (.partner.name // null), + "email": (.partner.email // null), + "phone": (.partner.phone // null), + "addressLine1": .partner_address.street_address, + "province": .partner_address.locality, + "region": .partner_address.region, + "postalCode": .partner_address.postal_code, + } +} diff --git a/spp_openid_vci_group/data/default_issuer_metadata.jq b/spp_openid_vci_group/data/default_issuer_metadata.jq new file mode 100644 index 000000000..80065dd55 --- /dev/null +++ b/spp_openid_vci_group/data/default_issuer_metadata.jq @@ -0,0 +1,89 @@ +[ + { + "id": .credential_type, + "format": .supported_format, + "scope": .scope, + "cryptographic_binding_methods_supported": [ + "did:jwk" + ], + "credential_signing_alg_values_supported": [ + "RS256" + ], + "proof_types_supported": [ + "jwt" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + .credential_type + ], + "credentialSubject": { + "fullName": { + "display": [ + { + "name": "Name", + "locale": "en" + } + ] + }, + "gender": { + "display": [ + { + "name": "Gender", + "locale": "en" + } + ] + }, + "dateOfBirth": { + "display": [ + { + "name": "Date of Birth", + "locale": "en" + } + ] + }, + "address": { + "display": [ + { + "name": "Address", + "locale": "en" + } + ] + }, + "UIN": { + "display": [ + { + "name": "Beneficiary ID", + "locale": "en" + } + ] + }, + "nationalID": { + "display": [ + { + "name": "National ID", + "locale": "en" + } + ] + } + } + }, + "display": [ + { + "name": "OpenG2P Registry Credential", + "locale": "en", + "logo": { + "url": (.web_base_url + "/g2p_openid_vci/static/description/icon.png"), + "alt_text": "a square logo of a OpenG2P" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "order": [ + "fullName", + "gender", + "dateOfBirth" + ] + } +] diff --git a/spp_openid_vci_group/models/__init__.py b/spp_openid_vci_group/models/__init__.py new file mode 100644 index 000000000..23d962efc --- /dev/null +++ b/spp_openid_vci_group/models/__init__.py @@ -0,0 +1 @@ +from . import vci_issuer diff --git a/spp_openid_vci_group/models/vci_issuer.py b/spp_openid_vci_group/models/vci_issuer.py new file mode 100644 index 000000000..8d0bb7da1 --- /dev/null +++ b/spp_openid_vci_group/models/vci_issuer.py @@ -0,0 +1,33 @@ +from odoo import fields, models + + +class CustomOpenIDVCIssuer(models.Model): + _inherit = "g2p.openid.vci.issuers" + + issuer_type = fields.Selection( + selection_add=[ + ( + "GroupRegistry", + "GroupRegistry", + ) + ], + ondelete={"GroupRegistry": "cascade"}, + ) + + def set_from_static_file_GroupRegistry( + self, module_name="spp_openid_vci_group", file_name="", field_name="", **kwargs + ): + return self.set_from_static_file_Registry( + module_name=module_name, file_name=file_name, field_name=field_name, **kwargs + ) + + def set_default_credential_type_GroupRegistry(self): + self.credential_type = "GroupRegistry" + + def issue_vc_GroupRegistry(self, auth_claims, credential_request): + return self.issue_vc_Registry(auth_claims, credential_request) + + def set_default_auth_allowed_issuers_GroupRegistry(self): + web_base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url").rstrip("/") + endpoint = "/api/v1/security" + self.auth_allowed_issuers = f"{web_base_url}{endpoint}" diff --git a/spp_openid_vci_group/pyproject.toml b/spp_openid_vci_group/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_openid_vci_group/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_openid_vci_group/views/group_view.xml b/spp_openid_vci_group/views/group_view.xml new file mode 100644 index 000000000..e709eea5d --- /dev/null +++ b/spp_openid_vci_group/views/group_view.xml @@ -0,0 +1,20 @@ + + + + view_group_form + res.partner + + + + + + + + From 5ab28ed7dee7596317685b0fef4e94395e7f9b74 Mon Sep 17 00:00:00 2001 From: red Date: Mon, 8 Apr 2024 18:03:24 +0800 Subject: [PATCH 05/10] fixed issue in signing and verifying the signed jwt --- spp_openid_vci/models/vci_issuer.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spp_openid_vci/models/vci_issuer.py b/spp_openid_vci/models/vci_issuer.py index f396a0a16..ec5ec6e82 100644 --- a/spp_openid_vci/models/vci_issuer.py +++ b/spp_openid_vci/models/vci_issuer.py @@ -11,3 +11,19 @@ def onchange_auth_allowed_issuers(self): args = [rec, f"set_default_auth_allowed_issuers_{rec.issuer_type}"] if hasattr(*args): getattr(*args)() + + def sign_and_issue_credential(self, credential: dict) -> dict: + self.ensure_one() + + ld_proof = self.build_empty_ld_proof() + + signature = self.get_encryption_provider().jwt_sign( + {"credential": credential, "proof": ld_proof}, + include_payload=False, + include_certificate=True, + include_cert_hash=True, + ) + ld_proof["jws"] = signature + ret = dict(credential) + ret["proof"] = ld_proof + return ret From 51690f857a53ca70287b2603c649aff540cd3e40 Mon Sep 17 00:00:00 2001 From: red Date: Wed, 17 Apr 2024 16:12:16 +0800 Subject: [PATCH 06/10] added openspp logo in id card --- spp_openid_vci/static/description/icon.png | Bin 0 -> 12567 bytes spp_openid_vci/views/id_card.xml | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 spp_openid_vci/static/description/icon.png diff --git a/spp_openid_vci/static/description/icon.png b/spp_openid_vci/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..35f8fec263588314689c920efc4b6728b407daaa GIT binary patch literal 12567 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®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 - + From 7a3efbcdb4e11b718f5770d9580ee04824ebb497 Mon Sep 17 00:00:00 2001 From: red Date: Wed, 17 Apr 2024 16:12:39 +0800 Subject: [PATCH 07/10] improved API retrieving by adding timeout --- spp_openid_vci/models/res_partner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spp_openid_vci/models/res_partner.py b/spp_openid_vci/models/res_partner.py index 2ec8cbfdc..4bd198d42 100644 --- a/spp_openid_vci/models/res_partner.py +++ b/spp_openid_vci/models/res_partner.py @@ -41,9 +41,10 @@ def _issue_vc(self, vci_issuer): web_base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url").rstrip("/") url = f"{web_base_url}/api/v1/vci/.well-known/openid-credential-issuer/{vci_issuer.name}" - - credential_issuer_response = requests.get(url) - + try: + credential_issuer_response = requests.get(url, timeout=5) + except requests.exceptions.Timeout as e: + raise UserError("The request to the credential issuer timed out.") from e issuer_data = credential_issuer_response.json() credential_issuer = f"{issuer_data['credential_issuer']}/api/v1/security" From 9bd27bd334bfd30ec63ad53b5fb3465449296aee Mon Sep 17 00:00:00 2001 From: red Date: Wed, 17 Apr 2024 16:12:57 +0800 Subject: [PATCH 08/10] added test cases --- spp_encryption/tests/__init__.py | 1 + .../tests/test_encryption_provider.py | 116 ++++++++++++ spp_openid_vci/tests/__init__.py | 1 + spp_openid_vci/tests/test_vci_issuer.py | 172 ++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 spp_encryption/tests/__init__.py create mode 100644 spp_encryption/tests/test_encryption_provider.py create mode 100644 spp_openid_vci/tests/__init__.py create mode 100644 spp_openid_vci/tests/test_vci_issuer.py diff --git a/spp_encryption/tests/__init__.py b/spp_encryption/tests/__init__.py new file mode 100644 index 000000000..c886abb6e --- /dev/null +++ b/spp_encryption/tests/__init__.py @@ -0,0 +1 @@ +from . import test_encryption_provider diff --git a/spp_encryption/tests/test_encryption_provider.py b/spp_encryption/tests/test_encryption_provider.py new file mode 100644 index 000000000..a3c9c2943 --- /dev/null +++ b/spp_encryption/tests/test_encryption_provider.py @@ -0,0 +1,116 @@ +from unittest.mock import patch + +from odoo.tests.common import TransactionCase + + +class EncryptionProviderTest(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.provide_no_type = cls.env["g2p.encryption.provider"].create({"name": "Test Provider"}) + cls.provider = cls.env["g2p.encryption.provider"].create( + {"name": "Test Provider with Type", "type": "jwcrypto"} + ) + cls.provider_with_key = cls.env["g2p.encryption.provider"].create( + {"name": "Test Provider with Key", "type": "jwcrypto"} + ) + cls.provider_with_key.generate_and_store_jwcrypto_key() + + def test_get_jwk_key(self): + self.assertEqual(self.provider.jwcrypto_key, False) + with self.assertRaises(ValueError): + self.provider._get_jwk_key() + + expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"] + self.assertTrue(all(elem in expected_keys for elem in self.provider_with_key._get_jwk_key().keys())) + + def test_encrypt_data_jwcrypto(self): + with self.assertRaises(NotImplementedError): + self.provide_no_type.encrypt_data(b"test") + + with patch.object(type(self.provider), "encrypt_data_jwcrypto", return_value=None) as encrypt_data_jwcrypto: + result = self.provider.encrypt_data(b"test") + encrypt_data_jwcrypto.assert_called_once() + + result_with_key = self.provider_with_key.encrypt_data(b"test") + + self.assertIsNone(result) + self.assertIsNotNone(result_with_key) + self.assertIsInstance(result_with_key, bytes) + + def test_decrypt_data_jwcrypto(self): + with self.assertRaises(NotImplementedError): + self.provide_no_type.decrypt_data(b"test") + + with patch.object(type(self.provider), "decrypt_data_jwcrypto", return_value=None) as decrypt_data_jwcrypto: + self.provider.decrypt_data(b"test") + decrypt_data_jwcrypto.assert_called_once() + + data = "test" + encoded_data = data.encode("utf-8") + + encrypted_data = self.provider_with_key.encrypt_data(encoded_data) + decrypted_data = self.provider_with_key.decrypt_data(encrypted_data) + + self.assertEqual(decrypted_data, encoded_data) + + def test_jwt_sign_jwcrypto(self): + with self.assertRaises(NotImplementedError): + self.provide_no_type.jwt_sign({}) + + with patch.object(type(self.provider), "jwt_sign_jwcrypto", return_value=None) as jwt_sign_jwcrypto: + self.provider.jwt_sign({}) + jwt_sign_jwcrypto.assert_called_once() + + data = {"test": "test"} + result = self.provider_with_key.jwt_sign(data) + + self.assertIsNotNone(result) + self.assertIsInstance(result, str) + + def test_jwt_verify_jwcrypto(self): + with self.assertRaises(NotImplementedError): + self.provide_no_type.jwt_verify("test") + + with patch.object(type(self.provider), "jwt_verify_jwcrypto", return_value=None) as jwt_verify_jwcrypto: + self.provider.jwt_verify("test") + jwt_verify_jwcrypto.assert_called_once() + + data = {"test": "test"} + token = self.provider_with_key.jwt_sign(data) + verified, received_jwt = self.provider_with_key.jwt_verify(token) + + self.assertTrue(verified) + self.assertIsNotNone(received_jwt) + + def test_get_jwks_jwcrypto(self): + with self.assertRaises(NotImplementedError): + self.provide_no_type.get_jwks() + + with patch.object(type(self.provider), "get_jwks_jwcrypto", return_value=None) as get_jwks_jwcrypto: + self.provider.get_jwks() + get_jwks_jwcrypto.assert_called_once() + + jwks = self.provider_with_key.get_jwks() + + self.assertIsNotNone(jwks) + self.assertTrue("keys" in jwks) + self.assertIsInstance(jwks["keys"], list) + self.assertEqual(len(jwks["keys"]), 1) + self.assertIsInstance(jwks["keys"][0], dict) + + expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"] + self.assertTrue(all(elem in expected_keys for elem in jwks["keys"][0].keys())) + + def test_generate_and_store_jwcrypto_key(self): + with self.assertRaisesRegex(ValueError, "Unsupported key type. Currently, only 'RSA' is supported."): + self.provider.generate_and_store_jwcrypto_key(key_type="DSA") + + self.assertIn(self.provider.jwcrypto_key, [False, None]) + + self.provider.generate_and_store_jwcrypto_key() + + expected_keys = ["kty", "kid", "n", "e", "d", "p", "q", "dp", "dq", "qi"] + + self.assertIsNotNone(self.provider.jwcrypto_key) + self.assertTrue(all(elem in expected_keys for elem in self.provider_with_key._get_jwk_key().keys())) diff --git a/spp_openid_vci/tests/__init__.py b/spp_openid_vci/tests/__init__.py new file mode 100644 index 000000000..1f8df8e65 --- /dev/null +++ b/spp_openid_vci/tests/__init__.py @@ -0,0 +1 @@ +from . import test_vci_issuer diff --git a/spp_openid_vci/tests/test_vci_issuer.py b/spp_openid_vci/tests/test_vci_issuer.py new file mode 100644 index 000000000..aa40c25c8 --- /dev/null +++ b/spp_openid_vci/tests/test_vci_issuer.py @@ -0,0 +1,172 @@ +from unittest.mock import Mock, patch + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase + +from ..models.res_partner import SPPRegistry as ResPartner + + +class VCIIssuer(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.res_partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.vci_issuer = cls.env["g2p.openid.vci.issuers"].create( + { + "name": "TestIssuer", + "issuer_type": "Registry", + "scope": "openid", + } + ) + + cls.id_type = cls.env["g2p.id.type"].create({"name": "Test ID Type"}) + cls.encryption_provider_id = cls.env["g2p.encryption.provider"].create( + { + "name": "Test Provider", + "type": "jwcrypto", + } + ) + cls.encryption_provider_id.generate_and_store_jwcrypto_key() + + cls.vci_issuer_complete = cls.env["g2p.openid.vci.issuers"].create( + { + "name": "TestIssuerComplete", + "issuer_type": "Registry", + "scope": "openid", + "auth_sub_id_type_id": cls.id_type.id, + "encryption_provider_id": cls.encryption_provider_id.id, + } + ) + cls.res_partner_complete = cls.env["res.partner"].create({"name": "Test Partner Complete"}) + cls.env["g2p.reg.id"].create( + { + "partner_id": cls.res_partner_complete.id, + "id_type": cls.id_type.id, + "value": "Test Value", + } + ) + + cls.test_issue_vc_json = { + "credential_configurations_supported": None, + "credential_endpoint": "http://localhost:8080/api/v1/vci/credential", + "credential_issuer": "http://localhost:8080", + "credentials_supported": [ + { + "credential_definition": { + "credentialSubject": { + "UIN": {"display": [{"locale": "en", "name": "Beneficiary " "ID"}]}, + "address": {"display": [{"locale": "en", "name": "Address"}]}, + "dateOfBirth": {"display": [{"locale": "en", "name": "Date " "of " "Birth"}]}, + "fullName": {"display": [{"locale": "en", "name": "Name"}]}, + "gender": {"display": [{"locale": "en", "name": "Gender"}]}, + "nationalID": {"display": [{"locale": "en", "name": "National " "ID"}]}, + }, + "type": ["VerifiableCredential", "Registry"], + }, + "credential_signing_alg_values_supported": ["RS256"], + "cryptographic_binding_methods_supported": ["did:jwk"], + "display": [ + { + "background_color": "#12107c", + "locale": "en", + "logo": { + "alt_text": "a square logo " "of a OpenG2P", + "url": "http://localhost:8080/g2p_openid_vci/static/description/icon.png", + }, + "name": "OpenG2P Registry Credential", + "text_color": "#FFFFFF", + } + ], + "format": "ldp_vc", + "id": "Registry", + "order": ["fullName", "gender", "dateOfBirth"], + "proof_types_supported": ["jwt"], + "scope": "IndividualRegistry", + } + ], + } + + def test_validate_vci_issuer(self): + with self.assertRaisesRegex(UserError, "No issuer found."): + self.res_partner._validate_vci_issuer(None) + + with self.assertRaisesRegex(UserError, "No auth sub id type found in the issuer."): + self.res_partner._validate_vci_issuer(self.vci_issuer) + + self.vci_issuer.auth_sub_id_type_id = self.id_type + + with self.assertRaisesRegex(UserError, "No encryption provider found in the issuer."): + self.res_partner._validate_vci_issuer(self.vci_issuer) + + self.vci_issuer.encryption_provider_id = self.encryption_provider_id + + self.res_partner._validate_vci_issuer(self.vci_issuer) + + @patch("requests.get") + def test_issue_vc(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = self.test_issue_vc_json + mock_get.return_value = mock_response + + with self.assertRaisesRegex( + UserError, f"No Registrant found with this ID Type: {self.vci_issuer.auth_sub_id_type_id.name}." + ): + self.res_partner._issue_vc(self.vci_issuer) + + with patch.object(type(self.vci_issuer), "issue_vc", return_value=None) as issue_vc: + self.res_partner_complete._issue_vc(self.vci_issuer_complete) + issue_vc.assert_called_once() + + def test_create_qr_code(self): + qr_img = self.res_partner_complete._create_qr_code("Test Data") + self.assertIsNotNone(qr_img) + self.assertIsInstance(qr_img, bytes) + + def test_registry_issue_card(self): + registry_issue_card_action = self.res_partner_complete.registry_issue_card() + + form_id = self.env.ref("spp_openid_vci.issue_card_wizard").id + + self.assertIsNotNone(registry_issue_card_action) + self.assertIsInstance(registry_issue_card_action, dict) + self.assertEqual(registry_issue_card_action.get("type"), "ir.actions.act_window") + self.assertEqual(registry_issue_card_action.get("res_model"), "spp.issue.card.wizard") + self.assertEqual(registry_issue_card_action.get("name"), "Issue Card") + self.assertEqual(registry_issue_card_action.get("view_id"), form_id) + self.assertEqual(registry_issue_card_action["context"]["default_partner_id"], self.res_partner_complete.id) + + @patch.object(ResPartner, "_validate_vci_issuer") + @patch.object(ResPartner, "_issue_vc") + def test_issue_vc_qr(self, mock_vci_issuer, mock_issue_vc): + mock_vci_issuer.return_value = True + mock_issue_vc.return_value = {} + + vc_qr = self.res_partner_complete._issue_vc_qr(self.vci_issuer_complete) + + self.assertIsNotNone(vc_qr) + self.assertIsInstance(vc_qr, dict) + self.assertEqual(vc_qr.get("type"), "ir.actions.report") + self.assertEqual(vc_qr.get("report_name"), "spp_openid_vci.id_vc_card") + self.assertEqual(vc_qr.get("report_type"), "qweb-pdf") + self.assertEqual(vc_qr.get("report_file"), "spp_openid_vci.id_vc_card") + self.assertEqual(vc_qr.get("name"), "ID Card") + self.assertEqual(vc_qr.get("context").get("active_ids")[0], self.res_partner_complete.id) + + def test_sign_and_issue_credential(self): + credential_data = { + "test_key": "test_value", + } + credential = self.vci_issuer_complete.sign_and_issue_credential(credential_data) + + self.assertIsNotNone(credential) + self.assertIsInstance(credential, dict) + self.assertIn("test_key", credential) + self.assertIn("proof", credential) + self.assertIn("jws", credential["proof"]) + self.assertIn("type", credential["proof"]) + self.assertIn("verificationMethod", credential["proof"]) + self.assertIn("proofPurpose", credential["proof"]) + self.assertIn("created", credential["proof"]) + self.assertIn("@context", credential["proof"]) From 04eb7087248f4144c189a7736f747c04dd69b77d Mon Sep 17 00:00:00 2001 From: red Date: Thu, 18 Apr 2024 16:25:33 +0800 Subject: [PATCH 09/10] fixed test cases --- .../data/default_credential_format.jq | 44 ++++++++++++++++--- .../models/vci_issuer.py | 3 -- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/spp_openid_vci_individual/data/default_credential_format.jq b/spp_openid_vci_individual/data/default_credential_format.jq index 6e6cb1a3c..0252a479c 100644 --- a/spp_openid_vci_individual/data/default_credential_format.jq +++ b/spp_openid_vci_individual/data/default_credential_format.jq @@ -9,16 +9,46 @@ "issuanceDate": .curr_datetime, "credentialSubject": { "vcVer": "VC-V1", - "id": (.partner.id | tostring), - "name": (.partner.name // null), - "fullName": (.partner.name // null), - "gender": (.partner.gender // null), + "id": (.web_base_url + "/api/v1/registry/individual/" + (.partner.id | tostring)), + "name": [ + { + "language": "eng", + "value": (.partner.name // null) + } + ], + "fullName": [ + { + "language": "eng", + "value": (.partner.name // null) + } + ], + "gender": (if .partner.gender then [ + { + "language": "eng", + "value": .partner.gender + } + ] else null end), "dateOfBirth": (.partner.birthdate // null), "email": (.partner.email // null), "phone": (.partner.phone // null), - "addressLine1": .partner_address.street_address, - "province": .partner_address.locality, - "region": .partner_address.region, + "addressLine1": (if .partner_address.street_address then [ + { + "language": "eng", + "value": .partner_address.street_address + } + ] else null end), + "province": (if .partner_address.locality then [ + { + "language": "eng", + "value": .partner_address.locality + } + ] else null end), + "region": (if .partner_address.region then [ + { + "language": "eng", + "value": .partner_address.region + } + ] else null end), "postalCode": .partner_address.postal_code, "face": .partner_face, "UIN": .reg_ids["NATIONAL ID"]?.value diff --git a/spp_openid_vci_individual/models/vci_issuer.py b/spp_openid_vci_individual/models/vci_issuer.py index 8a4a94ef9..d6bfa1822 100644 --- a/spp_openid_vci_individual/models/vci_issuer.py +++ b/spp_openid_vci_individual/models/vci_issuer.py @@ -15,6 +15,3 @@ def set_default_auth_allowed_issuers_Registry(self): web_base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url").rstrip("/") endpoint = "/api/v1/security" self.auth_allowed_issuers = f"{web_base_url}{endpoint}" - - def set_default_credential_type_Registry(self): - self.credential_type = "Registry" From 09da2fd4798337187b0ca5edf49907d1bccd3499 Mon Sep 17 00:00:00 2001 From: red Date: Wed, 24 Apr 2024 18:29:40 +0800 Subject: [PATCH 10/10] modifed data of vci group --- .../data/default_contexts.json | 50 ++----------------- .../data/default_credential_format.jq | 27 +++++++--- .../data/default_issuer_metadata.jq | 34 +------------ 3 files changed, 27 insertions(+), 84 deletions(-) diff --git a/spp_openid_vci_group/data/default_contexts.json b/spp_openid_vci_group/data/default_contexts.json index 8505dcdc2..c1becc087 100644 --- a/spp_openid_vci_group/data/default_contexts.json +++ b/spp_openid_vci_group/data/default_contexts.json @@ -15,73 +15,33 @@ "@id": "name", "@type": "@id", "@context": { - "value": "@value", - "language": "@language" + "value": "@value" } }, - "fullName": { - "@id": "fullName", - "@type": "@id", - "@context": { - "value": "@value", - "language": "@language" - } - }, - "gender": { - "@id": "gender", - "@type": "@id", - "@context": { - "value": "@value", - "language": "@language" - } - }, - "dateOfBirth": "dateOfBirth", - "email": "email", "phone": "phone", "addressLine1": { "@id": "addressLine1", "@type": "@id", "@context": { - "value": "@value", - "language": "@language" - } - }, - "addressLine2": { - "@id": "addressLine2", - "@type": "@id", - "@context": { - "value": "@value", - "language": "@language" - } - }, - "addressLine3": { - "@id": "addressLine3", - "@type": "@id", - "@context": { - "value": "@value", - "language": "@language" + "value": "@value" } }, "province": { "@id": "province", "@type": "@id", "@context": { - "value": "@value", - "language": "@language" + "value": "@value" } }, "region": { "@id": "region", "@type": "@id", "@context": { - "value": "@value", - "language": "@language" + "value": "@value" } }, "postalCode": "postalCode", - "face": "face", - "vcVer": "vcVer", - "UIN": "UIN" + "vcVer": "vcVer" } } } diff --git a/spp_openid_vci_group/data/default_credential_format.jq b/spp_openid_vci_group/data/default_credential_format.jq index 49eb654ec..12adc76fc 100644 --- a/spp_openid_vci_group/data/default_credential_format.jq +++ b/spp_openid_vci_group/data/default_credential_format.jq @@ -9,13 +9,28 @@ "issuanceDate": .curr_datetime, "credentialSubject": { "vcVer": "VC-V1", - "id": (.partner.id | tostring), - "name": (.partner.name // null), - "email": (.partner.email // null), + "id": (.web_base_url + "/api/v1/registry/individual/" + (.partner.id | tostring)), + "name": [ + { + "value": (.partner.name // null) + } + ], "phone": (.partner.phone // null), - "addressLine1": .partner_address.street_address, - "province": .partner_address.locality, - "region": .partner_address.region, + "addressLine1": (if .partner_address.street_address then [ + { + "value": .partner_address.street_address + } + ] else null end), + "province": (if .partner_address.locality then [ + { + "value": .partner_address.locality + } + ] else null end), + "region": (if .partner_address.region then [ + { + "value": .partner_address.region + } + ] else null end), "postalCode": .partner_address.postal_code, } } diff --git a/spp_openid_vci_group/data/default_issuer_metadata.jq b/spp_openid_vci_group/data/default_issuer_metadata.jq index 80065dd55..c9b886587 100644 --- a/spp_openid_vci_group/data/default_issuer_metadata.jq +++ b/spp_openid_vci_group/data/default_issuer_metadata.jq @@ -18,7 +18,7 @@ .credential_type ], "credentialSubject": { - "fullName": { + "name": { "display": [ { "name": "Name", @@ -26,22 +26,6 @@ } ] }, - "gender": { - "display": [ - { - "name": "Gender", - "locale": "en" - } - ] - }, - "dateOfBirth": { - "display": [ - { - "name": "Date of Birth", - "locale": "en" - } - ] - }, "address": { "display": [ { @@ -50,22 +34,6 @@ } ] }, - "UIN": { - "display": [ - { - "name": "Beneficiary ID", - "locale": "en" - } - ] - }, - "nationalID": { - "display": [ - { - "name": "National ID", - "locale": "en" - } - ] - } } }, "display": [

ID Card

OpenSPP