From 5827aac93188068cd1f1dc84b22633e9fff972bf Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 17 Jul 2024 21:32:06 +0530 Subject: [PATCH] Fix #42 #43: Credential validation for expiry and signature --- .../eudi_wallet_oidc_android/models/JwkKey.kt | 11 ++ .../models/JwksResponse.kt | 4 + .../CredentialValidator.kt | 36 ++++ .../CredentialValidatorInterface.kt | 15 ++ .../credentialValidation/ExpiryValidator.kt | 28 +++ .../SignatureValidator.kt | 175 ++++++++++++++++++ .../services/did/DIDService.kt | 36 ++++ .../services/did/DIDServiceInterface.kt | 8 + .../services/exceptions/ExpiryException.kt | 3 + .../services/exceptions/SignatureException.kt | 3 + 10 files changed, 319 insertions(+) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt new file mode 100644 index 0000000..5db5238 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt @@ -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 +) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt new file mode 100644 index 0000000..18d061f --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt @@ -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) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt new file mode 100644 index 0000000..52398a7 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt @@ -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") + } + } +} diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt new file mode 100644 index 0000000..e3965ec --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt @@ -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 +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt new file mode 100644 index 0000000..4fee784 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt new file mode 100644 index 0000000..380b0e3 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt index 37c4c2e..15cb61e 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt @@ -8,6 +8,7 @@ import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator import com.nimbusds.jose.util.Base64URL +import com.nimbusds.jose.util.JSONObjectUtils import java.nio.charset.StandardCharsets import java.security.KeyFactory import java.security.KeyPair @@ -204,6 +205,41 @@ class DIDService : DIDServiceInterface { return encoded } + /** + * Converts a DID string to a JWK (JSON Web Key). + * @param did - Decentralized Identifier (DID) string + * @return JWK object + * @throws IllegalArgumentException if the DID format is invalid, decoding fails, or JSON parsing errors occur + */ + override fun convertDIDToJWK(did: String): JWK { + val multiCodecBytes = try { + Base58.decode(did) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Base58 decoding failed", e) + } + + // Check the length of the decoded bytes + if (multiCodecBytes.size <= 3) { + throw IllegalArgumentException("Decoded bytes are too short to contain valid JSON") + } + + // Decode JSON content + val compactJson = + String(multiCodecBytes.copyOfRange(3, multiCodecBytes.size), StandardCharsets.UTF_8) + + // Parse JSON to retrieve x and y values + val jsonObject = JSONObjectUtils.parse(compactJson) + val x = jsonObject.get("x") as String + val y = jsonObject.get("y") as String + + // Create ECKey using Curve.P_256 (or appropriate curve) + val ecKey = ECKey.Builder(Curve.P_256, Base64URL.from(x), Base64URL.from(y)) + .build() + + // Return as JWK + return ecKey + } + /** * Convert the PrivateKey to ECPrivateKey * @param privateKey diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt index 7963283..b4a664c 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt @@ -77,4 +77,12 @@ interface DIDServiceInterface { * @return DID */ fun createEdDSADID(privateKeyX: Base64URL): String + + /** + * Converts a DID string to a JWK (JSON Web Key). + * @param did - Decentralized Identifier (DID) string + * @return JWK object + * @throws IllegalArgumentException if the DID format is invalid or conversion fails + */ + fun convertDIDToJWK(did:String):JWK } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt new file mode 100644 index 0000000..065e249 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt @@ -0,0 +1,3 @@ +package com.ewc.eudi_wallet_oidc_android.services.exceptions + +class ExpiryException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt new file mode 100644 index 0000000..e947a62 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt @@ -0,0 +1,3 @@ +package com.ewc.eudi_wallet_oidc_android.services.exceptions + +class SignatureException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause) \ No newline at end of file