diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt index e8de412..9149151 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt @@ -1,7 +1,6 @@ package com.ewc.eudi_wallet_oidc_android.models import com.ewc.eudi_wallet_oidc_android.models.v1.IssuerWellKnownConfigurationV1 -import com.ewc.eudi_wallet_oidc_android.models.v2.Claims import com.ewc.eudi_wallet_oidc_android.models.v2.IssuerWellKnownConfigurationV2 import com.google.gson.annotations.SerializedName @@ -54,7 +53,6 @@ data class CredentialDetails( @SerializedName("doctype") val doctype: String? = null, @SerializedName("credential_definition") val credentialDefinition: Any? = null, @SerializedName("vct") var vct: String? = null, - @SerializedName("claims") var claims: Claims? = null ) data class CredentialDisplay( @SerializedName("name") var name: String? = null, diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/v2/IssuerWellKnownConfigurationV2.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/v2/IssuerWellKnownConfigurationV2.kt index 65fb266..2ea2e4d 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/v2/IssuerWellKnownConfigurationV2.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/v2/IssuerWellKnownConfigurationV2.kt @@ -1,5 +1,8 @@ package com.ewc.eudi_wallet_oidc_android.models.v2 +import com.ewc.eudi_wallet_oidc_android.models.CredentialDisplay +import com.ewc.eudi_wallet_oidc_android.models.Display +import com.ewc.eudi_wallet_oidc_android.models.Image import com.ewc.eudi_wallet_oidc_android.models.TrustFramework import com.google.gson.annotations.SerializedName @@ -18,7 +21,7 @@ data class CredentialsSupportedV2( @SerializedName("format") var format: String? = null, @SerializedName("types") var types: ArrayList = arrayListOf(), @SerializedName("trust_framework") var trustFramework: TrustFramework? = TrustFramework(), - @SerializedName("display") var display: ArrayList = arrayListOf(), + @SerializedName("display") var display: ArrayList = arrayListOf(), @SerializedName("cryptographic_suites_supported") var cryptographicSuitesSupported: ArrayList = arrayListOf(), ) data class CredentialDetailsV2( @@ -26,44 +29,8 @@ data class CredentialDetailsV2( @SerializedName("scope") val scope: String? = null, @SerializedName("cryptographic_binding_methods_supported") val cryptographicBindingMethodsSupported: List? = null, @SerializedName("credential_signing_alg_values_supported") val credentialSigningAlgValuesSupported: List? = null, - @SerializedName("display") val display: List? = null, + @SerializedName("display") val display: List? = null, @SerializedName("doctype") val doctype: String? = null, @SerializedName("credential_definition") val credentialDefinition: Any? = null, @SerializedName("vct") var vct: String? = null, - @SerializedName("claims") var claims: Claims? = null -) -data class Claims( - @SerializedName("given_name") var givenName: List? = null, - @SerializedName("last_name") var lastName: List? = null -) - -data class DisplayInfoV2( - @SerializedName("name") var name: String, - @SerializedName("location") var location: String? = null, - @SerializedName("locale") var locale: String, - @SerializedName("description") var description: String? = null -) -data class CredentialDisplayV2( - @SerializedName("name") var name: String? = null, - @SerializedName("locale") var locale: String? = null, - @SerializedName("logo") var logo: ImageV2? = null, - @SerializedName("description") var description: String? = null, - @SerializedName("background_color") var backgroundColor: String? = null, - @SerializedName("background_image") var backgroundImage: ImageV2? = null, - @SerializedName("text_color") var textColor: String? = null -) - -data class DisplayV2( - @SerializedName("name") var name: String? = null, - @SerializedName("location") var location: String? = null, - @SerializedName("locale") var locale: String? = null, - @SerializedName("cover") var cover: ImageV2? = ImageV2(), - @SerializedName("logo") var logo: ImageV2? = ImageV2(), - @SerializedName("description") var description: String? = null -) - -data class ImageV2( - @SerializedName("uri") var uri: String? = null, - @SerializedName("url") var url: String? = null, - @SerializedName("alt_text") var altText: String? = null ) \ 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 index c917269..8cc3bab 100644 --- 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 @@ -1,26 +1,17 @@ package com.ewc.eudi_wallet_oidc_android.services.credentialValidation -import com.ewc.eudi_wallet_oidc_android.models.DIDDocument -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.credentialValidation.publicKeyExtraction.ProcessEbsiJWKFromKID +import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction.ProcessJWKFromJwksUri +import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction.ProcessJWKFromKID +import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction.ProcessKeyJWKFromKID +import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction.ProcessWebJWKFromKID import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException -import com.ewc.eudi_wallet_oidc_android.services.did.DIDService -import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager -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 import com.nimbusds.jose.JOSEException import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSVerifier -import retrofit2.Response -import java.text.ParseException class SignatureValidator { @@ -34,140 +25,42 @@ class SignatureValidator { * * Throws SignatureException if validation fails */ - @Throws(SignatureException::class) - suspend fun validateSignature(jwt: String?,jwksUri:String?=null): Boolean { + @Throws(SignatureException::class) // Declare that this function might throw a SignatureException + suspend fun validateSignature(jwt: String?, jwksUri: String? = null): Boolean { // Suspended function to validate JWT signature, allowing optional JWKS URI return try { - jwt?.let { - val jwsObject = JWSObject.parse(jwt) - val header = jwsObject.header - val kid = header.keyID - val algorithm = jwsObject.header.algorithm - - // Check the format of kid and process accordingly - val response = if ( kid !=null && kid.startsWith("did:key:z")) { - processJWKFromKID(kid,algorithm) - } else if ( kid !=null && kid.startsWith("did:ebsi:z")){ - processEbsiJWKFromKID(kid) + jwt?.let { // Null-safe check: proceed if jwt is not null + val jwsObject = JWSObject.parse(jwt) // Parse the JWT string to a JWSObject + val header = jwsObject.header // Retrieve the header from the parsed JWT + val kid = header.keyID // Extract the 'kid' (key ID) from the JWT header + val algorithm = jwsObject.header.algorithm // Extract the algorithm used in the JWT header + + // Check the format of 'kid' and process it accordingly + val response = if (kid != null && kid.startsWith("did:key:z")) { // Check if 'kid' starts with "did:key:z" + ProcessKeyJWKFromKID().processKeyJWKFromKID(kid, algorithm) // Process as a key-based DID JWK (Decentralized Identifier) + } else if (kid != null && kid.startsWith("did:ebsi:z")) { // Check if 'kid' starts with "did:ebsi:z" + ProcessEbsiJWKFromKID().processEbsiJWKFromKID(kid) // Process as an EBSI-based DID JWK + } else if (kid != null && kid.startsWith("did:jwk")) { // Check if 'kid' starts with "did:jwk" + ProcessJWKFromKID().processJWKFromKID(kid) // Process as a JWK (JSON Web Key) DID + } else if(kid !=null && kid.startsWith("did:web")){ + ProcessWebJWKFromKID().processWebJWKFromKID(kid) } else { - processJWKFromJwksUri(kid,jwksUri) + ProcessJWKFromJwksUri().processJWKFromJwksUri(kid, jwksUri) // Process JWK using the provided JWKS URI } + + // If a valid JWK response is received, verify the JWT signature if (response != null) { - val isSignatureValid = verifyJwtSignature(jwt, response.toJSONString()) - isSignatureValid + val isSignatureValid = verifyJwtSignature(jwt, response.toJSONString()) // Verify signature using the JWK response + isSignatureValid // Return the result of the signature verification } else { - throw SignatureException("Invalid signature") + throw SignatureException("Invalid signature") // Throw an exception if JWK response is null } - } ?: throw SignatureException("Invalid signature") - } catch (e: IllegalArgumentException) { - throw SignatureException("Invalid signature") - } - } - - // This function fetches and processes the DID Document, and extracts the P-256 JWK if present. - private suspend fun processEbsiJWKFromKID(did: String?): ECKey? { - return try { - // Validate DID format - if (did == null || !did.startsWith("did:ebsi:z")) { - throw IllegalArgumentException("Invalid DID format") - } - - val service = ApiManager.api.getService() ?: throw IllegalStateException("API service not available") - - // First attempt with conformance API - var response: Response? = service.ebsiDIDResolver( - "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/$did" - ) - - // If the conformance API call is not successful, attempt the pilot API - if (response == null || !response.isSuccessful) { - response = service.ebsiDIDResolver( - "https://api-pilot.ebsi.eu/did-registry/v5/identifiers/$did" - ) - } - - // If the second API call also fails, throw an exception - if (response == null || !response.isSuccessful) { - throw IllegalStateException("Failed to fetch DID Document from both endpoints") - } - - // Extract the P-256 JWK from the JSON response - val didDocument = response.body() ?: throw IllegalStateException("Empty response body") - extractJWK(didDocument) - } catch (e: Exception) { - // Handle errors, possibly log or rethrow as needed - println("Error processing DID: ${e.message}") - null - } - } - - private fun extractJWK(didDocument: DIDDocument): ECKey? { - return try { - // Iterate through each verification method - for (method in didDocument.verificationMethods) { - try { - val publicKeyJwk = method.publicKeyJwk - - // Check if 'crv' is 'P-256' - if (publicKeyJwk.crv == "P-256") { - // Convert the JSON JWK to a Nimbus JWK - val jwk = JWK.parse( - """{ - "kty": "${publicKeyJwk.kty}", - "crv": "${publicKeyJwk.crv}", - "x": "${publicKeyJwk.x}", - "y": "${publicKeyJwk.y}" - }""" - ) - if (jwk is ECKey) { - return jwk - } - } - } catch (e: ParseException) { - // Handle JWK parsing exceptions - println("Error parsing JWK: ${e.message}") - } - } - - // Return null if no matching JWK is found - null - } catch (e: Exception) { - // Handle any unexpected exceptions - println("Error processing DID document: ${e.message}") - null - } - } - - /** - * Processes a JWK from a DID - * - * @param did - * @return - */ - private fun processJWKFromKID(did: String?, algorithm: JWSAlgorithm): 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,algorithm) - } 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) + } ?: throw SignatureException("Invalid signature") // Handle the case where JWT is null + } catch (e: IllegalArgumentException) { // Catch any IllegalArgumentException thrown during the process + throw SignatureException("Invalid signature") // Wrap the exception into a SignatureException } - } - /** * Verifies the signature of a JWT using a JWK provided as JSON. * @@ -178,97 +71,39 @@ class SignatureValidator { @Throws(IllegalArgumentException::class) private fun verifyJwtSignature(jwt: String, jwkJson: String): Boolean { try { - // Parse the JWK from JSON + // Parse the JWK (JSON Web Key) from the JSON string val jwk = ECKey.parse(jwkJson) - // Create a JWS object from the JWT string + // Parse the JWT string into a JWS object (JSON Web Signature) val jwsObject = JWSObject.parse(jwt) - // Create a JWS verifier with the EC key -// val verifier = ECDSAVerifier(jwk) - // Get the algorithm from the JWS header + // Get the algorithm specified in the JWS header val algorithm = jwsObject.header.algorithm - // Create the appropriate verifier based on the algorithm + // Create the appropriate verifier based on the algorithm used in the JWS header val verifier: JWSVerifier = when (algorithm) { + // For ES256 (ECDSA using P-256 curve and SHA-256), create an ECDSAVerifier JWSAlgorithm.ES256 -> ECDSAVerifier(jwk.toECKey()) + + // For ES384 (ECDSA using P-384 curve and SHA-384), create an ECDSAVerifier JWSAlgorithm.ES384 -> ECDSAVerifier(jwk.toECKey()) + + // For ES512 (ECDSA using P-521 curve and SHA-512), create an ECDSAVerifier JWSAlgorithm.ES512 -> ECDSAVerifier(jwk.toECKey()) + + // Throw an exception if the algorithm is unsupported else -> throw JOSEException("Unsupported JWS algorithm $algorithm") } - // Verify the JWS signature + + // Verify the signature of the JWS using the appropriate verifier return jwsObject.verify(verifier) } catch (e: Exception) { - // Handle exceptions appropriately + // Print the stack trace for debugging purposes 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 The JwkKey object. - * @return The JWK object or null if jwkKey is null. - */ - private fun convertToJWK(jwkKey: JwkKey?): JWK? { - return jwkKey?.let { - val curve = when (it.crv) { - "P-256" -> Curve.P_256 - "P-384" -> Curve.P_384 - "P-521" -> Curve.P_521 - else -> throw IllegalArgumentException("Unsupported curve: ${it.crv}") - } - - ECKey.Builder(curve, Base64URL.from(it.x), Base64URL.from(it.y)) - .keyID(it.kid) - .build() + // Throw an IllegalArgumentException if signature verification fails or any error occurs + throw IllegalArgumentException("Invalid signature") } } - /** - * 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/credentialValidation/publicKeyExtraction/ProcessEbsiJWKFromKID.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessEbsiJWKFromKID.kt new file mode 100644 index 0000000..47db240 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessEbsiJWKFromKID.kt @@ -0,0 +1,87 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction + +import com.ewc.eudi_wallet_oidc_android.models.DIDDocument +import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import retrofit2.Response +import java.text.ParseException + +class ProcessEbsiJWKFromKID { + // This function fetches and processes the DID Document, and extracts the P-256 JWK if present. + suspend fun processEbsiJWKFromKID(did: String?): ECKey? { + return try { + // Validate DID format + if (did == null || !did.startsWith("did:ebsi:z")) { + throw IllegalArgumentException("Invalid DID format") + } + + val service = ApiManager.api.getService() + ?: throw IllegalStateException("API service not available") + + // First attempt with conformance API + var response: Response? = service.ebsiDIDResolver( + "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/$did" + ) + + // If the conformance API call is not successful, attempt the pilot API + if (response == null || !response.isSuccessful) { + response = service.ebsiDIDResolver( + "https://api-pilot.ebsi.eu/did-registry/v5/identifiers/$did" + ) + } + + // If the second API call also fails, throw an exception + if (response == null || !response.isSuccessful) { + throw IllegalStateException("Failed to fetch DID Document from both endpoints") + } + + // Extract the P-256 JWK from the JSON response + val didDocument = response.body() ?: throw IllegalStateException("Empty response body") + extractJWK(didDocument) + } catch (e: Exception) { + // Handle errors, possibly log or rethrow as needed + println("Error processing DID: ${e.message}") + null + } + } + + private fun extractJWK(didDocument: DIDDocument): ECKey? { + return try { + // Iterate through each verification method + for (method in didDocument.verificationMethods) { + try { + val publicKeyJwk = method.publicKeyJwk + + // Check if 'crv' is 'P-256' + if (publicKeyJwk.crv == "P-256") { + // Convert the JSON JWK to a Nimbus JWK + val jwk = JWK.parse( + """{ + "kty": "${publicKeyJwk.kty}", + "crv": "${publicKeyJwk.crv}", + "x": "${publicKeyJwk.x}", + "y": "${publicKeyJwk.y}" + }""" + ) + if (jwk is ECKey) { + return jwk + } + } + } catch (e: ParseException) { + // Handle JWK parsing exceptions + println("Error parsing JWK: ${e.message}") + } + } + + // Return null if no matching JWK is found + null + } catch (e: Exception) { + // Handle any unexpected exceptions + println("Error processing DID document: ${e.message}") + null + } + } + + +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessJWKFromJwksUri.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessJWKFromJwksUri.kt new file mode 100644 index 0000000..8dfc08f --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessJWKFromJwksUri.kt @@ -0,0 +1,79 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction + +import com.ewc.eudi_wallet_oidc_android.models.JwkKey +import com.ewc.eudi_wallet_oidc_android.models.JwksResponse +import com.google.gson.Gson +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 ProcessJWKFromJwksUri { + /** + * Processes a JWK from a JWKS (JSON Web Key Set) URI. + * + * @param kid + * @param jwksUri + * @return + */ + suspend fun processJWKFromJwksUri(kid: String?, jwksUri:String?): JWK? { + if (jwksUri != null) { + val jwkKey = fetchJwks(jwksUri =jwksUri, kid = kid) + return convertToJWK(jwkKey) + } + return null + } + /** + * Fetches a JwkKey object from a specified JWKS (JSON Web Key Set) URI. + * + * @param jwksUri + * @param kid + * @return + */ + 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 + } + } + } + /** + * Converts a JwkKey object to a JWK (JSON Web Key). + * + * @param jwkKey The JwkKey object. + * @return The JWK object or null if jwkKey is null. + */ + fun convertToJWK(jwkKey: JwkKey?): JWK? { + return jwkKey?.let { + val curve = when (it.crv) { + "P-256" -> Curve.P_256 + "P-384" -> Curve.P_384 + "P-521" -> Curve.P_521 + else -> throw IllegalArgumentException("Unsupported curve: ${it.crv}") + } + + ECKey.Builder(curve, Base64URL.from(it.x), Base64URL.from(it.y)) + .keyID(it.kid) + .build() + } + } + +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessJWKFromKID.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessJWKFromKID.kt new file mode 100644 index 0000000..1706324 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessJWKFromKID.kt @@ -0,0 +1,29 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction + +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.util.Base64URL + +class ProcessJWKFromKID { + fun processJWKFromKID(kid: String): JWK? { + return try { + // Ensure the kid starts with "did:jwk" + if (kid.startsWith("did:jwk:")) { + // Extract the Base64URL-encoded JWK + val jwkString = kid.removePrefix("did:jwk:") + + // Decode the JWK JSON + val decodedJwkJson = String(Base64URL(jwkString).decode()) + + // Parse the JWK using Nimbus library + val jwk = JWK.parse(decodedJwkJson) + jwk + } else { + throw IllegalArgumentException("Invalid DID format for JWK") + } + } catch (e: Exception) { + // Handle any exceptions that occur during parsing + println("Error processing JWK from KID: ${e.message}") + null + } + } +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessKeyJWKFromKID.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessKeyJWKFromKID.kt new file mode 100644 index 0000000..2ba83c1 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessKeyJWKFromKID.kt @@ -0,0 +1,37 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction + +import com.ewc.eudi_wallet_oidc_android.services.did.DIDService +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.jwk.JWK + +class ProcessKeyJWKFromKID { + /** + * Processes a JWK from a DID + * + * @param did + * @return + */ + + fun processKeyJWKFromKID(did: String?, algorithm: JWSAlgorithm): 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,algorithm) + } 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) + } + + } +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessWebJWKFromKID.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessWebJWKFromKID.kt new file mode 100644 index 0000000..c22219e --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/publicKeyExtraction/ProcessWebJWKFromKID.kt @@ -0,0 +1,107 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction + +import com.google.gson.Gson +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 java.net.URL +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ProcessWebJWKFromKID { + + /** + * Processes a JWK from a Web DID and retrieves it using the constructed JWKS URI. + * + * @param did The DID (Decentralized Identifier) starting with "did:web". + * @return The JWK object if found; otherwise, null. + */ + suspend fun processWebJWKFromKID(did: String?): JWK? { + // Ensure that the DID is valid and starts with "did:web" + if (did == null || !did.startsWith("did:web:")) { + return null // Return null if DID is null or not a web DID + } + + // Remove the "did:web:" prefix and split by "#" to handle the path + val cleanedDid = did.removePrefix("did:web:") + val pathAndFragment = cleanedDid.split("#")[0] // Get the part before the fragment if it exists + + // Construct the JWKS URI by replacing ":" with "/" and appending "/did.json" + val parts = pathAndFragment.split(":") + val domain = parts.first() // The first part is the domain + val path = parts.drop(1).joinToString("/") // Join remaining parts with "/" + + // Construct the full JWKS URI + val jwksUri = if (path.isNotEmpty()) { + "https://$domain/$path/did.json" // Use the path if available + } else { + "https://$domain/.well-known/did.json" // Use the default path if no additional path is given + } + val jwkKey =fetchJwks(jwksUri =jwksUri, kid = null) + val converted = convertToJWK(jwkKey) + + + // Fetch the JWK using the constructed JWKS URI + return converted// Call your existing fetchJwks function + } + +} +// Define your data classes +data class JwksResponse( + val verificationMethod: List +) + +data class JwkKey( + val id: String, + val type: String, + val controller: String, + val publicKeyJwk: PublicKeyJwk +) + +data class PublicKeyJwk( + val kty: String, + val use: String, + val crv: String, + val x: String, + val y: String +) + +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.verificationMethod.firstOrNull { it.publicKeyJwk.use == "sig" } + + // If no "sig" key is found, find by kid + if (jwkKey == null && kid != null) { + jwkKey = jwksResponse.verificationMethod.firstOrNull { it.id == kid } + } + return@withContext jwkKey + } catch (e: Exception) { + println(e.toString()) + return@withContext null + } + } +} +fun convertToJWK(jwkKey: JwkKey?): JWK? { + return jwkKey?.let { + val publicKeyJwk = it.publicKeyJwk // Access the nested publicKeyJwk + + val curve = when (publicKeyJwk.crv) { + "P-256" -> Curve.P_256 + "P-384" -> Curve.P_384 + "P-521" -> Curve.P_521 + else -> throw IllegalArgumentException("Unsupported curve: ${publicKeyJwk.crv}") + } + + ECKey.Builder(curve, Base64URL.from(publicKeyJwk.x), Base64URL.from(publicKeyJwk.y)) + .keyID(it.id) + .build() + } +} \ No newline at end of file