-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix #42 #43: Credential validation for expiry and signature
- Loading branch information
Showing
10 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.ewc.eudi_wallet_oidc_android.models | ||
|
||
// Data class representing a JSON Web Key (JWK). | ||
data class JwkKey( | ||
val kty: String, | ||
val kid: String, | ||
val crv: String, | ||
val x: String, | ||
val y: String, | ||
val use: String | ||
) |
4 changes: 4 additions & 0 deletions
4
...wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package com.ewc.eudi_wallet_oidc_android.models | ||
|
||
// Data class representing a response containing a list of JSON Web Keys (JWKs). | ||
data class JwksResponse(val keys: List<JwkKey>) |
36 changes: 36 additions & 0 deletions
36
...ava/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation | ||
|
||
import com.ewc.eudi_wallet_oidc_android.services.exceptions.ExpiryException | ||
import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException | ||
|
||
class CredentialValidator:CredentialValidatorInterface { | ||
|
||
/** | ||
* Validates a JWT credential by checking its expiration and signature. | ||
* | ||
* @param jwt | ||
* @param jwksUri | ||
* @return | ||
* | ||
* Returns true if the JWT is valid; otherwise, throws IllegalArgumentException with appropriate messages. | ||
*/ | ||
@Throws(IllegalArgumentException::class) | ||
override suspend fun validateCredential(jwt: String?, jwksUri: String?): Boolean { | ||
try { | ||
// Check if the JWT has expired | ||
ExpiryValidator().isJwtExpired(jwt = jwt) | ||
|
||
// Validate the JWT signature using the provided JWKS URI | ||
SignatureValidator().validateSignature(jwt = jwt, jwksUri = jwksUri) | ||
|
||
// If both checks pass, return true indicating the credential is valid | ||
return true | ||
} catch (expiryException: ExpiryException) { | ||
// Throw IllegalArgumentException if JWT is expired | ||
throw IllegalArgumentException("JWT token expired") | ||
} catch (signatureException: SignatureException) { | ||
// Throw IllegalArgumentException if JWT signature is invalid | ||
throw IllegalArgumentException("JWT signature invalid") | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...wc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation | ||
|
||
interface CredentialValidatorInterface { | ||
/** | ||
* Validates a JWT credential by checking its expiration and signature. | ||
* | ||
* @param jwt | ||
* @param jwksUri | ||
* @return | ||
* | ||
* Returns true if the JWT is valid; otherwise, throws IllegalArgumentException with appropriate messages. | ||
*/ | ||
@Throws(IllegalArgumentException::class) | ||
suspend fun validateCredential(jwt: String?,jwksUri:String?):Boolean | ||
} |
28 changes: 28 additions & 0 deletions
28
...in/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation | ||
|
||
import com.ewc.eudi_wallet_oidc_android.services.exceptions.ExpiryException | ||
import com.nimbusds.jwt.SignedJWT | ||
import java.text.ParseException | ||
import java.util.Date | ||
|
||
class ExpiryValidator { | ||
/** | ||
* Checks if the provided JWT (JSON Web Token) has expired. | ||
* | ||
* @param jwt | ||
* @return | ||
* | ||
* Returns true if the JWT is expired, false otherwise. | ||
* Throws ExpiryException if parsing the JWT or checking expiration encounters errors. | ||
*/ | ||
@Throws(ExpiryException::class) | ||
fun isJwtExpired(jwt: String?): Boolean { | ||
return try { | ||
val signedJWT = SignedJWT.parse(jwt) | ||
val expirationTime = signedJWT.jwtClaimsSet.expirationTime | ||
expirationTime?.before(Date()) ?: throw ExpiryException("JWT token expired") | ||
} catch (e: ParseException) { | ||
throw ExpiryException("JWT token expired", e) | ||
} | ||
} | ||
} |
175 changes: 175 additions & 0 deletions
175
...java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation | ||
|
||
import com.ewc.eudi_wallet_oidc_android.models.JwkKey | ||
import com.ewc.eudi_wallet_oidc_android.models.JwksResponse | ||
import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException | ||
import com.ewc.eudi_wallet_oidc_android.services.did.DIDService | ||
import com.google.gson.Gson | ||
import com.nimbusds.jose.JWSObject | ||
import com.nimbusds.jose.crypto.ECDSAVerifier | ||
import com.nimbusds.jose.jwk.Curve | ||
import com.nimbusds.jose.jwk.ECKey | ||
import com.nimbusds.jose.jwk.JWK | ||
import com.nimbusds.jose.util.Base64URL | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import java.net.URL | ||
|
||
class SignatureValidator { | ||
|
||
/** | ||
* Validates the signature of a JWT using a JWK fetched either from | ||
* Kid or JWKS URI present in Authorisation configuration. | ||
* | ||
* @param jwt | ||
* @param jwksUri | ||
* @return | ||
* | ||
* Throws SignatureException if validation fails | ||
*/ | ||
@Throws(SignatureException::class) | ||
suspend fun validateSignature(jwt: String?,jwksUri:String?=null): Boolean { | ||
return try { | ||
jwt?.let { | ||
val jwsObject = JWSObject.parse(jwt) | ||
val header = jwsObject.header | ||
val kid = header.keyID | ||
// Check the format of kid and process accordingly | ||
val response = if (kid.startsWith("did:key:z")) { | ||
processJWKFromKID(kid) | ||
} else { | ||
processJWKFromJwksUri(kid,jwksUri) | ||
} | ||
if (response != null) { | ||
val isSignatureValid = verifyJwtSignature(jwt, response.toJSONString()) | ||
isSignatureValid | ||
} else { | ||
throw SignatureException("Invalid signature") | ||
} | ||
} ?: throw SignatureException("Invalid signature") | ||
} catch (e: IllegalArgumentException) { | ||
throw SignatureException("Invalid signature") | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Processes a JWK from a DID | ||
* | ||
* @param did | ||
* @return | ||
*/ | ||
private fun processJWKFromKID(did: String?): JWK? { | ||
try { | ||
if (did == null || !did.startsWith("did:key:z")) { | ||
throw IllegalArgumentException("Invalid DID format") | ||
} | ||
// Extract the multiBaseEncoded part | ||
val multiBaseEncoded = if (did.contains("#")) { | ||
did.split("#")[0].substring("did:key:z".length) | ||
} else { | ||
did.substring("did:key:z".length) | ||
} | ||
// Call convertDIDToJWK function from DIDService | ||
return DIDService().convertDIDToJWK(multiBaseEncoded) | ||
} catch (e: IllegalArgumentException) { | ||
// Handle specific exception if needed | ||
throw IllegalArgumentException("Error converting DID to JWK", e) | ||
} catch (e: Exception) { | ||
// Handle other exceptions | ||
throw IllegalArgumentException("Error converting DID to JWK", e) | ||
} | ||
|
||
} | ||
|
||
|
||
/** | ||
* Verifies the signature of a JWT using a JWK provided as JSON. | ||
* | ||
* @param jwt | ||
* @param jwkJson | ||
* @return | ||
*/ | ||
@Throws(IllegalArgumentException::class) | ||
private fun verifyJwtSignature(jwt: String, jwkJson: String): Boolean { | ||
try { | ||
// Parse the JWK from JSON | ||
val jwk = ECKey.parse(jwkJson) | ||
|
||
// Create a JWS object from the JWT string | ||
val jwsObject = JWSObject.parse(jwt) | ||
|
||
// Create a JWS verifier with the EC key | ||
val verifier = ECDSAVerifier(jwk) | ||
|
||
// Verify the JWS signature | ||
return jwsObject.verify(verifier) | ||
} catch (e: Exception) { | ||
// Handle exceptions appropriately | ||
e.printStackTrace() | ||
throw IllegalArgumentException("Invalid signature") | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Processes a JWK from a JWKS (JSON Web Key Set) URI. | ||
* | ||
* @param kid | ||
* @param jwksUri | ||
* @return | ||
*/ | ||
private suspend fun processJWKFromJwksUri(kid: String?, jwksUri:String?): JWK? { | ||
if (jwksUri != null) { | ||
val jwkKey = fetchJwks(jwksUri =jwksUri, kid = kid) | ||
return convertToJWK(jwkKey) | ||
} | ||
return null | ||
} | ||
|
||
|
||
/** | ||
* Converts a JwkKey object to a JWK (JSON Web Key). | ||
* | ||
* @param jwkKey | ||
* @return | ||
*/ | ||
private fun convertToJWK(jwkKey: JwkKey?): JWK? { | ||
return jwkKey?.let { | ||
ECKey.Builder(Curve.P_256, Base64URL.from(it.x), Base64URL.from(it.y)) | ||
.keyID(it.kid) | ||
.build() | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Fetches a JwkKey object from a specified JWKS (JSON Web Key Set) URI. | ||
* | ||
* @param jwksUri | ||
* @param kid | ||
* @return | ||
*/ | ||
private suspend fun fetchJwks(jwksUri: String, kid: String?): JwkKey? { | ||
return withContext(Dispatchers.IO) { | ||
try { | ||
val url = URL(jwksUri) | ||
val json = url.readText() | ||
// Parse JSON into JwksResponse object | ||
val jwksResponse = Gson().fromJson(json, JwksResponse::class.java) | ||
|
||
// Find the JWK with "use" = "sig" | ||
var jwkKey = jwksResponse.keys.firstOrNull { it.use == "sig" } | ||
|
||
// If no "sig" key is found, find by kid | ||
if (jwkKey == null && kid != null) { | ||
jwkKey = jwksResponse.keys.firstOrNull { it.kid == kid } | ||
} | ||
return@withContext jwkKey | ||
} catch (e: Exception) { | ||
println(e.toString()) | ||
return@withContext null | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
...oid/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package com.ewc.eudi_wallet_oidc_android.services.exceptions | ||
|
||
class ExpiryException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause) |
3 changes: 3 additions & 0 deletions
3
.../src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package com.ewc.eudi_wallet_oidc_android.services.exceptions | ||
|
||
class SignatureException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause) |