From 80ca6603af51aab234f6fed714b4b13c6584e0a4 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Tue, 15 Jul 2025 14:39:11 +0000 Subject: [PATCH 1/8] add unsecure credential storage option Signed-off-by: PatStLouis --- acapy_agent/vc/routes.py | 10 +++++--- acapy_agent/vc/vc_ld/models/options.py | 30 ++++++++++++++++++++++ acapy_agent/vc/vc_ld/models/web_schemas.py | 3 ++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/acapy_agent/vc/routes.py b/acapy_agent/vc/routes.py index 9977d90d5c..e769049c30 100644 --- a/acapy_agent/vc/routes.py +++ b/acapy_agent/vc/routes.py @@ -154,14 +154,16 @@ async def store_credential_route(request: web.BaseRequest): manager = VcLdpManager(context.profile) try: - vc = body["verifiableCredential"] - cred_id = vc["id"] if "id" in vc else f"urn:uuid:{str(uuid4())}" - options = {} if "options" not in body else body["options"] + vc = body.get("verifiableCredential") + options = body.get("options", {}) + cred_id = vc.get("id", options.get("credentialId", str(uuid4()))) vc = VerifiableCredential.deserialize(vc) options = LDProofVCOptions.deserialize(options) - await manager.verify_credential(vc) + if options.get("verify", True): + await manager.verify_credential(vc) + await manager.store_credential(vc, cred_id) return web.json_response({"credentialId": cred_id}, status=200) diff --git a/acapy_agent/vc/vc_ld/models/options.py b/acapy_agent/vc/vc_ld/models/options.py index a939df56ac..3217a85d46 100644 --- a/acapy_agent/vc/vc_ld/models/options.py +++ b/acapy_agent/vc/vc_ld/models/options.py @@ -165,3 +165,33 @@ class Meta: ) }, ) + + +class CredentialStoreOptionsSchema(Schema): + """Verifiable Credential store options schema.""" + + class Meta: + """Accept parameter overload.""" + + unknown = INCLUDE + + credentialId = fields.Str( + required=False, + metadata={ + "description": ("Credential ID to use in storage & api calls."), + "example": "257d68ae-2b9c-406c-bc00-8e205d1abd44", + }, + ) + + verify = fields.Bool( + required=False, + metadata={ + "description": ( + "Store a Verifiable Credential without verifying any of the proofs." + "This is an unsecured option to be used for development " + "and experimentation of new unsupported cryptosuites." + ), + "example": True, + }, + default=True, + ) diff --git a/acapy_agent/vc/vc_ld/models/web_schemas.py b/acapy_agent/vc/vc_ld/models/web_schemas.py index 277c04e872..10131b7474 100644 --- a/acapy_agent/vc/vc_ld/models/web_schemas.py +++ b/acapy_agent/vc/vc_ld/models/web_schemas.py @@ -5,7 +5,7 @@ from ....messaging.models.openapi import OpenAPISchema from ..validation_result import PresentationVerificationResultSchema from .credential import CredentialSchema, VerifiableCredentialSchema -from .options import LDProofVCOptionsSchema +from .options import LDProofVCOptionsSchema, CredentialStoreOptionsSchema from .presentation import PresentationSchema, VerifiablePresentationSchema @@ -51,6 +51,7 @@ class StoreCredentialRequest(OpenAPISchema): """Request schema for verifying an LDP VP.""" verifiableCredential = fields.Nested(VerifiableCredentialSchema) + options = fields.Nested(CredentialStoreOptionsSchema) class StoreCredentialResponse(OpenAPISchema): From 2cd9cf69e3aed38785b24a84fc55ebc6feb7bbad Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Wed, 16 Jul 2025 15:41:07 +0000 Subject: [PATCH 2/8] update store route logic error Signed-off-by: PatStLouis --- acapy_agent/holder/routes.py | 1 - acapy_agent/vc/routes.py | 3 +- acapy_agent/vc/vc_ld/models/options.py | 5 --- acapy_agent/vc/vc_ld/models/web_schemas.py | 5 ++- acapy_agent/vc/vc_ld/tests/test_routes.py | 50 ++++++++++++++++++++++ 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 acapy_agent/vc/vc_ld/tests/test_routes.py diff --git a/acapy_agent/holder/routes.py b/acapy_agent/holder/routes.py index 8dfb0f3785..c49b46d911 100644 --- a/acapy_agent/holder/routes.py +++ b/acapy_agent/holder/routes.py @@ -118,7 +118,6 @@ class W3CCredentialsListRequestSchema(OpenAPISchema): ) types = fields.List( fields.Str( - validate=ENDPOINT_VALIDATE, metadata={ "description": "Credential type to match", "example": ENDPOINT_EXAMPLE, diff --git a/acapy_agent/vc/routes.py b/acapy_agent/vc/routes.py index e769049c30..554a32936f 100644 --- a/acapy_agent/vc/routes.py +++ b/acapy_agent/vc/routes.py @@ -156,10 +156,9 @@ async def store_credential_route(request: web.BaseRequest): try: vc = body.get("verifiableCredential") options = body.get("options", {}) - cred_id = vc.get("id", options.get("credentialId", str(uuid4()))) + cred_id = options.get("credentialId", vc.get("id", str(uuid4()))) vc = VerifiableCredential.deserialize(vc) - options = LDProofVCOptions.deserialize(options) if options.get("verify", True): await manager.verify_credential(vc) diff --git a/acapy_agent/vc/vc_ld/models/options.py b/acapy_agent/vc/vc_ld/models/options.py index 3217a85d46..23eb88fad3 100644 --- a/acapy_agent/vc/vc_ld/models/options.py +++ b/acapy_agent/vc/vc_ld/models/options.py @@ -170,11 +170,6 @@ class Meta: class CredentialStoreOptionsSchema(Schema): """Verifiable Credential store options schema.""" - class Meta: - """Accept parameter overload.""" - - unknown = INCLUDE - credentialId = fields.Str( required=False, metadata={ diff --git a/acapy_agent/vc/vc_ld/models/web_schemas.py b/acapy_agent/vc/vc_ld/models/web_schemas.py index 10131b7474..e24d18a3fa 100644 --- a/acapy_agent/vc/vc_ld/models/web_schemas.py +++ b/acapy_agent/vc/vc_ld/models/web_schemas.py @@ -51,7 +51,10 @@ class StoreCredentialRequest(OpenAPISchema): """Request schema for verifying an LDP VP.""" verifiableCredential = fields.Nested(VerifiableCredentialSchema) - options = fields.Nested(CredentialStoreOptionsSchema) + options = fields.Nested( + CredentialStoreOptionsSchema, + required=False + ) class StoreCredentialResponse(OpenAPISchema): diff --git a/acapy_agent/vc/vc_ld/tests/test_routes.py b/acapy_agent/vc/vc_ld/tests/test_routes.py new file mode 100644 index 0000000000..4070184df7 --- /dev/null +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -0,0 +1,50 @@ +import json +import uuid +from unittest import IsolatedAsyncioTestCase + +import copy +import pytest +from aiohttp import web + +from ...routes import store_credential_route + +VALID_VC = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": [ + "VerifiableCredential" + ], + "issuer": "did:key:z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je", + "credentialSubject": { + "name": "Alice" + }, + "proof": { + "type": "Ed25519Signature2020", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:key:z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je#z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je", + "created": "2025-07-16T15:20:23+00:00", + "proofValue": "z2uey5H4Bz9NHQezA6i2NNpvyrDNspHaFei3hcNTCqjAJi3ocs4DzzTbnXRGs5a6LMp9uNo7RyqtBcBmrstyAg1ML" + } +} +INVALID_VC = copy.deepcopy(VALID_VC) +INVALID_VC['proof']['proofValue'] = "unsecured" + +@pytest.mark.vc +class TestVcApiRoutes(IsolatedAsyncioTestCase): + + async def test_credentials_store(self): + response = store_credential_route(VALID_VC) + assert response + + async def test_credentials_store_unsecured(self): + options = { + 'credentialId': str(uuid.uuid4()), + 'verify': False + } + response = store_credential_route(INVALID_VC, options) + assert response + + with self.assertRaises(web.HTTPBadRequest): + response = store_credential_route(INVALID_VC) \ No newline at end of file From df60c0c0db5a1d4e49c75a6435e7e8c1d9ec863a Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Wed, 16 Jul 2025 15:43:22 +0000 Subject: [PATCH 3/8] lint Signed-off-by: PatStLouis --- acapy_agent/vc/vc_ld/models/web_schemas.py | 5 +-- acapy_agent/vc/vc_ld/tests/test_routes.py | 37 +++++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/acapy_agent/vc/vc_ld/models/web_schemas.py b/acapy_agent/vc/vc_ld/models/web_schemas.py index e24d18a3fa..169b3cb958 100644 --- a/acapy_agent/vc/vc_ld/models/web_schemas.py +++ b/acapy_agent/vc/vc_ld/models/web_schemas.py @@ -51,10 +51,7 @@ class StoreCredentialRequest(OpenAPISchema): """Request schema for verifying an LDP VP.""" verifiableCredential = fields.Nested(VerifiableCredentialSchema) - options = fields.Nested( - CredentialStoreOptionsSchema, - required=False - ) + options = fields.Nested(CredentialStoreOptionsSchema, required=False) class StoreCredentialResponse(OpenAPISchema): diff --git a/acapy_agent/vc/vc_ld/tests/test_routes.py b/acapy_agent/vc/vc_ld/tests/test_routes.py index 4070184df7..49480cbe99 100644 --- a/acapy_agent/vc/vc_ld/tests/test_routes.py +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -10,41 +10,34 @@ VALID_VC = { "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "type": [ - "VerifiableCredential" + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1", ], + "type": ["VerifiableCredential"], "issuer": "did:key:z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je", - "credentialSubject": { - "name": "Alice" - }, + "credentialSubject": {"name": "Alice"}, "proof": { - "type": "Ed25519Signature2020", - "proofPurpose": "assertionMethod", - "verificationMethod": "did:key:z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je#z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je", - "created": "2025-07-16T15:20:23+00:00", - "proofValue": "z2uey5H4Bz9NHQezA6i2NNpvyrDNspHaFei3hcNTCqjAJi3ocs4DzzTbnXRGs5a6LMp9uNo7RyqtBcBmrstyAg1ML" - } + "type": "Ed25519Signature2020", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:key:z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je#z6MksJQETYp2tT6PQhs1pmhqH8c77C8Ki6s23pWYPtC5Z2je", + "created": "2025-07-16T15:20:23+00:00", + "proofValue": "z2uey5H4Bz9NHQezA6i2NNpvyrDNspHaFei3hcNTCqjAJi3ocs4DzzTbnXRGs5a6LMp9uNo7RyqtBcBmrstyAg1ML", + }, } INVALID_VC = copy.deepcopy(VALID_VC) -INVALID_VC['proof']['proofValue'] = "unsecured" +INVALID_VC["proof"]["proofValue"] = "unsecured" + @pytest.mark.vc class TestVcApiRoutes(IsolatedAsyncioTestCase): - async def test_credentials_store(self): response = store_credential_route(VALID_VC) assert response - + async def test_credentials_store_unsecured(self): - options = { - 'credentialId': str(uuid.uuid4()), - 'verify': False - } + options = {"credentialId": str(uuid.uuid4()), "verify": False} response = store_credential_route(INVALID_VC, options) assert response with self.assertRaises(web.HTTPBadRequest): - response = store_credential_route(INVALID_VC) \ No newline at end of file + response = store_credential_route(INVALID_VC) From bfd1c7d9ff43827fa4ed368fd9934aaa6db7ec3a Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Wed, 16 Jul 2025 15:53:25 +0000 Subject: [PATCH 4/8] update tests Signed-off-by: PatStLouis --- acapy_agent/vc/vc_ld/tests/test_routes.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/acapy_agent/vc/vc_ld/tests/test_routes.py b/acapy_agent/vc/vc_ld/tests/test_routes.py index 49480cbe99..d2949aa6c6 100644 --- a/acapy_agent/vc/vc_ld/tests/test_routes.py +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -1,4 +1,3 @@ -import json import uuid from unittest import IsolatedAsyncioTestCase @@ -31,13 +30,24 @@ @pytest.mark.vc class TestVcApiRoutes(IsolatedAsyncioTestCase): async def test_credentials_store(self): - response = store_credential_route(VALID_VC) + self.request.match_info = { + "verifiableCredential": VALID_VC, + "options": {"credentialId": str(uuid.uuid4())}, + } + response = store_credential_route(self.request) assert response async def test_credentials_store_unsecured(self): - options = {"credentialId": str(uuid.uuid4()), "verify": False} - response = store_credential_route(INVALID_VC, options) + self.request.match_info = { + "verifiableCredential": INVALID_VC, + "options": {"credentialId": str(uuid.uuid4()), "verify": False}, + } + response = store_credential_route(self.request) assert response with self.assertRaises(web.HTTPBadRequest): + self.request.match_info = { + "verifiableCredential": INVALID_VC, + "options": {"credentialId": str(uuid.uuid4()), "verify": True}, + } response = store_credential_route(INVALID_VC) From 2ec8b42df7057e7274787da56fae2f98adec7744 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Wed, 16 Jul 2025 16:01:31 +0000 Subject: [PATCH 5/8] update test Signed-off-by: PatStLouis --- acapy_agent/vc/vc_ld/tests/test_routes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/acapy_agent/vc/vc_ld/tests/test_routes.py b/acapy_agent/vc/vc_ld/tests/test_routes.py index d2949aa6c6..a29513536e 100644 --- a/acapy_agent/vc/vc_ld/tests/test_routes.py +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -6,6 +6,8 @@ from aiohttp import web from ...routes import store_credential_route +from ....tests import mock +from ....utils.testing import create_test_profile VALID_VC = { "@context": [ @@ -29,6 +31,22 @@ @pytest.mark.vc class TestVcApiRoutes(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.profile = await create_test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = self.profile.context + setattr(self.context, "profile", self.profile) + self.request = mock.MagicMock( + app={}, + match_info={}, + query={}, + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, + ) + async def test_credentials_store(self): self.request.match_info = { "verifiableCredential": VALID_VC, From af30ca2e739e4844e467423dad71d95e4dc1fc0f Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Wed, 16 Jul 2025 16:20:13 +0000 Subject: [PATCH 6/8] assert verification results Signed-off-by: PatStLouis --- acapy_agent/vc/routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acapy_agent/vc/routes.py b/acapy_agent/vc/routes.py index 554a32936f..7a51bc8728 100644 --- a/acapy_agent/vc/routes.py +++ b/acapy_agent/vc/routes.py @@ -161,7 +161,9 @@ async def store_credential_route(request: web.BaseRequest): vc = VerifiableCredential.deserialize(vc) if options.get("verify", True): - await manager.verify_credential(vc) + verification = await manager.verify_credential(vc) + if not verification.verified: + return web.json_response({"verified": False}, status=400) await manager.store_credential(vc, cred_id) From fc7bfb17678999617eddb322f99683622b67498e Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 21 Jul 2025 16:10:38 +0000 Subject: [PATCH 7/8] remove negative test Signed-off-by: PatStLouis --- acapy_agent/vc/vc_ld/tests/test_routes.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/acapy_agent/vc/vc_ld/tests/test_routes.py b/acapy_agent/vc/vc_ld/tests/test_routes.py index a29513536e..6c505e03f7 100644 --- a/acapy_agent/vc/vc_ld/tests/test_routes.py +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -62,10 +62,3 @@ async def test_credentials_store_unsecured(self): } response = store_credential_route(self.request) assert response - - with self.assertRaises(web.HTTPBadRequest): - self.request.match_info = { - "verifiableCredential": INVALID_VC, - "options": {"credentialId": str(uuid.uuid4()), "verify": True}, - } - response = store_credential_route(INVALID_VC) From d3052b22353508e55c3a86f4f33d0f75cc248fb5 Mon Sep 17 00:00:00 2001 From: PatStLouis Date: Mon, 21 Jul 2025 16:48:22 +0000 Subject: [PATCH 8/8] remove unused import Signed-off-by: PatStLouis --- acapy_agent/vc/vc_ld/tests/test_routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/acapy_agent/vc/vc_ld/tests/test_routes.py b/acapy_agent/vc/vc_ld/tests/test_routes.py index 6c505e03f7..351e89e541 100644 --- a/acapy_agent/vc/vc_ld/tests/test_routes.py +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -3,7 +3,6 @@ import copy import pytest -from aiohttp import web from ...routes import store_credential_route from ....tests import mock