Skip to content
1 change: 0 additions & 1 deletion acapy_agent/holder/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ class W3CCredentialsListRequestSchema(OpenAPISchema):
)
types = fields.List(
fields.Str(
validate=ENDPOINT_VALIDATE,
metadata={
"description": "Credential type to match",
"example": ENDPOINT_EXAMPLE,
Expand Down
13 changes: 8 additions & 5 deletions acapy_agent/vc/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions acapy_agent/vc/vc_ld/models/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
3 changes: 2 additions & 1 deletion acapy_agent/vc/vc_ld/models/web_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
63 changes: 63 additions & 0 deletions acapy_agent/vc/vc_ld/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -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
Loading