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:

  1. The verifier sends an authorization request to the holder.
  2. The holder validates the verifier’s identity.
  3. The holder generates a VP containing selected credentials.
  4. The VP is encrypted and sent back.
  5. 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:

  • iss must match the verifier identity
  • aud must clearly target the wallet
  • nonce prevents 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.