Skip to content

ricdex/did-example-android-backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

6 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

DID Mobile Wallet β€” POC Backend

Sistema de Identidad Descentralizada (DID) sin blockchain, compuesto por una wallet Android y un backend Java.


Actores

Actor Rol DΓ³nde vive
Holder DueΓ±o de la identidad. Genera su clave, solicita credenciales y las presenta. App Android
Issuer Emite credenciales verificables firmadas. Registra e invalida DIDs. Backend Java
Verifier Verifica presentaciones del holder: valida firmas, estado del DID y revocaciΓ³n de VCs. Backend Java

POC: Issuer y Verifier estΓ‘n implementados en el mismo backend, separados por endpoints distintos. En un sistema real serΓ­an organizaciones independientes.

El holder es el ΓΊnico custodio de sus credenciales. El issuer no las almacena.


Flujos

1. Registro de identidad (solo en el dispositivo)

Holder (app)
──────────────────────────────────────────────────
1. Genera par de claves secp256k1
2. Protege la clave privada en hardware del dispositivo
3. Deriva su DID:key a partir de la clave pΓΊblica
   β†’ did:key:zQ3s...

No requiere red. No requiere backend.

2. Registro del DID en el issuer (primer uso)

Antes de solicitar credenciales, el holder registra su DID junto a su identificador de cliente. Esto permite al issuer conocer quΓ© DIDs estΓ‘n activos y a quΓ© persona corresponden.

Holder (app)              Issuer (backend)
─────────────────         ──────────────────────────────────

POST /dids/register ────────────────────────────→
{ client_id: "user@example.com",
  did: "did:key:zQ3s..." }
                          Guarda { did, clientId, active: true }
                          ←────────────── { did, client_id, active: true }

Si el holder pierde el dispositivo, el issuer puede invalidar ese DID con POST /dids/{did}/invalidate. En el dispositivo nuevo se registra un DID nuevo bajo el mismo client_id.

3. ObtenciΓ³n de una Verifiable Credential

El DID debe estar registrado y activo para obtener un nonce.

Holder (app)              Issuer (backend)
─────────────────         ──────────────────────────────────
                          Tiene su propio DID e identidad

1. GET /credentials/nonce?holder_did=did:key:z... ──→
                          βœ“ DID registrado y activo
                          ←────────────── { nonce }

2. Firma un Proof JWT con su clave privada
   (incluye el nonce y los claims que quiere acreditar)

3. POST /credentials/issue ─────────────────────→
   { holder_did, proof }
                          βœ“ DID activo
                          βœ“ Firma vΓ‘lida + nonce vΓ‘lido
                          Emite VC JWT firmada con su clave
                          Guarda solo metadatos (no el JWT)
                          ←────────────── { credential: <VC JWT> }

4. Almacena la VC cifrada en el dispositivo

3. PresentaciΓ³n y verificaciΓ³n de una VC

El holder construye una VP JWT (Verifiable Presentation) que empaqueta sus VCs y la firma con su clave privada. La envΓ­a al endpoint POST /credentials/verify del backend.

POC: el endpoint de verificaciΓ³n estΓ‘ en el mismo backend que el issuer. En producciΓ³n, el Verifier serΓ­a una organizaciΓ³n separada que consumirΓ­a los endpoints pΓΊblicos del issuer (GET /dids/{did} y GET /credentials?holder_did=) para consultar estado y revocaciΓ³n.

Holder (app)              Backend (Verifier + Issuer β€” mismo proceso)
─────────────────         ──────────────────────────────────────────

1. Toma sus VCs almacenadas
2. Construye VP JWT:
   { iss: holderDid,
     aud: "http://backend",
     vp: { verifiableCredential: [VC_JWT] } }
3. La firma con su clave privada

4. POST /credentials/verify ─────────────────→
   { "vp_jwt": "eyJ..." }

                          βœ“ alg = ES256K
                          βœ“ VP no expirada
                          βœ“ Firma ES256K del holder vΓ‘lida
                            (clave pΓΊblica derivada del did:key del iss)
                          βœ“ DID del holder registrado y activo

                          Para cada VC dentro del VP:
                          βœ“ Firma ES256K del issuer vΓ‘lida
                          βœ“ iss de la VC = DID de este issuer
                          βœ“ sub de la VC = holderDid del VP
                          βœ“ VC no expirada
                          βœ“ VC no revocada (consulta interna al store)

   ←─── 200 { "valid": true, "holder_did": "...",
               "credentials": [{ credential_id, type, subject, ... }] }

Por quΓ© no se necesita red para verificar firmas: el did:key contiene la clave pΓΊblica β€” se deriva directamente del DID sin llamadas externas. Solo el estado dinΓ‘mico (DID activo, VC revocada) requiere consulta al store.


Seguridad

Clave privada del holder

La clave privada nunca sale del dispositivo en claro. Se protege en dos capas:

Clave privada secp256k1
    └── cifrada con AES-256/GCM
            └── cuya clave vive en Android Keystore (hardware)
                    β”œβ”€β”€ StrongBox (chip dedicado) β€” si el dispositivo lo tiene
                    └── TEE (zona aislada del SoC) β€” fallback en casi todos los Android modernos

La app detecta automΓ‘ticamente el nivel disponible y usa el mΓ‘s seguro.

Verifiable Credentials en disco

Las VCs se almacenan cifradas con EncryptedSharedPreferences (AES-256/GCM). Incluso con acceso fΓ­sico al dispositivo o backup ADB, el contenido es ilegible.

Custodia de credenciales

El issuer no guarda el JWT de la VC. Lo entrega una sola vez al holder y conserva solo un hash para poder revocarla en el futuro. El holder es el ΓΊnico custodio.

Anti-replay

Cada solicitud de credencial requiere un nonce fresco del issuer. El nonce se consume al verificarlo β€” enviarlo dos veces es rechazado.


CΓ³mo funciona el Proof JWT

El Proof JWT demuestra que el holder posee la clave privada correspondiente a su DID, sin revelarla. Es el mecanismo central de autenticaciΓ³n del protocolo.

Paso 1 β€” Generar el par de claves y el DID (una sola vez)

clave privada (secp256k1, 32 bytes)
        β”‚
        β–Ό
clave pΓΊblica comprimida (33 bytes)
        β”‚
        β–Ό
multicodec prefix [0xe7, 0x01] + pub_bytes
        β”‚
        β–Ό
base58btc encode β†’ z<encoded>
        β”‚
        β–Ό
DID = "did:key:z<encoded>"

El DID es la clave pΓΊblica codificada. No se registra en ningΓΊn servidor β€” se resuelve localmente. Quien recibe el DID puede derivar la clave pΓΊblica y verificar firmas sin contactar al issuer.

Paso 2 β€” Registrar el DID en el issuer (una sola vez)

POST /dids/register
{ "client_id": "user@example.com", "did": "did:key:z..." }
β†’ { "did": "...", "client_id": "...", "active": true }

El issuer solo entrega nonces a DIDs que conoce y estΓ‘n activos.

Paso 3 β€” Pedir el nonce (el DID debe estar activo)

GET /credentials/nonce?holder_did=did:key:z...
β†’ { "nonce": "abc123" }

El nonce es de un solo uso y tiene expiraciΓ³n de 5 minutos. Protege contra replay attacks: si alguien intercepta el Proof JWT, no puede reutilizarlo porque el nonce ya fue consumido.

Paso 4 β€” Firmar el Proof JWT (con la clave privada)

header  = { alg: ES256K, typ: openid4vci-proof+jwt, kid: "did:key:z...#z..." }
payload = {
    iss:             "did:key:z..."     ← quiΓ©n firma
    aud:             "https://issuer"   ← a quiΓ©n va dirigido
    nonce:           "abc123"           ← el nonce recibido
    credential_type: "UniversityDegreeCredential"
    subject_claims:  { givenName, familyName, email }
}
firma = ECDSA(SHA256(header.payload), clave_privada)

La firma cubre el nonce β€” si alguien modifica el nonce, la firma es invΓ‘lida.

Paso 5 β€” El backend verifica sin conocer la clave privada

1. Extrae el DID del campo iss
2. Deriva la clave pΓΊblica del DID (es pΓΊblica, estΓ‘ en el DID)
3. Verifica la firma ECDSA con esa clave pΓΊblica
4. Verifica que el nonce coincida y no haya sido usado antes
5. Si todo ok β†’ emite la VC firmada por el issuer

La clave privada nunca sale del dispositivo. El backend solo necesita la clave pΓΊblica (obtenida del DID) para verificar.

Flujo completo

App Android / Postman                  Issuer Backend
─────────────────────                  ──────────────────────────────
1. ./scripts/gen-holder.sh
   β†’ genera clave privada + pΓΊblica
   β†’ deriva DID del holder
   β†’ guarda en .holder-keys

2. POST /dids/register ────────────────────────────────────────────▢
   { client_id: "user@example.com", did: "did:key:z..." }
                                       guarda { did, clientId, active: true }
   ◀─ { active: true } ───────────────────────────────────────────

3. GET /credentials/nonce?holder_did=did:key:z... ─────────────────▢
                                       βœ“ DID registrado y activo
                                       guarda nonce de un solo uso
   ◀─ { nonce: "abc123" } ────────────────────────────────────────

4. ./scripts/gen-proof.sh <nonce>
   β†’ lee clave privada de .holder-keys
   β†’ firma JWT con nonce + subject_claims
   β†’ imprime PROOF_JWT

5. POST /credentials/issue ────────────────────────────────────────▢
   { holder_did, proof: PROOF_JWT }
                                       βœ“ DID activo
                                       βœ“ firma vΓ‘lida (clave pΓΊblica del DID)
                                       βœ“ nonce consumido (one-time)
                                       emite VC JWT firmada por el issuer
   ◀─ { credential: "eyJ..." } ───────────────────────────────────

CΓ³mo funciona el VP JWT

El VP JWT (Verifiable Presentation) es el mecanismo con el que el holder demuestra ante el Verifier que posee una o mΓ‘s VCs. Lo construye Γ©l mismo, lo firma con su clave privada y lo envΓ­a al backend.

QuΓ© contiene

header  = { alg: ES256K, typ: JWT, kid: "did:key:z...#z..." }
payload = {
    iss: "did:key:z..."           ← DID del holder (quiΓ©n presenta)
    aud: "https://backend"        ← a quiΓ©n va dirigido
    iat: <unix timestamp>
    exp: <unix timestamp + 300>   ← vΓ‘lido 5 minutos
    vp: {
        type: ["VerifiablePresentation"],
        verifiableCredential: [   ← lista de VCs JWT a presentar
            "eyJ...VC_JWT..."
        ]
    }
}
firma = ECDSA(SHA256(header.payload), clave_privada_del_holder)

La diferencia clave con el Proof JWT: no lleva nonce del issuer. La VP se construye de forma autΓ³noma β€” el holder decide cuΓ‘ndo y quΓ© presenta.

QuΓ© verifica el backend al recibirlo

1. alg = ES256K
2. exp > ahora (presentaciΓ³n no expirada)
3. Derivar clave pΓΊblica del holder desde el campo iss (did:key)
4. Verificar firma ES256K sobre header.payload
5. DID del holder estΓ‘ registrado y activo (consulta al store)

Por cada VC en vp.verifiableCredential:
6. Verificar firma ES256K del issuer (clave pΓΊblica derivada del iss de la VC)
7. iss de la VC == DID de este issuer (rechaza VCs de issuers desconocidos)
8. sub de la VC == iss del VP (la VC le pertenece al holder que presenta)
9. exp de la VC > ahora
10. VC no revocada (consulta al store por su jti)

Flujo completo de verificaciΓ³n

App Android / Terminal                 Backend (Verifier)
──────────────────────                 ──────────────────────────────
(ya tienes .holder-keys y VC_JWT)

./scripts/gen-vp.sh <VC_JWT>
  β†’ lee clave privada de .holder-keys
  β†’ construye VP JWT con la VC
  β†’ firma con ES256K
  β†’ POST /credentials/verify ────────────────────────────────────▢
                                        βœ“ firma del holder
                                        βœ“ DID activo
                                        βœ“ firma del issuer en la VC
                                        βœ“ VC no revocada
    ◀── { valid: true, holder_did, credentials: [...] } ─────────

Para pruebas con Postman

# Paso 1 β€” generar identidad del holder (una sola vez)
./scripts/gen-holder.sh
# β†’ imprime HOLDER_DID, guarda .holder-keys
# β†’ copiar HOLDER_DID a la variable HOLDER_DID de la colecciΓ³n

# Paso 2 β€” registrar DID en el issuer (ejecutar "POST Register DID" en Postman)
# β†’ body: { "client_id": "user@example.com", "did": "{{HOLDER_DID}}" }

# Paso 3 β€” obtener nonce (ejecutar "GET Nonce" en Postman)
# β†’ NONCE se guarda automΓ‘ticamente en la variable de colecciΓ³n

# Paso 4 β€” generar proof con el nonce
./scripts/gen-proof.sh <NONCE>
# β†’ imprime PROOF_JWT
# β†’ copiar a la variable PROOF_JWT de la colecciΓ³n

# Paso 5 β€” ejecutar "POST Issue VC" en Postman
# β†’ guarda el valor de "credential" (VC_JWT)

# Paso 6 β€” generar y enviar la VP (ejecuta curl automΓ‘ticamente)
./scripts/gen-vp.sh <VC_JWT>
# β†’ imprime VP_JWT y resultado del backend (valid: true/false)
# β†’ para probarlo en Postman: copiar VP_JWT a "POST Verify VP"

Ver README-postman.md para la guΓ­a completa paso a paso.


Formato de los mensajes

Proof JWT β€” del holder al issuer

{
  "iss": "did:key:zQ3s...",
  "aud": "http://issuer.example.com",
  "nonce": "abc123",
  "credential_type": "UniversityDegreeCredential",
  "subject_claims": { "givenName": "Juan", "familyName": "PΓ©rez" }
}

Firmado con la clave privada del holder (ES256K).

VC JWT β€” del issuer al holder

{
  "iss": "did:key:zIssuer...",
  "sub": "did:key:zQ3s...",
  "vc": {
    "type": ["VerifiableCredential", "UniversityDegreeCredential"],
    "credentialSubject": { "id": "did:key:zQ3s...", "givenName": "Juan" }
  }
}

Firmado con la clave privada del issuer (ES256K).

VP JWT β€” del holder al verifier

{
  "iss": "did:key:zQ3s...",
  "aud": "https://backend.example.com",
  "iat": 1772674193,
  "exp": 1772674493,
  "vp": {
    "type": ["VerifiablePresentation"],
    "verifiableCredential": ["<VC_JWT>"]
  }
}

Firmado con la clave privada del holder (ES256K). VΓ‘lido 5 minutos. Sin nonce β€” el holder lo construye de forma autΓ³noma.


EstΓ‘ndares implementados

EstΓ‘ndar Para quΓ© se usa
W3C DID Core 1.0 Formato y resoluciΓ³n de DIDs
did:key MΓ©todo DID autΓ³nomo β€” el DID se deriva directamente de la clave pΓΊblica, sin registro externo
W3C VC Data Model 1.1 Estructura de las Verifiable Credentials y Presentations
JWT VC/VP Encoding de VCs y VPs como tokens JWT compactos
ES256K β€” RFC 8812 Algoritmo de firma: ECDSA con curva secp256k1 y hash SHA-256
OpenID4VCI (draft) Protocolo de solicitud de credencial entre holder e issuer

Claves y su propΓ³sito

Clave Tipo QuiΓ©n la tiene Para quΓ© se usa
Clave privada del holder secp256k1 (256 bits) Solo el dispositivo Android Firmar el Proof JWT y las VPs. Nunca sale del dispositivo.
Clave pΓΊblica del holder secp256k1 comprimida (33 bytes) Embebida en el DID:key Permite que cualquiera verifique sus firmas sin contactar al issuer.
Clave privada del issuer secp256k1 (256 bits) Solo el backend Firmar las VCs que emite. Se genera una vez y se persiste.
Clave pΓΊblica del issuer secp256k1 comprimida (33 bytes) Su DID Document Permite que el verifier valide las VCs sin contactar al issuer.
Clave AES-256 (wrapping) AES-256 simΓ©trica Android Keystore (hardware) Cifrar la clave privada del holder en disco. Nunca sale del chip.

La clave AES no tiene rol criptogrΓ‘fico en el protocolo DID β€” su ΓΊnico trabajo es proteger la clave secp256k1 mientras estΓ‘ almacenada en el dispositivo.


QuΓ© necesita el backend para operar

  1. Un par de claves secp256k1 β€” se genera automΓ‘ticamente al primer inicio
  2. Un DID propio β€” derivado de su clave pΓΊblica (did:key)
  3. Endpoint de nonce β€” protege contra replay attacks
  4. Endpoint de emisiΓ³n β€” verifica el proof y firma la VC
  5. Registro de DIDs β€” saber quΓ© DIDs estΓ‘n activos y a quΓ© cliente pertenecen
  6. Persistencia β€” solo metadatos de auditorΓ­a (H2 en local, Azure Table Storage en cloud)

No se necesita blockchain, PKI propia ni registro DID externo. did:key se resuelve localmente a partir del propio DID.


Endpoints del backend

GestiΓ³n de DIDs del holder

MΓ©todo URL DescripciΓ³n
POST /dids/register Registra un DID asociado a un client_id
GET /dids/{did} Estado del DID (activo/inactivo) β€” para verificadores
POST /dids/{did}/invalidate Invalida un DID (dispositivo perdido o app borrada)
GET /clients/{clientId}/dids Lista todos los DIDs de un cliente

Credenciales

MΓ©todo URL DescripciΓ³n
GET /credentials/nonce?holder_did= Nonce de un solo uso (DID debe estar activo)
POST /credentials/issue Recibe proof JWT, emite VC JWT
POST /credentials/verify Verifica una VP JWT (firma, DID activo, VCs no revocadas)
GET /credentials?holder_did= Metadatos de VCs emitidas (sin contenido)
POST /credentials/{credentialId}/revoke Revoca una VC por su ID

Identidad del issuer

MΓ©todo URL DescripciΓ³n
GET /issuer/did DID e info del issuer
GET /issuer/did-document DID Document del issuer

Estructura del proyecto

didbmo/
β”œβ”€β”€ android/app/src/main/java/com/did/wallet/
β”‚   β”œβ”€β”€ security/KeyManager.java          ← claves secp256k1 + Android Keystore
β”‚   β”œβ”€β”€ did/DIDManager.java               ← construcciΓ³n del DID:key
β”‚   β”œβ”€β”€ credential/
β”‚   β”‚   β”œβ”€β”€ CredentialRequestBuilder.java ← proof JWT firmado
β”‚   β”‚   └── CredentialService.java        ← HTTP al issuer + almacenamiento cifrado
β”‚   β”œβ”€β”€ presentation/VPBuilder.java       ← VP JWT firmada
β”‚   └── ui/MainActivity.java             ← demo de los 3 flujos
β”‚
└── backend/src/main/java/com/did/issuer/
    β”œβ”€β”€ config/IssuerKeyConfig.java       ← clave e identidad del issuer
    β”œβ”€β”€ controller/
    β”‚   β”œβ”€β”€ CredentialController.java     ← nonce, issue, revoke, metadatos
    β”‚   └── HolderDIDController.java      ← register, invalidate, status
    β”œβ”€β”€ service/
    β”‚   β”œβ”€β”€ NonceService.java             ← nonces single-use
    β”‚   β”œβ”€β”€ ProofVerifier.java            ← verifica proof JWT
    β”‚   β”œβ”€β”€ CredentialIssuerService.java  ← emite y firma VCs
    β”‚   └── HolderDIDService.java         ← registro e invalidaciΓ³n de DIDs
    β”œβ”€β”€ store/
    β”‚   β”œβ”€β”€ CredentialStore.java          ← interface
    β”‚   β”œβ”€β”€ JpaCredentialStore.java       ← H2 (local)
    β”‚   β”œβ”€β”€ AzureTableCredentialStore.java← Azure Table Storage
    β”‚   β”œβ”€β”€ HolderDIDStore.java           ← interface
    β”‚   β”œβ”€β”€ JpaHolderDIDStore.java        ← H2 (local)
    β”‚   └── AzureTableHolderDIDStore.java ← Azure Table Storage
    └── model/
        β”œβ”€β”€ CredentialRecord.java         ← metadatos de VC emitida
        └── HolderDIDRecord.java          ← registro de DID del holder

About

DID Mobile Wallet + Issuer Backend

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors