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 9977d90d5c..7a51bc8728 100644 --- a/acapy_agent/vc/routes.py +++ b/acapy_agent/vc/routes.py @@ -154,14 +154,17 @@ 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 = options.get("credentialId", vc.get("id", str(uuid4()))) vc = VerifiableCredential.deserialize(vc) - options = LDProofVCOptions.deserialize(options) - await manager.verify_credential(vc) + if options.get("verify", True): + 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) 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..23eb88fad3 100644 --- a/acapy_agent/vc/vc_ld/models/options.py +++ b/acapy_agent/vc/vc_ld/models/options.py @@ -165,3 +165,28 @@ class Meta: ) }, ) + + +class CredentialStoreOptionsSchema(Schema): + """Verifiable Credential store options schema.""" + + 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..169b3cb958 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, 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..351e89e541 --- /dev/null +++ b/acapy_agent/vc/vc_ld/tests/test_routes.py @@ -0,0 +1,63 @@ +import uuid +from unittest import IsolatedAsyncioTestCase + +import copy +import pytest + +from ...routes import store_credential_route +from ....tests import mock +from ....utils.testing import create_test_profile + +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 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, + "options": {"credentialId": str(uuid.uuid4())}, + } + response = store_credential_route(self.request) + assert response + + async def test_credentials_store_unsecured(self): + self.request.match_info = { + "verifiableCredential": INVALID_VC, + "options": {"credentialId": str(uuid.uuid4()), "verify": False}, + } + response = store_credential_route(self.request) + assert response