owned this note
owned this note
Published
Linked with GitHub
# Trusted Verifier Design Document
**Issue**: [#225 Spike: verifier identification](https://github.com/openwallet-foundation/acapy-vc-authn-oidc/issues/225)
## Overview
This document outlines the design for implementing **Trusted Verifier Authentication** in VC-AuthN OIDC and Bifold Wallet. This feature enables wallets to verify that a verifier (VC-AuthN instance) is authorized to request sensitive credential information before the wallet responds to proof requests.
## Key Objectives
1. **Mutual Authentication**: Enable bidirectional proof exchange where wallets can challenge verifiers
2. **Protected Claims**: Allow credential schemas to designate sensitive claims requiring verifier authorization
3. **Seamless UX**: Implement authorization checks without disrupting existing proof request flows
4. **Reusable Connections**: Leverage established connections for bidirectional proof exchanges
## Scope
**In Scope:**
- VC-AuthN holder/prover capabilities for responding to proof requests
- Bifold wallet capability to send reverse proof requests to verifiers
- Configuration for protected claims and trusted verifier credentials
- Bidirectional proof exchange over existing connections
**Out of Scope:**
- Governance framework for issuing trusted verifier credentials
- Credential revocation handling (Phase 1)
- Connectionless verifier authorization
## Problem Statement
### Business Context
When VC-AuthN requests credentials containing sensitive information (PII, unique identifiers, health records, etc.), holders have no mechanism to verify that the verifier is legitimate and authorized to receive this information. This creates:
- **Security Risk**: Malicious verifiers can request sensitive data without accountability
- **Privacy Concern**: Users cannot validate who is collecting their personal information
- **Compliance Gap**: Regulated industries may require verifier authentication for sensitive data
### User Story
**As a** wallet holder
**I want to** verify that a verifier is authorized to request my sensitive credentials
**So that** I can trust that my personal information is being collected by legitimate, authorized entities
### Technical Challenge
Currently:
```
Wallet <- proof-request <- VC-AuthN
(Holder) -> presentation -> (Verifier)
```
**One-way verification**: Only VC-AuthN verifies the wallet; the wallet cannot verify VC-AuthN.
Required:
```
Wallet <- proof-request <- VC-AuthN
(Verifier -> proof-request -> (Verifier
+Holder) <- presentation <- +Holder)
-> presentation ->
```
**Bidirectional verification**: Both parties authenticate each other.
---
## Current State Analysis
| Component | Current Capabilities | Missing for Trusted Verifier |
|-----------|---------------------|------------------------------|
| **VC-AuthN** | - Create/send presentation requests<br>- Receive presentations<br>- Connection management | - Incoming proof request handler<br>- Credential storage/management<br>- Presentation response logic<br>- Role-based routing |
| **Bifold** | - Send proof requests (verifier)<br>- Receive proof requests (holder)<br>- Detect sensitive claims<br>- Connection management | - Bidirectional proof exchange<br>- Verify verifier credentials<br>- Conditional response logic |
## Proposed Solution
### High-Level Architecture
The solution implements bidirectional proof exchange:
0. **VC-AuthN is offered and stores a dedicated credential**
1. **VC-AuthN sends proof request** to wallet (existing flow)
2. **Wallet detects protected claims** and pauses
3. **Wallet sends reverse proof request** to VC-AuthN asking for authorization
4. **VC-AuthN responds with trusted verifier credential**
5. **Wallet validates authorization** and decides to proceed or decline
6. **Wallet responds to original proof request** (if authorized)
### Key Design Decisions
| Decision | Choice | Rationale | Critical Notes |
|----------|--------|-----------|----------------|
| **Connection Type** | Ephemeral connections | Works with current implementation | Authorization MUST complete before original exchange for proper cleanup |
| **Connection Reuse** | Same connection for both exchanges | Reduces latency, simpler flow | Each exchange has unique `pres_ex_id`, shares `connection_id` |
| **Response Mode** | Automatic (Phase 1) | Faster, simpler implementation | Future: Add manual approval option |
| **Protected Claims** | Schema-level metadata | Centralized policy, OCA compatible | Bifold checks `containsPI` flag |
| **Authorization Timeout** | 30 seconds | Balance UX and network latency | Auto-decline on timeout |
| **Cleanup Logic** | Role-aware (verifier only) | Prevents premature connection deletion | Prover-role exchanges don't trigger cleanup |
### Storing Connections
---
## Technical Architecture
This section provides detailed architecture diagrams showing the system components, data models, and interaction patterns. The diagrams use PlantUML notation and show both the current single-direction verification and the required bidirectional approach.
### Current vs Required Architecture
#### Current Architecture (Verifier Only)
```plantuml
@startuml
!theme plain
participant "Wallet\n(Holder)" as Wallet
participant "VC-AuthN\n(Verifier)" as VCAuthN
VCAuthN -> Wallet : Proof Request
activate Wallet
Wallet -> VCAuthN : Presentation
deactivate Wallet
activate VCAuthN
VCAuthN -> VCAuthN : Verify Presentation
deactivate VCAuthN
note right of VCAuthN
One-way verification:
Only VC-AuthN verifies the wallet
Wallet cannot verify VC-AuthN
end note
@enduml
```
#### Required Architecture (Bidirectional)
```plantuml
@startuml
!theme plain
participant "Wallet\n(Verifier + Holder)" as Wallet
participant "VC-AuthN\n(Verifier + Holder)" as VCAuthN
VCAuthN -> Wallet : Proof Request
activate Wallet
Wallet -> VCAuthN : Reverse Proof Request\n(Prove Authorization)
activate VCAuthN
VCAuthN -> Wallet : Presentation\n(Trusted Verifier Credential)
deactivate VCAuthN
Wallet -> Wallet : Validate Verifier
alt Verifier Authorized
Wallet -> VCAuthN : Presentation
activate VCAuthN
VCAuthN -> VCAuthN : Verify Presentation
deactivate VCAuthN
else Verifier Not Authorized
Wallet -> VCAuthN : Decline (Problem Report)
end
deactivate Wallet
note right of VCAuthN
Bidirectional verification:
Both parties authenticate each other
Wallet validates verifier authorization
before sharing credentials
end note
@enduml
```
### Sequence Diagram
```plantuml
@startuml
autonumber
participant "BC Wallet" as Wallet
participant "Frontend"
participant "VC-AuthN OIDC Controller" as Verifier
participant "ACA-Py Agent" as ACA
ACA -> Wallet : Send proof request with protected claims
== Mutual Authentication: Wallet Verifies Verifier ==
Wallet -> Wallet : Detect protected claims in proof request
note left: Check if requested credentials/claims\nrequire verifier authorization
Wallet -> ACA : POST /present-proof-2.0/send-request\n(connection_id)\n[Request: "trusted verifier" credential]
note left: Wallet sends reverse proof request\nto verify verifier's authorization
ACA -> ACA : Auto-respond with matching credential\n(--auto-respond-presentation-request)
ACA -> Wallet : Send verifier authorization proof
ACA -> Verifier : Webhook: present_proof_v2_0 (request-received, done)
note right: Controller logs for observability
Wallet -> Wallet : Validate verifier credential\n- Check schema/cred_def\n- Verify issuer\n- Check claims match requirements
note left: Validation determines if\nverifier is authorized for\nthe requested protected claims
alt Verifier Authorization Valid
Wallet -> Wallet : Authorization successful\nProceed with original proof request
Wallet -> ACA : POST /present-proof-2.0/records/{pres_ex_id}/send-presentation\n[Original proof request response]
note left: Wallet presents requested credentials\nafter successful authorization
ACA -> Verifier : Webhook: present_proof_v2_0 (presentation-received)
ACA -> Verifier : Webhook: present_proof_v2_0 (done)
Verifier -> Verifier : Update AuthSession (VERIFIED)
Verifier --> Frontend : WebSocket notification: verified
Verifier --> Frontend : Return OIDC token with vc_presented_attributes
else Verifier Authorization Failed or Timeout
Wallet -> Wallet : Authorization failed/timeout\nDecline original proof request
note left: Timeout if verifier doesn't respond\nto authorization request
Wallet -> ACA : POST /present-proof-2.0/records/{pres_ex_id}/problem-report
Note left: Wallet declines original proof request\ndue to failed authorization
ACA -> Verifier : Webhook: present_proof_v2_0 (abandoned)
Verifier -> Verifier : Update AuthSession (FAILED)
Verifier --> Frontend : WebSocket notification: failed
Verifier --> Frontend : Return error: Unauthorized verifier
end
@enduml
```
### Data Model
#### AuthSession Extension (VC-AuthN)
```plantuml
@startuml
!theme plain
class AuthSession {
' Existing fields
+ id: str
+ pres_exch_id: str
+ connection_id: str | None
' NEW: Track reverse proof exchange
+ verifier_challenge_exch_id: str | None
+ verifier_challenge_state: str | None
+ verifier_challenge_timestamp: datetime | None
}
note right of AuthSession::pres_exch_id
Original proof request
(VC-AuthN as verifier)
end note
note right of AuthSession::verifier_challenge_exch_id
Reverse request
(VC-AuthN as prover)
Both exchanges share the
same connection_id
end note
@enduml
```
#### Presentation Exchange Correlation
```plantuml
@startuml
!theme plain
object "Exchange 1\n(Original)" as E1 {
pres_ex_id = "abc123"
role = "verifier"
direction = "VC-AuthN -> Wallet"
connection_id = "conn-xyz"
}
object "Exchange 2\n(Reverse/Challenge)" as E2 {
pres_ex_id = "def456"
role = "prover"
direction = "Wallet -> VC-AuthN"
connection_id = "conn-xyz"
}
object "Connection\nconn-xyz" as Conn {
connection_id = "conn-xyz"
state = "active"
}
E1 --> Conn : uses
E2 --> Conn : uses (same connection!)
note bottom of Conn
**Problem:** Two different pres_ex_ids
**Solution:** Lookup by connection_id
This allows correlating both exchanges
to the same AuthSession
end note
@enduml
```
**Solution**:
- Lookup AuthSession by `connection_id` (not `pres_ex_id`) when handling reverse proof requests
- Store reverse exchange ID in `verifier_challenge_exch_id` field
- Both exchanges correlate via shared `connection_id`
### Configuration Schema
**ACA-Py Settings** (`charts/vc-authn-oidc/values.yaml` ->`acapy.argfile.yml`):
- `auto-respond-presentation-request: true` - Auto-respond to proof requests with matching credentials
**Bifold Configuration** (`packages/core/src/types/config.ts`):
- `verifierAuthorization.enabled` - Feature flag
- `verifierAuthorization.protectedClaims` - Claim names requiring authorization (array)
- `verifierAuthorization.protectedSchemas` - Schema IDs requiring authorization (array)
- `verifierAuthorization.verifierCredentialSchema` - Expected verifier credential schema
- `verifierAuthorization.timeoutMs` - Timeout in milliseconds (default: 30000)
**Helm Chart** (`charts/vc-authn-oidc/values.yaml`):
- `acapy.argfile.yml.auto-respond-presentation-request` - Enable auto-response (default: false)
---
## Implementation Requirements
This section outlines the implementation work broken into 4 phases: (1) ACA-Py agent setup, (2) VC-AuthN controller code, (3) Bifold wallet changes, and (4) integration testing. Each phase includes specific tasks, implementation details, and acceptance criteria.
### Phase 1: VC-AuthN - ACA-Py Agent Setup
#### Tasks
1. **Enable Auto-Respond in Helm Chart**
- Add `auto-respond-presentation-request: true` to `acapy.argfile.yml` in values.yaml
- ACA-Py will automatically respond to proof requests with matching credentials
2. **Verify ACA-Py Capabilities**
- Confirm holder/prover features enabled
- Validate webhook delivery for `request-received` state with `role: "prover"`
3. **Issue Trusted Verifier Credential**
Choose approach based on environment:
- **Dev/Test**: Self-issuance via bootstrap script (agent issues to itself)
- **Production**: External trusted issuer (governance authority)
Bootstrap script should:
- Create schema/cred_def if not exists
- Issue credential to VC-AuthN's agent
- Be idempotent (safe to re-run)
4. **Test Auto-Respond Flow**
- Manually send proof request to VC-AuthN
- Verify ACA-Py auto-responds with credential
- Validate presentation verification
#### Acceptance Criteria
**Helm Chart**:
- [ ] `auto-respond-presentation-request: true` added to values.yaml
**ACA-Py Capabilities**:
- [ ] ACA-Py agent successfully receives proof requests
- [ ] Webhook fires with `state: "request-received"` and `role: "prover"`
- [ ] ACA-Py auto-responds with matching credential
- [ ] Trusted verifier credential issued via issue-credential protocol
---
### Phase 2: VC-AuthN - Controller Implementation
Since ACA-Py handles auto-response via `--auto-respond-presentation-request`, the controller only needs logging for observability.
#### Task 2.1: Add Prover-Role Webhook Logging
**What to Build**:
- Check `role` field in `present_proof_v2_0` webhook
- Log prover-role events for observability:
- `request-received`: "Received verifier authorization challenge"
- `presentation-sent`: "Sent trusted verifier credential"
- `done`: "Verifier authorization exchange complete"
- Include: `connection_id`, `pres_ex_id`, timestamp
- **CRITICAL**: Don't trigger connection cleanup from prover-role exchanges
**Acceptance Criteria**:
- [ ] Webhook routes based on `role` field
- [ ] Prover-role events logged with relevant IDs
- [ ] No cleanup from prover-role handler
---
#### Task 2.2: Extend AuthSession Data Model (Optional)
For correlation/observability, track the reverse exchange:
- Add `verifier_challenge_exch_id` field to AuthSession model
- Use existing `get_by_connection_id()` method to lookup session
**Acceptance Criteria**:
- [ ] Challenge exchange ID stored for correlation
---
### Phase 3: Bifold Wallet Implementation
Add wallet capability to challenge verifiers before responding to proof requests with protected claims.
#### Task 3.1: Create VerifierAuthorizationService
**What to Build**:
- `requiresAuthorization()` - Check if proof request needs verifier auth (based on protected claims/schemas)
- `requestVerifierCredential()` - Send reverse proof request to verifier
- `verifyVerifierCredential()` - Validate verifier's credential response
- `isAuthorizedForClaims()` - Check if verifier authorized for specific claims
#### Task 3.2: Create React Hook
**File**: `packages/core/src/hooks/verifier-authorization.ts`
**What to Build**:
- State management: `isAuthorized`, `isChecking`, `error`
- `checkAuthorization()` method with 30s timeout
- Integration with VerifierAuthorizationService
#### Task 3.3: Modify ProofRequest Screen
**What to Build**:
- Pre-authorization check before `handleAcceptPress`
- Loading state during authorization
- Auto-decline if authorization fails
- Skip authorization for non-protected claims
#### Task 3.4: Create Authorization Modal
**What to Build**:
- Show "Checking verifier..." loading state
- Display verifier credential details
- Show success/failure states with appropriate messaging
#### Task 3.5: Add Configuration
**What to Build**:
- `verifierAuthorization.enabled` - Feature flag
- `verifierAuthorization.protectedClaims` - Claim names requiring auth
- `verifierAuthorization.protectedSchemas` - Schema IDs requiring auth
- `verifierAuthorization.verifierCredentialSchema` - Expected verifier cred schema
- `verifierAuthorization.timeoutMs` - Timeout (default: 30000)
---
### Phase 4: Integration Testing
#### Test Scenarios
| Scenario | Expected Outcome |
|----------|------------------|
| **Success Path** | Wallet validates verifier credential, proceeds with original proof request |
| **Missing Credential** | VC-AuthN can't provide credential, wallet declines |
| **Invalid Credential** | Wallet validation fails, declines proof request |
| **Timeout** | 30s timeout expires, wallet auto-declines |
| **Non-Protected Claims** | Wallet skips authorization, proceeds normally |
#### Testing Approach
1. **Local Testing**: Docker environment (`./manage start`) + Bifold dev build
2. **Network Testing**: Deploy to BCovrin Test ledger for end-to-end validation
#### Success Criteria
- [ ] All scenarios pass with proper error handling
- [ ] Authorization completes in <5 seconds
- [ ] No connection/exchange leaks
- [ ] Clear user error messages
- [ ] Complete audit logging
---
## Flow Diagrams
This section provides additional visual representations to understand the behavior and state management of the trusted verifier feature.
### AuthSession State Transitions
The following diagram shows how authentication sessions progress through different states during the bidirectional proof exchange. Protected claims trigger an authorization challenge before verification proceeds.
### State Diagram
```plantuml
@startuml
!theme plain
title AuthSession State Transitions
[*] --> INITIALIZED
INITIALIZED --> CONNECTED : Connection\nestablished
CONNECTED --> REQUEST_SENT : Proof request\nsent
REQUEST_SENT --> CHALLENGE_SENT : Protected claims\ndetected
REQUEST_SENT --> PRESENTATION_RECEIVED : Non-protected claims\n(bypass authorization)
CHALLENGE_SENT --> CHALLENGE_RECEIVED : Verifier responds\nwith credential
CHALLENGE_RECEIVED --> VERIFIED : Credential valid
CHALLENGE_RECEIVED --> FAILED : Credential invalid\nor timeout
PRESENTATION_RECEIVED --> VERIFIED : Verification\nsuccessful
PRESENTATION_RECEIVED --> FAILED : Verification\nfailed
VERIFIED --> [*]
FAILED --> [*]
note right of REQUEST_SENT
Branching point:
- Protected claims -> Challenge verifier
- Non-protected -> Direct verification
end note
note right of CHALLENGE_RECEIVED
Validation checks:
- Schema matches expected
- Issuer is trusted
- Claims authorize request
end note
@enduml
```
### Webhook Handler Routing
This diagram illustrates how the webhook handler distinguishes between VC-AuthN acting as a verifier (normal flow) versus acting as a prover (responding to wallet challenges). The key difference is using `pres_ex_id` lookup for verifier-role webhooks and `connection_id` lookup for prover-role webhooks.
### Webhook Routing Logic
```plantuml
@startuml
!theme plain
title Webhook Handler Routing (present_proof_v2_0)
start
:Webhook Received;
:Parse body\nGet "role" field;
if (role == "verifier"?) then (yes)
partition "EXISTING FLOW\n(VC-AuthN sent request)" {
:Lookup AuthSession\nby pres_ex_id;
if (state?) then (presentation-received)
:Process presentation;
elseif (done)
:Finalize verification;
elseif (abandoned)
:Mark as failed;
endif
}
else (no, role == "prover")
partition "NEW FLOW\n(Wallet sent request)" {
:Log event for observability;
note right: ACA-Py auto-responds\nvia --auto-respond-presentation-request
:Lookup AuthSession\nby connection_id (optional);
:Store challenge exchange ID\nfor correlation;
}
endif
stop
note right
**Key Difference:**
- Verifier role: pres_ex_id lookup
- Prover role: connection_id lookup
This solves the correlation problem
for bidirectional exchanges.
end note
@enduml
```
---
## Success Criteria
### Functional Requirements
- [ ] **FR-1**: VC-AuthN can receive and respond to proof requests from wallets
- [ ] **FR-2**: VC-AuthN automatically responds with trusted verifier credential when configured
- [ ] **FR-3**: Bifold wallet detects protected claims in proof requests
- [ ] **FR-4**: Bifold wallet sends reverse proof request to verify verifier authorization
- [ ] **FR-5**: Bifold wallet validates verifier credential before responding
- [ ] **FR-6**: Bidirectional proof exchanges use the same connection
- [ ] **FR-7**: Authorization failures result in declined proof requests
- [ ] **FR-8**: Configuration controls authorization behavior
### Non-Functional Requirements
- [ ] **NFR-1**: Authorization check completes within 5 seconds (95th percentile)
- [ ] **NFR-2**: No additional connections created for authorization
- [ ] **NFR-3**: Authorization timeout prevents indefinite waiting (30s default)
- [ ] **NFR-4**: Error messages clearly indicate authorization failure reasons
- [ ] **NFR-5**: Audit logs capture all authorization attempts
- [ ] **NFR-6**: Feature can be disabled via configuration
- [ ] **NFR-7**: No breaking changes to existing proof request flows
### User Experience
- [ ] **UX-1**: Users see clear indication that verifier is being checked
- [ ] **UX-2**: Users see verifier credential details before proceeding
- [ ] **UX-3**: Timeout failures show helpful error messages
- [ ] **UX-4**: Non-protected claims bypass authorization (seamless)
---
## Risks and Mitigation
### Security Risks
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| **Malicious Credential Offers** | High | Low | Credential Offers Should Be Manually Accepted or Rate Limited |
| **Malicious Authorization Requests** | High | Low | Validate proof request format, rate limit requests |
| **Credential Theft** | High | Low | Only respond to valid proof request formats |
| **Man-in-the-Middle** | High | Low | Use DIDComm encryption, validate DIDs |
| **Replay Attacks** | Medium | Low | Use presentation exchange nonces |
### Operational Risks
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| **Configuration Errors** | Medium | High | Validation on startup, clear documentation |
| **Performance Degradation** | Medium | Low | Load testing, resource monitoring |
| **Breaking Changes** | Low | Low | Comprehensive testing, feature flags |
---
## Appendix
### B. References
- [Issue #225: Spike - Verifier Identification](https://github.com/openwallet-foundation/acapy-vc-authn-oidc/issues/225)
- [Bifold Wallet Architecture](https://github.com/openwallet-foundation/bifold-wallet)
### D. Configuration Examples
**Helm Chart** (`values.yaml`):
```yaml
acapy:
argfile.yml:
auto-respond-presentation-request: true
```
**Bifold Configuration:**
- `verifierAuthorization.enabled=true`
- `verifierAuthorization.protectedClaims=["ssn", "license_number", ...]`
- `verifierAuthorization.verifierCredentialSchema=<schema-id>`
- `verifierAuthorization.timeoutMs=30000`