From 4b85c60fb66e9498003c70c6ffaa7a8a52d532e8 Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 24 Jul 2024 01:35:11 +0530 Subject: [PATCH] Fix #44: Signature validation support of DID starting with did:ebsi --- .../models/DIDDocument.kt | 54 ++++++++++ .../SignatureValidator.kt | 101 ++++++++++++++++-- .../services/network/ApiServices.kt | 4 + 3 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt new file mode 100644 index 0000000..2d7c653 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt @@ -0,0 +1,54 @@ +package com.ewc.eudi_wallet_oidc_android.models + +import com.google.gson.annotations.SerializedName + +data class DIDDocument( + @SerializedName("@context") + val context: List, + + @SerializedName("id") + val id: String, + + @SerializedName("controller") + val controller: List, + + @SerializedName("verificationMethod") + val verificationMethods: List, + + @SerializedName("authentication") + val authentication: List, + + @SerializedName("assertionMethod") + val assertionMethods: List, + + @SerializedName("capabilityInvocation") + val capabilityInvocations: List +) + +data class VerificationMethod( + @SerializedName("id") + val id: String, + + @SerializedName("type") + val type: String, + + @SerializedName("controller") + val controller: String, + + @SerializedName("publicKeyJwk") + val publicKeyJwk: PublicKeyJwk +) + +data class PublicKeyJwk( + @SerializedName("kty") + val kty: String, + + @SerializedName("crv") + val crv: String, + + @SerializedName("x") + val x: String, + + @SerializedName("y") + val y: String +) \ 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 380b0e3..777e4ee 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,9 +1,11 @@ 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.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 @@ -14,6 +16,13 @@ import com.nimbusds.jose.util.Base64URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.URL +import com.google.gson.JsonArray +import com.google.gson.JsonParser +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,10 +43,14 @@ class SignatureValidator { 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")) { + val response = if ( kid !=null && kid.startsWith("did:key:z")) { processJWKFromKID(kid) - } else { + } else if ( kid !=null && kid.startsWith("did:ebsi:z")){ + processEbsiJWKFromKID(kid) + } + else { processJWKFromJwksUri(kid,jwksUri) } if (response != null) { @@ -52,6 +65,64 @@ class SignatureValidator { } } + // 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") + } + + // Fetch the DID document from the API + val response:Response = ApiManager.api.getService()?.ebsiDIDResolver( + "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/$did" + ) ?: throw IllegalStateException("API service not available") + + // 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 @@ -100,8 +171,17 @@ class SignatureValidator { val jwsObject = JWSObject.parse(jwt) // Create a JWS verifier with the EC key - val verifier = ECDSAVerifier(jwk) +// val verifier = ECDSAVerifier(jwk) + // Get the algorithm from the JWS header + val algorithm = jwsObject.header.algorithm + // Create the appropriate verifier based on the algorithm + val verifier: JWSVerifier = when (algorithm) { + JWSAlgorithm.ES256 -> ECDSAVerifier(jwk.toECKey()) + JWSAlgorithm.ES384 -> ECDSAVerifier(jwk.toECKey()) + JWSAlgorithm.ES512 -> ECDSAVerifier(jwk.toECKey()) + else -> throw JOSEException("Unsupported JWS algorithm $algorithm") + } // Verify the JWS signature return jwsObject.verify(verifier) } catch (e: Exception) { @@ -127,22 +207,27 @@ class SignatureValidator { return null } - /** * Converts a JwkKey object to a JWK (JSON Web Key). * - * @param jwkKey - * @return + * @param jwkKey The JwkKey object. + * @return The JWK object or null if jwkKey is null. */ private fun convertToJWK(jwkKey: JwkKey?): JWK? { return jwkKey?.let { - ECKey.Builder(Curve.P_256, Base64URL.from(it.x), Base64URL.from(it.y)) + 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() } } - /** * Fetches a JwkKey object from a specified JWKS (JSON Web Key Set) URI. * diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt index 7c7b488..9b0e3e9 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt @@ -4,6 +4,7 @@ import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfi import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.CredentialRequest import com.ewc.eudi_wallet_oidc_android.models.CredentialResponse +import com.ewc.eudi_wallet_oidc_android.models.DIDDocument import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.TokenResponse import okhttp3.ResponseBody @@ -74,4 +75,7 @@ interface ApiService { @Url url: String, @FieldMap map: Map ): Response + + @GET + suspend fun ebsiDIDResolver(@Url url: String): Response } \ No newline at end of file