INJIMOB-3751 - Add JWT VC Support in VC Verifier#234
INJIMOB-3751 - Add JWT VC Support in VC Verifier#234KVD-1302 wants to merge 15 commits intoinji:developfrom
Conversation
…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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
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 forexpandnbfchecks.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.
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Outdated
Show resolved
Hide resolved
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
… and restore library build integrity Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
There was a problem hiding this comment.
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:excludecalls are onConfiguration, notResolutionStrategy— misleading nesting.The
exclude(group, module)method belongs to theConfigurationreceiver, notResolutionStrategy. Kotlin's implicit receiver resolution makes this compile and work, but placing them insideresolutionStrategy { }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.
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
…on, and correct Gradle exclusion nesting per CodeRabbit Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
abhip2565
left a comment
There was a problem hiding this comment.
Please address the comments.
Can we add appropriate exhaustive tests, for the required changes.
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Outdated
Show resolved
Hide resolved
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
…stic support and general cleanup. Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
There was a problem hiding this comment.
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.
...r/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidatorTest.kt
Show resolved
Hide resolved
...r/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidatorTest.kt
Show resolved
Hide resolved
...ier/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifierTest.kt
Outdated
Show resolved
Hide resolved
...ier/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifierTest.kt
Show resolved
Hide resolved
vc-verifier/kotlin/vcverifier/src/test/resources/jwt_vc/validJwt.txt
Outdated
Show resolved
Hide resolved
Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Show resolved
Hide resolved
…security check. Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
…nding unit tests. Signed-off-by: Vinay <vinayadattakavuluri@gmail.com>
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
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 |
There was a problem hiding this comment.
Why are we trying to fetch kid from jwk? Isnt jwk itself the key?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
@coderabbitai, as per spec https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html
how the issuer's public key will be shared
There was a problem hiding this comment.
🧩 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:
- OID4VCI Credential Issuer Metadata (
.well-known/openid-credential-issuer)
- The Wallet retrieves issuer metadata from
/.well-known/openid-credential-issuerderived from thecredential_issuerURL. The response can be unsigned JSON (application/json) or signed metadata as a JWT (application/jwt). [1] - This metadata includes a
jwksparameter (a JWKS) containing public keys the Wallet uses as input for key agreement / encrypting credential requests (i.e., to the Credential Endpoint). [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_issuerentity type. [2] - Issuer public key material can be published in federation metadata via a
jwksproperty under thevc_issuerentity type identifier (per OpenID Federation rules). [2]
- 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
issis an HTTPS URL: fetch JWT VC Issuer Metadata at/.well-known/jwt-vc-issuer(derived fromiss) and use the published keys. [3] - DID-based if
issis a DID: resolve the DID Document and use the verification method identified bykid(as a DID URL). [3] - X.509-based: use the public key from the certificate chain conveyed via
x5c/x5theader parameters (with additional constraints in certainissforms). [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
issclaim - Use the verification method identified by the
kidheader parameter
c) X.509-based:
- Extract the public key from the certificate chain in
x5corx5tJWT 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
...ier/src/test/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifierTest.kt
Show resolved
Hide resolved
Signed-off-by: KVD-1302 <vinay@hirekarma.in>
vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt
Outdated
Show resolved
Hide resolved
| verificationResult.value = VerificationResult(false, "Processing...", "") | ||
|
|
There was a problem hiding this comment.
any reason to have this as later we are adding additional check of message not being Processing at line#88
There was a problem hiding this comment.
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.
vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt
Outdated
Show resolved
Hide resolved
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Show resolved
Hide resolved
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Show resolved
Hide resolved
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Show resolved
Hide resolved
...ifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/validator/JwtValidator.kt
Outdated
Show resolved
Hide resolved
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
| val payload = jwsObject.payload.toJSONObject() | ||
| ?: throw IllegalArgumentException("JWT payload is not a valid JSON object") | ||
|
|
||
| val kid = header.keyID ?: header.jwk?.keyID |
There was a problem hiding this comment.
@coderabbitai, as per spec https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html
how the issuer's public key will be shared
...erifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/JwtVerifier.kt
Outdated
Show resolved
Hide resolved
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>
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:
Example App & Build Environment:
Unit Tests:
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.
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.
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.
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.
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.
Video
feat-3751-v2.mp4
Summary by CodeRabbit
New Features
Tests