Skip to content

Commit 8fadca2

Browse files
feat(core): support configurable proof format for anoncreds (#904)
* feat(core): support configurable proof format for anoncreds Signed-off-by: Yuki I <[email protected]> * test(core): isolate tests to prevent pollution and improve logging Signed-off-by: Yuki I <[email protected]> * refactor(core): add config validation and improve proof format fallback Signed-off-by: Yuki I <[email protected]> * fix(build): align proof format config with project standards Signed-off-by: Yuki I <[email protected]> * style(core): apply black code formatting Signed-off-by: Yuki I <[email protected]> --------- Signed-off-by: Yuki I <[email protected]> Co-authored-by: Yuki I <[email protected]>
1 parent 4906a76 commit 8fadca2

File tree

10 files changed

+228
-43
lines changed

10 files changed

+228
-43
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ In order to use the VC OIDC authentication, a couple of extra steps are required
5454
so, the following command can be used to post a configuration requesting a BC Wallet Showcase Person credential:
5555
- Though not implemented in this built-in config, proof-request configurations can optionally include substitution variables. Details can be found [here](docs/ConfigurationGuide.md#proof-substitution-variables)
5656

57+
**Note:** The following demo commands are for an **Indy-based** credential ecosystem. The application defaults to the `indy` proof format, so these examples work out-of-the-box. You can switch to `anoncreds` by setting the `ACAPY_PROOF_FORMAT` environment variable.
58+
5759
```bash
5860
curl -X 'POST' \
5961
'http://localhost:5000/ver_configs/' \

docker/docker-compose.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ services:
4444
- CONTROLLER_PRESENTATION_RECORD_RETENTION_HOURS=${CONTROLLER_PRESENTATION_RECORD_RETENTION_HOURS}
4545
- CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE=${CONTROLLER_VARIABLE_SUBSTITUTION_OVERRIDE}
4646
- CONTROLLER_TEMPLATE_DIR=${CONTROLLER_TEMPLATE_DIR}
47+
- ACAPY_PROOF_FORMAT=${ACAPY_PROOF_FORMAT}
4748
- ACAPY_TENANCY=${AGENT_TENANT_MODE}
4849
- ACAPY_AGENT_URL=${AGENT_ENDPOINT}
4950
- ACAPY_ADMIN_URL=${AGENT_ADMIN_URL}
@@ -211,4 +212,4 @@ volumes:
211212
controller-db-data:
212213
keycloak-db-data:
213214
agent-wallet-db:
214-
redis-data:
215+
redis-data:

docker/manage

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ configureEnvironment() {
207207
#controller app settings
208208
export INVITATION_LABEL=${INVITATION_LABEL:-"VC-AuthN"}
209209
export SET_NON_REVOKED="True"
210+
export ACAPY_PROOF_FORMAT=${ACAPY_PROOF_FORMAT:-indy}
210211
export USE_OOB_LOCAL_DID_SERVICE=${USE_OOB_LOCAL_DID_SERVICE:-"true"}
211212
export USE_CONNECTION_BASED_VERIFICATION=${USE_CONNECTION_BASED_VERIFICATION:-"true"}
212213
export WALLET_DEEP_LINK_PREFIX=${WALLET_DEEP_LINK_PREFIX:-"bcwallet://aries_proof-request"}

docs/ConfigurationGuide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Several functions in ACAPy VC-AuthN can be tweaked by using the following enviro
7676

7777
| Variable | Type | What it does | NOTES |
7878
| ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
79+
| ACAPY_PROOF_FORMAT | string ("indy" or "anoncreds") | Sets the proof format for ACA-Py presentation requests. Use `anoncreds` for modern AnonCreds support or `indy` for legacy Indy-based ecosystems. | Defaults to `indy` for backward compatibility with existing deployments. |
7980
| SET_NON_REVOKED | bool | if True, the `non_revoked` attributed will be added to each of the present-proof request `requested_attribute` and `requested_predicate` with 'from=0' and'to=`int(time.time())` | |
8081
| USE_OOB_LOCAL_DID_SERVICE | bool | Instructs ACAPy VC-AuthN to use a local DID, it must be used when the agent service is not registered on the ledger with a public DID | Use this when `ACAPY_WALLET_LOCAL_DID` is set to `true` in the agent. |
8182
| WALLET_DEEP_LINK_PREFIX | string | Custom URI scheme and host to use for deep links (e.g. `{WALLET_DEEP_LINK_PREFIX}?c_i={connection invitation payload`) | Default bcwallet://aries_proof-request |

oidc-controller/api/core/acapy/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ def create_presentation_request(
5050
self, presentation_request_configuration: dict
5151
) -> CreatePresentationResponse:
5252
logger.debug(">>> create_presentation_request")
53+
54+
format_key = settings.ACAPY_PROOF_FORMAT
5355
present_proof_payload = {
54-
"presentation_request": {"indy": presentation_request_configuration}
56+
"presentation_request": {format_key: presentation_request_configuration}
5557
}
5658

5759
resp_raw = requests.post(
@@ -215,9 +217,10 @@ def send_presentation_request_by_connection(
215217
"""
216218
logger.debug(">>> send_presentation_request_by_connection")
217219

220+
format_key = settings.ACAPY_PROOF_FORMAT
218221
present_proof_payload = {
219222
"connection_id": connection_id,
220-
"presentation_request": {"indy": presentation_request_configuration},
223+
"presentation_request": {format_key: presentation_request_configuration},
221224
}
222225

223226
resp_raw = requests.post(

oidc-controller/api/core/acapy/tests/test_client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,65 @@ async def test_send_problem_report_sends_correct_payload(requests_mock):
587587
# Verify the request was made with correct parameters
588588
assert requests_mock.last_request.json() == {"description": description}
589589
assert requests_mock.call_count == 1
590+
591+
592+
@pytest.mark.asyncio
593+
async def test_create_presentation_request_uses_configured_proof_format(requests_mock):
594+
# Verify payload key respects ACAPY_PROOF_FORMAT
595+
requests_mock.post(
596+
settings.ACAPY_ADMIN_URL + CREATE_PRESENTATION_REQUEST_URL,
597+
headers={},
598+
json=json.dumps(create_presentation_response_http),
599+
status_code=200,
600+
)
601+
602+
# Patch the setting to 'anoncreds'
603+
with mock.patch.object(settings, "ACAPY_PROOF_FORMAT", "anoncreds"):
604+
with mock.patch.object(
605+
CreatePresentationResponse,
606+
"model_validate",
607+
return_value={"result": "success"},
608+
):
609+
client = AcapyClient()
610+
client.agent_config.get_headers = mock.MagicMock(
611+
return_value={"x-api-key": ""}
612+
)
613+
614+
client.create_presentation_request(presentation_request_configuration)
615+
616+
# Inspect the actual JSON body sent to ACA-Py
617+
request_json = requests_mock.last_request.json()
618+
assert "anoncreds" in request_json["presentation_request"]
619+
assert "indy" not in request_json["presentation_request"]
620+
621+
622+
@pytest.mark.asyncio
623+
async def test_send_presentation_request_by_connection_uses_configured_proof_format(
624+
requests_mock,
625+
):
626+
# Verify connection-based request also respects ACAPY_PROOF_FORMAT
627+
requests_mock.post(
628+
settings.ACAPY_ADMIN_URL + SEND_PRESENTATION_REQUEST_URL,
629+
headers={},
630+
json=json.dumps(create_presentation_response_http),
631+
status_code=200,
632+
)
633+
634+
with mock.patch.object(settings, "ACAPY_PROOF_FORMAT", "anoncreds"):
635+
with mock.patch.object(
636+
CreatePresentationResponse,
637+
"model_validate",
638+
return_value={"result": "success"},
639+
):
640+
client = AcapyClient()
641+
client.agent_config.get_headers = mock.MagicMock(
642+
return_value={"x-api-key": ""}
643+
)
644+
645+
client.send_presentation_request_by_connection(
646+
"conn_id", presentation_request_configuration
647+
)
648+
649+
request_json = requests_mock.last_request.json()
650+
assert "anoncreds" in request_json["presentation_request"]
651+
assert "indy" not in request_json["presentation_request"]

oidc-controller/api/core/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ class GlobalConfig(BaseSettings):
204204

205205
ACAPY_ADMIN_URL: str = os.environ.get("ACAPY_ADMIN_URL", "http://localhost:8031")
206206

207+
ACAPY_PROOF_FORMAT: str = os.environ.get("ACAPY_PROOF_FORMAT", "indy")
208+
207209
MT_ACAPY_WALLET_ID: str | None = os.environ.get("MT_ACAPY_WALLET_ID")
208210
MT_ACAPY_WALLET_KEY: str = os.environ.get("MT_ACAPY_WALLET_KEY", "random-key")
209211

@@ -301,3 +303,9 @@ def get_configuration() -> GlobalConfig:
301303

302304

303305
settings = get_configuration()
306+
307+
# Add startup validation for ACAPY_PROOF_FORMAT
308+
if settings.ACAPY_PROOF_FORMAT not in ["indy", "anoncreds"]:
309+
raise ValueError(
310+
f"ACAPY_PROOF_FORMAT must be 'indy' or 'anoncreds', got '{settings.ACAPY_PROOF_FORMAT}'"
311+
)

oidc-controller/api/core/oidc/issue_token_service.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ...authSessions.models import AuthSession
1313
from ...verificationConfigs.models import ReqAttr, VerificationConfig
1414
from ...core.models import RevealedAttribute
15+
from ..config import settings
1516

1617
logger: structlog.typing.FilteringBoundLogger = structlog.getLogger(__name__)
1718

@@ -50,27 +51,44 @@ def get_claims(
5051
)
5152

5253
presentation_claims: dict[str, Claim] = {}
54+
55+
# Make fallback logic more robust by checking both pres_request and pres.
56+
# This handles in-flight sessions during configuration changes and guards against malformed data.
57+
pres_request = auth_session.presentation_exchange.get("pres_request", {})
58+
pres = auth_session.presentation_exchange.get("pres", {})
59+
proof_format = settings.ACAPY_PROOF_FORMAT
60+
61+
if proof_format not in pres_request or proof_format not in pres:
62+
if "indy" in pres_request and "indy" in pres:
63+
logger.debug(
64+
f"Configured proof format '{proof_format}' not found in record, falling back to 'indy'"
65+
)
66+
proof_format = "indy"
67+
elif "anoncreds" in pres_request and "anoncreds" in pres:
68+
logger.debug(
69+
f"Configured proof format '{proof_format}' not found in record, falling back to 'anoncreds'"
70+
)
71+
proof_format = "anoncreds"
72+
5373
logger.info(
54-
"pres_request_token"
55-
+ str(
56-
auth_session.presentation_exchange["pres_request"]["indy"][
57-
"requested_attributes"
58-
]
59-
)
74+
"Extracted requested attributes from presentation request",
75+
requested_attributes=auth_session.presentation_exchange["pres_request"][
76+
proof_format
77+
]["requested_attributes"],
6078
)
6179

6280
referent: str
6381
requested_attr: ReqAttr
6482
try:
6583
for referent, requested_attrdict in auth_session.presentation_exchange[
6684
"pres_request"
67-
]["indy"]["requested_attributes"].items():
85+
][proof_format]["requested_attributes"].items():
6886
requested_attr = ReqAttr(**requested_attrdict)
6987
logger.debug(
7088
f"Processing referent: {referent}, requested_attr: {requested_attr}"
7189
)
7290
revealed_attrs: dict[str, RevealedAttribute] = (
73-
auth_session.presentation_exchange["pres"]["indy"][
91+
auth_session.presentation_exchange["pres"][proof_format][
7492
"requested_proof"
7593
]["revealed_attr_groups"]
7694
)

0 commit comments

Comments
 (0)