In modern identity systems, Verifiable Presentations (VPs) are a core building block for secure, privacy-preserving interactions between a holder (often a mobile wallet) and a verifier (typically a backend service). When these interactions involve sensitive claims, the security bar becomes significantly higher. Plain JSON over HTTPS is no longer enough.
To address replay attacks, payload tampering, verifier impersonation, and data leakage, production-grade VP flows often combine:
- JAR (JWT Authorization Request) signed requests
- Encrypted VP responses
- Certificate-based verifier identity validation
This article walks through how to approach and implement these mechanisms end-to-end using Spring Boot on the verifier side and Android on the holder side. The focus is not only on what to implement, but why each component exists and how they work together as a cohesive security model.
Understanding the VP Flow Security Model
Before touching code, it’s essential to understand the trust boundaries.
In a typical VP flow:
- The verifier sends an authorization request to the holder.
- The holder validates the verifier’s identity.
- The holder generates a VP containing selected credentials.
- The VP is encrypted and sent back.
- The verifier decrypts and validates the VP.
Each step introduces attack surfaces:
- Authorization requests can be modified
- Verifiers can be spoofed
- VP payloads can be intercepted
- Claims can be replayed
The combination of JAR, encryption, and certificates ensures:
- Integrity of the request
- Confidentiality of the response
- Cryptographic binding to real-world verifier identity
What Is a JAR Signed Authorization Request?
A JAR (JWT Authorization Request) is a JWT-encoded authorization request where parameters are:
- Canonically represented
- Digitally signed
- Optionally encrypted
Instead of sending query parameters like:
response_type=vp_token&client_id=verifier123
The verifier sends a signed JWT containing these values.
This provides:
- Tamper resistance
- Non-repudiation
- Clear verification of request origin
Designing the JAR Payload Structure
A minimal VP-oriented JAR payload might look like:
{
"iss": "https://verifier.example.com",
"aud": "wallet",
"response_type": "vp_token",
"client_id": "verifier.example.com",
"nonce": "n-0S6_WzA2Mj",
"presentation_definition": {
"id": "vp_request",
"input_descriptors": []
},
"iat": 1710000000,
"exp": 1710000600
}
Key considerations:
issmust match the verifier identityaudmust clearly target the walletnonceprevents replay attacks- Short expiration windows are critical
Signing the JAR in Spring Boot
On the verifier side, Spring Boot typically signs the JAR using a private key associated with an X.509 certificate.
Creating a Signed JAR
KeyPair keyPair = loadKeyPair();
JWSSigner signer = new RSASSASigner(keyPair.getPrivate());
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer("https://verifier.example.com")
.audience("wallet")
.claim("response_type", "vp_token")
.claim("client_id", "verifier.example.com")
.claim("nonce", UUID.randomUUID().toString())
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 600000))
.build();
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256).build(),
claims
);
signedJWT.sign(signer);
String jar = signedJWT.serialize();
This signed JWT is then delivered to the Android wallet via redirect, QR code, or deep link.
Verifying the JAR on Android
On Android, the wallet must verify:
- Signature validity
- Certificate trust chain
- Issuer consistency
- Audience and expiration
JAR Verification on Android
val signedJwt = SignedJWT.parse(jarString)
val cert = extractCertificate(signedJwt.header)
val verifier = RSASSAVerifier(cert.publicKey as RSAPublicKey)
require(signedJwt.verify(verifier)) { "Invalid signature" }
val claims = signedJwt.jwtClaimsSet
require(claims.expirationTime.after(Date())) { "Expired request" }
require(claims.issuer == expectedIssuer)
This step ensures the wallet only responds to trusted verifiers.
Certificate-Based Verifier Identity
Certificates elevate trust from “this key signed something” to:
“This request comes from a legally or organizationally identifiable verifier.”
A verifier certificate usually contains:
- Organization name
- Country
- DNS name
- Public key
- CA signature
The wallet should enforce:
- Trusted root CA
- Valid certificate path
- Domain binding to
iss
This prevents rogue verifiers from impersonating trusted entities.
Generating the Verifiable Presentation on Android
Once the request is trusted, the wallet builds a VP.
VP Creation
val vp = mapOf(
"@context" to listOf("https://www.w3.org/2018/credentials/v1"),
"type" to listOf("VerifiablePresentation"),
"verifiableCredential" to selectedCredentials,
"holder" to did
)
At this stage, do not send the VP in plaintext.
Encrypting the VP Response (JWE)
The VP must be encrypted so only the verifier can read it. This is usually done using JWE with the verifier’s public key.
Encrypting VP on Android
val jweHeader = JWEHeader.Builder(
JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A256GCM
).build()
val payload = Payload(JSONObject(vp).toString())
val jweObject = JWEObject(jweHeader, payload)
jweObject.encrypt(RSAEncrypter(verifierPublicKey))
val encryptedVp = jweObject.serialize()
Benefits:
- VP confidentiality
- Protection against MITM attacks
- Cryptographic binding to verifier identity
Sending the Encrypted VP to the Verifier
The encrypted VP is sent via POST:
{
"vp_token": "<encrypted-jwe>",
"state": "xyz"
}
The state parameter ensures request-response correlation.
Decrypting the VP in Spring Boot
On the verifier backend:
JWEObject jwe = JWEObject.parse(encryptedVp);
jwe.decrypt(new RSADecrypter(privateKey));
String vpJson = jwe.getPayload().toString();
After decryption, the verifier must:
- Validate VP structure
- Verify credential signatures
- Check nonce binding
- Enforce expiration and audience rules
Binding the VP to the Original Request
A critical but often overlooked step is binding.
The VP must cryptographically reference:
- The original
nonce - The verifier audience
This prevents replaying the VP to a different verifier or session.
{
"nonce": "n-0S6_WzA2Mj",
"aud": "https://verifier.example.com"
}
If these values do not match the original request, the VP must be rejected.
Error Handling and Security Pitfalls
Common mistakes include:
- Accepting unsigned JARs
- Skipping certificate validation
- Allowing long-lived authorization requests
- Reusing nonces
- Logging decrypted VPs
Every one of these mistakes undermines the security guarantees of VP flows.
Scaling and Operational Considerations
In production environments:
- Rotate signing certificates regularly
- Maintain CRL / revocation checks
- Cache verifier public keys carefully
- Enforce strict clock skew limits
- Monitor failed verifications for abuse
Security is not a one-time setup — it’s an operational discipline.
Conclusion
Approaching JAR signed requests, encrypted VP responses, and certificate-based verifier identity as isolated features is a common mistake. Their real power emerges when they are designed and implemented as a unified trust model.
JARs guarantee that authorization requests are authentic, tamper-proof, and time-bound, giving wallets confidence that they are responding to a legitimate request. Certificate-based verifier identity bridges the gap between cryptography and real-world trust, allowing wallets to make informed decisions about who is requesting sensitive credentials. Encrypted VP responses ensure that even if transport layers are compromised, the most sensitive user data remains confidential and verifier-bound.
When implemented correctly with Spring Boot on the verifier side and Android on the holder side, these mechanisms create a robust, scalable, and standards-aligned VP flow suitable for regulated industries, cross-border identity systems, and high-assurance digital wallets.
Most importantly, this architecture respects the core principle of modern identity systems: the holder remains in control, while verifiers receive exactly what they are entitled to — no more, no less — in a manner that is cryptographically provable, auditable, and secure by design.
If you get this right, you’re not just building a VP flow — you’re building trust infrastructure.