Skip to content

INJIMOB-3751 - Add JWT VC Support in VC Verifier#234

Open
KVD-1302 wants to merge 15 commits intoinji:developfrom
KVD-1302:INJIMOB-3751-jwt-vc-support
Open

INJIMOB-3751 - Add JWT VC Support in VC Verifier#234
KVD-1302 wants to merge 15 commits intoinji:developfrom
KVD-1302:INJIMOB-3751-jwt-vc-support

Conversation

@KVD-1302
Copy link

@KVD-1302 KVD-1302 commented Feb 6, 2026

Description

This PR implements cryptographic signature verification and structural validation for JWT Verifiable Credentials (jwt_vc) within the vc-verifier library. The "fail-closed" security implementation that ensures both the integrity and the authenticity of JWT-based credentials.

Changes

Core Verifier Implementation:

  • JwtVerifier.kt: Implemented a strict cryptographic verifier using an algorithm allowlist (RS/ES families) to prevent algorithm-confusion attacks. It resolves the issuer's public key and handles malformed payloads with explicit error checking to prevent NullPointerExceptions.
  • JwtValidator.kt: Added a multi-layered validation pipe with upfront input normalization and clock skew tolerance (60s) for time-based claims to prevent false rejections in distributed environments.
  • CredentialVerifierFactory.kt & CredentialFormat.kt: Integrated the new JWT logic into the factory pattern, enabling the library to dynamically select the correct verifier for JWT_VC formats.

Example App & Build Environment:

  • MainActivity.kt: Updated the example app to demonstrate the hybrid verification flow. Included a sample encoded JWT VC to facilitate end-to-end testing of the new verifier.
  • build.gradle.kts: Implemented targeted dependency exclusions within the example app's configuration to resolve Bouncy Castle and Titanium JSON-LD version conflicts. This approach preserves the core library’s dependency integrity while ensuring a stable build environment for the Android demo.

Unit Tests:

  • Added unit tests for signature verification and public key resolution via did:jwk.
  • Implemented validation for JWT format, expiration (exp), and "not before" (nbf) claims.
  • Added test resources for valid and tampered JWT strings.

Ticket ID

INJIMOB-3749

Testing and Screenshots

Methodology: Validated the implementation using the internal example app. Testing focused on bypassing IDE/build caches to ensure the code was live and responsive to tampering.

Verification Steps:
1. Positive Flow (Success): Triggered verification on a valid, original JWT VC.
Result: SUCCESS — Signature and structure verified successfully.

image

2. Negative Flow (Cryptographic Tampering): Modified the signature bits (e.g., swapping a character at the end of the string from Q to s).
Result: FAILED — Correcty caught by the verifier with a CRYPTO_FAILURE alert.

image

3. Negative Flow (Structural/Character Tampering): Injected illegal Base64 characters (e.g., !) and modified the JWT header structure.
Result: FAILED — Caught by the Regex/Validator layer, preventing unparseable data from reaching the crypto engine.

image

4. Negative Flow: Header Meddling (The "Broken Envelope"): The JWT header contains the metadata (like the algorithm). If you delete even one character, the resulting string is no longer a valid Base64-encoded JSON object.
The Result: FAILED — caught by an error depending on which part of header is effected.

image

5. Negative Flow: Payload Tampering (The "Corrupted Payload"): The payload contains the actual credential data. Messing with this part by adding or deleting just a few characters proves the validator ensures the content is readable JSON before the verifier even looks at the signature.
The Result: FAILED — caught by error saying Invalid JWT structure: Payload of JWS object is not a valid JSON object.

image

Video

feat-3751-v2.mp4

Summary by CodeRabbit

  • New Features

    • Added full JWT verifiable credential support, including validation and signature verification.
    • Factory routing now handles JWT credential format.
    • Example app updated with an interactive verification flow showing waiting, success, and failure states and a sample JWT.
  • Tests

    • Added unit tests and test resources covering valid, invalid, expired, not-yet-valid, and missing-claim JWT scenarios.

…nd structural validation for JWT VCs

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
…and update UI for fail-closed error handling

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds JWT-based Verifiable Credential support across verifier core and example app: new JwtValidator, JwtVerifier, JwtVerifiableCredential, factory registration and enum entry (JWT_VC); integrates validation + signature verification into the example MainActivity and adds unit tests and test fixtures.

Changes

Cohort / File(s) Summary
Credential format & factory
vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialFormat.kt, vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/CredentialVerifierFactory.kt
Added JWT_VC enum value and wired JwtVerifiableCredential into the factory mapping.
JWT verifiable credential type
vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/types/JwtVerifiableCredential.kt
New JwtVerifiableCredential implementing VerifiableCredential, delegating validate/verify to JwtValidator/JwtVerifier.
JWT validation logic
vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
New JwtValidator that checks JWT structure, regex, exp/nbf with clock skew, and presence of vc claim; returns ValidationStatus with messages.
JWT cryptographic verification
vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
New JwtVerifier that parses JWS, extracts iss, resolves public key via PublicKeyResolverFactory, enforces alg/payload expectations, and performs signature verification (throws on failure).
Example app integration
vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt
Replaced placeholder verify flow with hardcoded JWT, uses CredentialVerifierFactory to validate() then verify(), handles exceptions, and updates UI with detailed VerificationResult messages.
Build config changes
vc-verifier/kotlin/example/build.gradle.kts
Switched dependency exclusion approach to inline group-qualified excludes in configurations.all.
Tests & fixtures
vc-verifier/kotlin/vcverifier/src/test/java/.../JwtValidatorTest.kt, .../JwtVerifierTest.kt, src/test/resources/jwt_vc/validJwt.txt, .../invalidJwt.txt
Added unit tests for JwtValidator and JwtVerifier plus valid/invalid JWT test resources; includes MockK usage for public key resolution.

Sequence Diagram

sequenceDiagram
    participant Main as MainActivity
    participant Factory as CredentialVerifierFactory
    participant JwtVC as JwtVerifiableCredential
    participant Validator as JwtValidator
    participant Verifier as JwtVerifier
    participant KeyResolver as PublicKeyResolverFactory
    participant Crypto as SignatureVerification

    Main->>Factory: getCredentialVerifier(CredentialFormat.JWT_VC)
    Factory->>Main: JwtVerifiableCredential
    Main->>JwtVC: validate(jwtToken)
    JwtVC->>Validator: validate(jwtToken)
    Validator->>Validator: parse JWT, check regex, exp/nbf, vc claim
    Validator->>JwtVC: ValidationStatus
    alt ValidationStatus == SUCCESS
        Main->>JwtVC: verify(jwtToken)
        JwtVC->>Verifier: verify(jwtToken)
        Verifier->>Verifier: parse JWS, extract iss, check payload
        Verifier->>KeyResolver: resolvePublicKey(iss)
        KeyResolver->>Verifier: PublicKey
        Verifier->>Crypto: verifySignature(jws, PublicKey)
        Crypto->>Verifier: verificationResult
        Verifier->>JwtVC: Boolean
        JwtVC->>Main: verification result
    else Validation failed
        JwtVC->>Main: ValidationStatus (error)
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

Suggested Reviewers

  • mayuradesh
  • swatigoel

Poem

🐰 I hopped through headers, claims, and keys,
I checked exp and nbf with careful ease.
Found the issuer, fetched the key with glee,
Verified the signature — hop! — jubilee! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically identifies the main change: adding JWT Verifiable Credential (jwt_vc) support to the VC Verifier library, which aligns directly with the substantial implementation across multiple new files and integrations.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@vc-verifier/kotlin/build.gradle.kts`:
- Around line 10-18: The root build.gradle.kts currently uses configurations.all
{ resolutionStrategy { exclude(...) } } to globally remove Bouncy Castle and
Titanium JSON‑LD which breaks vcverifier because vcverifier/build.gradle.kts
declares implementation(libs.bouncyCastle) and
implementation(libs.titaniumJsonLd); remove these global exclude entries (the
exclude calls inside resolutionStrategy) and instead apply targeted
per-dependency excludes or configuration-level excludes where needed (following
the pattern in vcverifier/build.gradle.kts such as ldSignaturesJava {
exclude(group=..., module=...) } or configuration.exclude(...)), ensuring you do
not exclude the actual module names used (e.g., titanium-json-ld-jre8 vs
titanium-json-ld) and preserving implementation(libs.bouncyCastle) and
implementation(libs.titaniumJsonLd).

In
`@vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt`:
- Around line 13-18: The code trims the credential only for the jwtRegex check
but then passes the untrimmed credential into SignedJWT.parse, causing
inconsistent behavior; update JwtValidator to normalize the input once (e.g.,
assign val normalized = credential.trim()) and use that normalized value for
both the jwtRegex.matches check and the SignedJWT.parse call, ensuring
ValidationStatus and any subsequent logic also operate on the normalized string
(references: jwtRegex, credential.trim(), SignedJWT.parse, ValidationStatus).

In
`@vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt`:
- Around line 21-22: The call jwsObject.payload.toJSONObject() can return null
and currently payload is used without a null-check (payload["iss"]), so update
the JwtVerifier logic to handle a null payload: assign val payload =
jwsObject.payload.toJSONObject(), then check if payload == null and if so throw
or return a clear error (e.g., IllegalArgumentException or a descriptive
exception logged) before accessing payload["iss"]; after the check safely
extract issuer with payload["iss"]?.toString() and proceed. Ensure the fix is
applied where JwtVerifier constructs payload/issuer so any malformed/non-JSON
JWS payloads produce a descriptive error instead of an NPE.
🧹 Nitpick comments (3)
vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt (2)

20-32: Consider adding clock skew tolerance for exp and nbf checks.

In distributed systems, minor clock differences between the issuer and verifier can cause valid credentials to be incorrectly rejected. A small tolerance (e.g., 30–60 seconds) is common practice for JWT time-based claims validation.


39-39: Replace implicit success representation with a dedicated constant or factory method.

Using ValidationStatus("", "") to signal success is implicit and fragile across multiple validators (JwtValidator, CwtValidator, SdJwtValidator). Consider defining a constant (e.g., companion object { val SUCCESS = ValidationStatus("", "") }) or a factory method (e.g., fun success() = ValidationStatus("", "")) to make the intent explicit and consistent.

vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt (1)

16-43: Consider restricting accepted JWS algorithms to prevent algorithm confusion attacks.

The verifier trusts whatever algorithm the JWT header declares and only dispatches based on key type. An attacker who can influence the JWT could potentially exploit algorithm confusion (e.g., using "alg": "none" or an unexpected algorithm). Best practice is to explicitly whitelist acceptable algorithms (e.g., ES256, RS256) and reject JWTs whose header algorithm doesn't match.

… and restore library build integrity

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt`:
- Around line 33-36: The Elvis operator is unnecessary because
PublicKeyResolverFactory.get(URI) returns a non-null PublicKey and throws on
failure; in JwtVerifier.kt replace the current nullable-handling line with a
direct call to val publicKey = factory.get(URI.create(issuer)) and either let
the underlying PublicKeyNotFoundException (or other resolver exceptions)
propagate or explicitly catch and rethrow the specific exceptions from
PublicKeyResolverFactory.get to preserve original failure semantics.
🧹 Nitpick comments (2)
vc-verifier/kotlin/example/build.gradle.kts (1)

56-63: exclude calls are on Configuration, not ResolutionStrategy — misleading nesting.

The exclude(group, module) method belongs to the Configuration receiver, not ResolutionStrategy. Kotlin's implicit receiver resolution makes this compile and work, but placing them inside resolutionStrategy { } is misleading. Move the excludes outside:

♻️ Suggested restructure
 configurations.all {
-    resolutionStrategy {
-        exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
-        exclude(group = "org.bouncycastle", module = "bcutil-jdk18on")
-        exclude(group = "org.bouncycastle", module = "bcprov-jdk15on")
-        exclude(group = "org.bouncycastle", module = "bcutil-jdk15on")
-        exclude(group = "com.apicatalog", module = "titanium-json-ld")
-    }
+    exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
+    exclude(group = "org.bouncycastle", module = "bcutil-jdk18on")
+    exclude(group = "org.bouncycastle", module = "bcprov-jdk15on")
+    exclude(group = "org.bouncycastle", module = "bcutil-jdk15on")
+    exclude(group = "com.apicatalog", module = "titanium-json-ld")
 }

Also, the comment on line 55 mentions "force versions" but no force(...) call exists — consider updating the comment.

vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt (1)

20-24: Algorithm restriction is case-sensitive and uses a blocklist instead of an allowlist.

The alg == "none" check is case-sensitive, so variants like "None" or "NONE" bypass it. More importantly, a blocklist approach is weaker than an allowlist for preventing algorithm confusion attacks.

Consider comparing against Nimbus's constant and/or switching to an allowlist of accepted algorithms:

Proposed fix (allowlist approach)
-        val alg = jwsObject.header.algorithm.name
-        if (alg == "none") {
-            throw SecurityException("Unsecured JWTs (alg: none) are not allowed")
+        val alg = jwsObject.header.algorithm
+        val allowedAlgorithms = setOf(
+            com.nimbusds.jose.JWSAlgorithm.RS256,
+            com.nimbusds.jose.JWSAlgorithm.RS384,
+            com.nimbusds.jose.JWSAlgorithm.RS512,
+            com.nimbusds.jose.JWSAlgorithm.ES256,
+            com.nimbusds.jose.JWSAlgorithm.ES384,
+            com.nimbusds.jose.JWSAlgorithm.ES512,
+        )
+        if (alg !in allowedAlgorithms) {
+            throw SecurityException("Unsupported or disallowed JWT algorithm: ${alg.name}")
         }

An allowlist ensures only vetted algorithms are accepted, which is a stronger security posture than blocking "none" alone.

…on, and correct Gradle exclusion nesting per CodeRabbit

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
Copy link
Contributor

@abhip2565 abhip2565 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the comments.
Can we add appropriate exhaustive tests, for the required changes.

…stic support and general cleanup.

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidatorTest.kt`:
- Around line 32-39: The test `should validate a valid JWT VC successfully` is
time-sensitive because it uses the static fixture `validJwt.txt` whose embedded
exp will expire; update the test to avoid future CI breakage by either updating
the fixture to a far-future `exp` or, better, generate the JWT dynamically in
the test (use `loadSampleJwt` replacement or a new helper) and set the `exp`
claim relative to now (e.g., Instant.now().plusYears(10)) before calling
`validator.validate(vc)` so the token remains valid; modify the test to create
the token at runtime and reference the same validator.validate(...) and
assertion lines unchanged.
- Around line 14-17: The loader function loadSampleJwt currently returns the raw
file contents which may include trailing newlines or whitespace that break
tests; update loadSampleJwt to trim the returned string (e.g., call .trim() on
the String returned by Files.readAllBytes(...) conversion) so the JWT value has
no leading/trailing whitespace before being used by tests or JwtValidator.

In
`@vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifierTest.kt`:
- Around line 26-27: The test reads the JWT file into the vc string without
trimming, so trailing newlines will break the token; update JwtVerifierTest to
trim the file content (e.g., call .trim() on the created String or on vc) after
reading the bytes so the JWT has no leading/trailing whitespace — adjust the
code that sets vc (and any helper like loadSampleJwt if present) to use .trim().
- Around line 43-54: The test JwtVerifierTest::`should return false for tampered
jwt` currently returns a blank mock PublicKey and thus doesn't validate the
signature; update the test to resolve and return the real PublicKey (same source
used in the success test) from the mocked PublicKeyResolverFactory instead of
every { anyConstructed<PublicKeyResolverFactory>().get(any()) } returns mockk(),
then run JwtVerifier().verify(vc) against the tampered jwt and assert the
expected contract (either assertThrows if verify throws on bad sig or rename the
test to reflect exception behavior if verify signals failure via exception, or
change the assertion to assertFalse if verify returns a boolean).

In `@vc-verifier/kotlin/vcverifier/src/test/resources/jwt_vc/validJwt.txt`:
- Line 1: The fixture validJwt.txt contains a hardcoded exp (1801960146 ≈ 2027)
that will make JwtValidatorTest.should validate a valid JWT VC successfully fail
in future; update the test fixture or test to avoid a time-bomb by either (a)
replacing the exp claim in validJwt.txt with a far-future timestamp (e.g., year
2099) or (b) stop using the static validJwt.txt and instead generate the JWT
dynamically in JwtValidatorTest setup (createJwt / JwtBuilder or equivalent used
by the test) so the exp is computed at runtime and never expires during CI.

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
…security check.

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
…nding unit tests.

Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
Signed-off-by: KVD-1302 <vinay@hirekarma.in>
val payload = jwsObject.payload.toJSONObject()
?: throw IllegalArgumentException("JWT payload is not a valid JSON object")

val kid = header.keyID ?: header.jwk?.keyID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we trying to fetch kid from jwk? Isnt jwk itself the key?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, jwk is the public key but we cant blindly trust it. If we parse and use the embedded jwk directly as the verification key, an attacker could generate their own key pair, sign a malicious payload, and embed their own public key in the header. The signature would pass, but it wouldn't be from the real issuer.
To maintain a secure trust anchor (as recommended by CodeRabbit's security analysis), we only use the jwk to extract the kid as a hint. We then pass this kid to our PublicKeyResolverFactory so it fetches the real, authoritative key from the issuer's trusted source (like their DID document or JWKS endpoint).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

OpenID for Verifiable Credential Issuance issuer public key distribution mechanisms

💡 Result:

In OpenID for Verifiable Credential Issuance (OID4VCI), issuer public keys get distributed via a few distinct mechanisms, depending on what the key is used for:

  1. OID4VCI Credential Issuer Metadata (.well-known/openid-credential-issuer)
  • The Wallet retrieves issuer metadata from /.well-known/openid-credential-issuer derived from the credential_issuer URL. The response can be unsigned JSON (application/json) or signed metadata as a JWT (application/jwt). [1]
  • This metadata includes a jwks parameter (a JWKS) containing public keys the Wallet uses as input for key agreement / encrypting credential requests (i.e., to the Credential Endpoint). [1]
  1. OpenID Federation (when used with OID4VCI)
  • Instead of (or in addition to) the regular OID4VCI well-known metadata, issuer metadata can be carried in an OpenID Federation Entity Configuration under the openid_credential_issuer entity type. [2]
  • Issuer public key material can be published in federation metadata via a jwks property under the vc_issuer entity type identifier (per OpenID Federation rules). [2]
  1. Keys for verifying the issued credential (depends on VC format / “issuer signature mechanism”)
    For SD-JWT VC, the SD-JWT VC spec defines how a verifier/recipient obtains the issuer verification key:
  • Web-based metadata if iss is an HTTPS URL: fetch JWT VC Issuer Metadata at /.well-known/jwt-vc-issuer (derived from iss) and use the published keys. [3]
  • DID-based if iss is a DID: resolve the DID Document and use the verification method identified by kid (as a DID URL). [3]
  • X.509-based: use the public key from the certificate chain conveyed via x5c/x5t header parameters (with additional constraints in certain iss forms). [3]

Sources: OID4VCI draft (metadata retrieval; signed metadata; jwks) [1], DIIP/OpenID Federation profiling for OID4VCI (entity configuration + vc_issuer.jwks) [2], SD-JWT VC (issuer key discovery mechanisms; /.well-known/jwt-vc-issuer) [3].


@swatigoel, great question! According to the OpenID for Verifiable Credential Issuance specification and related standards, the issuer's public key distribution depends on what the key is used for:

1. Keys for the Issuance Protocol (OID4VCI)

During the credential issuance flow, the Wallet retrieves issuer metadata from:

{credential_issuer}/.well-known/openid-credential-issuer

This metadata includes a jwks parameter containing public keys used for key agreement and encrypting credential requests to the Credential Endpoint.

2. Keys for Verifying the Issued Credential Signature (what this PR implements)

For JWT-based Verifiable Credentials (like the jwt_vc_json format in this PR), the issuer's verification key is discovered based on the iss claim value:

a) HTTPS-based issuers:

{iss}/.well-known/jwt-vc-issuer

Fetches JWT VC Issuer Metadata containing the issuer's JWKS.

b) DID-based issuers:

  • Resolve the DID Document from the iss claim
  • Use the verification method identified by the kid header parameter

c) X.509-based:

  • Extract the public key from the certificate chain in x5c or x5t JWT header parameters

How This Aligns with the Current Implementation

The JwtVerifier code extracts the kid from the JWT header (Line 18 in the diff) and uses PublicKeyResolverFactory to fetch the verification key from the issuer's trusted source. This factory likely supports:

  • DID resolution (for DID-based issuers)
  • JWKS endpoint discovery (for HTTPS-based issuers)

This approach correctly implements the fail-closed security model mentioned in the PR description—by fetching keys from authoritative sources rather than trusting embedded jwk values in the JWT header, which could be attacker-controlled.

The implementation is spec-compliant for verifying JWT VC signatures! 🎯


🧠 Learnings used
Learnt from: mayuradesh
Repo: inji/vc-verifier PR: 230
File: vc-verifier/kotlin/vcverifier/build.gradle.kts:71-72
Timestamp: 2026-02-04T10:03:45.514Z
Learning: In vc-verifier/kotlin/vcverifier/build.gradle.kts, the dependency `co.nstant.in:cbor:0.9` (libs.cbor) is actively used in MsoMdocCredentialData.kt for MSO-MDOC credential processing (CborDecoder, CborEncoder, and CBOR model classes like Array, Map, ByteString, DataItem, UnicodeString).

Learnt from: jaswanthkumartw
Repo: inji/vc-verifier PR: 216
File: vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/keyResolver/Utils.kt:79-85
Timestamp: 2026-01-09T05:57:56.476Z
Learning: In the file vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/keyResolver/Utils.kt, the constant JWK_KEY_TYPE_EC is defined as "EC", so when this constant is used in pattern matching, it already covers the raw "EC" string value.

Learnt from: mayuradesh
Repo: inji/vc-verifier PR: 230
File: vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/CwtVerifier.kt:122-146
Timestamp: 2026-02-04T10:04:53.460Z
Learning: In CwtVerifier, the issuer (iss) claim in CWT credentials is expected to always be an HTTP(S) URL for JWKS-based public key discovery, not a DID-based key. UnsupportedDidUrl exceptions are not expected in normal operation and can be wrapped as UnknownException for exceptional cases.

Learnt from: jaswanthkumartw
Repo: inji/vc-verifier PR: 216
File: vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/CwtVerifier.kt:22-26
Timestamp: 2026-01-09T06:10:25.708Z
Learning: In the CWT verification flow for the CwtVerifier and CwtValidator classes, validation is performed before verification. CwtValidator.validate() checks structure and claims first, then CwtVerifier.verify() performs signature verification. The verifier does not need to duplicate detailed structure validation since it's already handled by the validator.

Learnt from: jaswanthkumartw
Repo: inji/vc-verifier PR: 216
File: vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/CwtVerifier.kt:95-123
Timestamp: 2026-01-07T10:57:37.322Z
Learning: In the CwtVerifier class, the `PublicKeyResolverFactory().get(issuer)` method throws `PublicKeyNotFoundException` when a key cannot be resolved, rather than returning null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if JWK itself is the key then how this logic will work?
and if an attacker replace the header with their own public key, how will signature pass? Key pair generated by issuer will have different public key, then how will verification pass?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Signed-off-by: KVD-1302 <vinay@hirekarma.in>
Comment on lines +54 to +55
verificationResult.value = VerificationResult(false, "Processing...", "")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to have this as later we are adding additional check of message not being Processing at line#88

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the state to 'Processing...' is required to trigger the Pending/Loading icon immediately.

In the Image composable logic below, we explicitly check verificationMessage != "Processing..." to distinguish between a Failure (which shows R.drawable.error) and an active Processing state (which falls through to else and shows R.drawable.pending). Without this line, the UI wouldn't switch to the pending icon while the background thread is running.

val payload = jwsObject.payload.toJSONObject()
?: throw IllegalArgumentException("JWT payload is not a valid JSON object")

val kid = header.keyID ?: header.jwk?.keyID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signed-off-by: KVD-1302 <vinay@hirekarma.in>
…y, and handle verification failures safely.

Signed-off-by: KVD-1302 <vinay@hirekarma.in>
Signed-off-by: KVD-1302 <vinay@hirekarma.in>
…e mock configurations for safe exception handling.

Signed-off-by: KVD-1302 <vinay@hirekarma.in>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants