Skip to main content
CCDigital implements decentralized digital identity management using Hyperledger Indy and ACA-Py (Aries Cloud Agent - Python) to enable verifiable credential-based authentication and state synchronization.

Architecture Overview

The identity layer consists of two ACA-Py agents operating in a peer-to-peer relationship:

Issuer Agent

Issues verifiable credentials to citizens and verifies proof presentations during login

Holder Agent

Stores citizen credentials and responds to proof requests from the verifier
Both agents communicate using the Aries Present-Proof 2.0 protocol over a persistent connection established during the credential issuance flow.

Credential Schema

Credentials issued by CCDigital contain the following attributes:
  • id_type - Type of identification (CC, TI, CE, etc.)
  • id_number - Identification number
  • first_name - First name(s)
  • last_name - Last name(s)
  • email - Email address
These attributes are anchored on the Indy ledger through a credential definition referenced by cred_def_id in the application configuration.

Verifiable Login Flow

CCDigital replaces traditional username/password authentication with a present-proof workflow that validates the user’s identity through their verifiable credential.
1

Initiate Proof Request

The system constructs a proof request filtered by the user’s id_number and sends it via /present-proof-2.0/send-request.
// IndyProofLoginService.java:100
public Map<String, Object> startLoginByIdNumber(String idNumber) {
    String connectionId = resolveHolderConnectionId();
    Map<String, Object> payload = buildSendRequestPayload(connectionId, idNumber);
    
    String url = verifierAdmin + "/present-proof-2.0/send-request";
    Map<String, Object> body = exchangeForMap(url, HttpMethod.POST, req);
    
    return body; // Contains presExId for polling
}
2

Holder Presents Proof

The holder agent (user’s wallet) automatically responds to the proof request by selecting the matching credential and presenting the requested attributes.
3

Verify Presentation

The issuer agent verifies the cryptographic proof and extracts revealed attributes.
// IndyProofLoginService.java:167
public Map<String, String> getVerifiedResultWithAttrs(String presExId) {
    Map<String, Object> record = getProofRecord(presExId);
    
    if (!Boolean.TRUE.equals(verified)) {
        throw new IllegalStateException("Proof no verificado");
    }
    
    Map<String, Object> rawAttrs = extractRevealedAttrsRaw(record);
    
    // Returns: id_type, id_number, first_name, last_name, email
    return out;
}
4

Authenticate User

The backend validates the revealed attributes against the persons table and creates an authenticated session.Service: UserAuthFlowService (src/main/java/co/edu/unbosque/ccdigital/service/)
The proof request includes a restriction to ensure only credentials issued with the configured cred_def_id and matching attr::id_number::value are accepted.

Credential Issuance

Credentials are issued to registered citizens using a Python script that reads from the MySQL database: Script: issue_credentials_from_db.py (external Indy tools) The script:
  1. Queries the persons table for eligible citizens
  2. Resolves or creates a connection with the holder agent
  3. Issues a credential with the person’s attributes
  4. Records the credential exchange ID for audit purposes

State Synchronization

User access states (ENABLED, SUSPENDED, DISABLED) are synchronized to the holder agent’s connection metadata to enable server-side access control decisions.
1

Admin Changes State

Government admin updates a user’s access_state via the admin dashboard.Service: UserAccessGovernanceService.java:85
2

Sync to ACA-Py

The system writes the state to the connection metadata endpoint:
// UserAccessGovernanceService.java:159
private SyncResult syncStateToIndy(AppUser user, Person person, 
                                   UserAccessState targetState, String reason) {
    Map<String, Object> statePayload = buildStatePayload(user, person, targetState, reason);
    Map<String, Object> payload = buildPostPayload(path, statePayload);
    
    JsonNode response = indyAdminClient.post(baseUrl, path, payload);
    return new SyncResult(true, true, "Sincronización Indy completada", null);
}
3

Record Audit Event

The state change is recorded on Hyperledger Fabric for immutable audit trail.Service: FabricAuditCliService
State synchronization requires ccdigital.indy.user-access-sync-enabled=true and a valid user-access-sync-path configuration pointing to the holder connection’s metadata endpoint.

Proof Status Polling

After initiating a proof request, the client polls the backend to check completion:
// IndyProofLoginService.java:133
public Map<String, Object> getProofStatus(String presExId) {
    Map<String, Object> record = getProofRecord(presExId);
    
    String state = asString(record.get("state"));
    Boolean verified = asBoolean(record.get("verified"));
    
    boolean done = "done".equalsIgnoreCase(state) || "abandoned".equalsIgnoreCase(state);
    
    return Map.of(
        "presExId", presExId,
        "state", state,
        "verified", verified,
        "done", done
    );
}
Polling configuration:
  • acapy.proof.poll-interval-ms (default: 1000ms)
  • acapy.proof.poll-timeout-ms (default: 120000ms)

Connection Resolution

The system resolves the holder’s connection_id in two modes:
  1. Fixed Mode: Uses ccdigital.indy.holder-connection-id if configured
  2. Auto Mode: Queries /connections?state=active and matches by their_label against ccdigital.indy.holder-label
Implementation: IndyProofLoginService.java:376

Configuration Reference

VariablePurpose
INDY_ISSUER_ADMIN_URLIssuer agent admin API endpoint
INDY_HOLDER_ADMIN_URLHolder agent admin API endpoint
INDY_HOLDER_CONNECTION_IDFixed connection ID or “auto”
INDY_HOLDER_LABELLabel to match holder connection in auto mode
INDY_CRED_DEF_IDCredential definition ID on Indy ledger
INDY_ADMIN_API_KEYAPI key for admin endpoints (optional)
INDY_USER_ACCESS_SYNC_ENABLEDEnable state synchronization
INDY_USER_ACCESS_SYNC_PATHMetadata endpoint path template

Key Services

Location: src/main/java/co/edu/unbosque/ccdigital/service/IndyProofLoginService.javaImplements present-proof 2.0 login workflow, status polling, and attribute extraction from verified presentations.
Location: src/main/java/co/edu/unbosque/ccdigital/service/UserAccessGovernanceService.javaManages user access state lifecycle and synchronization to Indy connection metadata.
Location: src/main/java/co/edu/unbosque/ccdigital/service/IndyAdminClient.javaHTTP client wrapper for ACA-Py admin API calls with API key authentication.
Location: src/main/java/co/edu/unbosque/ccdigital/service/UserAuthFlowService.javaOrchestrates the complete authentication flow including proof request, polling, OTP verification, and session creation.

Security Considerations

Cryptographic Verification: All proofs are verified cryptographically using Indy’s ZKP (Zero-Knowledge Proof) primitives. The backend never stores or transmits raw credential data.
Selective Disclosure: Only the requested attributes are revealed during proof presentation. Other credential attributes remain private.
Connection Security: The connection between issuer and holder must be established securely during initial credential issuance. Ensure invitations are transmitted over trusted channels.

Build docs developers (and LLMs) love