diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt index 91a218d..3045ad9 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt @@ -40,14 +40,14 @@ class MainActivity : AppCompatActivity() { } private fun initClicks() { - binding.btnCreateDID.setOnClickListener { - viewModel?.subJwk = DIDService().createJWK() - viewModel?.did = DIDService().createDID(viewModel?.subJwk!!) - - viewModel?.displayText?.value = "Sub JWK : \n ${Gson().toJson(viewModel?.subJwk)}\n\n" - viewModel?.displayText?.value = - "${viewModel?.displayText?.value}Did : ${viewModel?.did}\n\n" - } +// binding.btnCreateDID.setOnClickListener { +// viewModel?.subJwk = DIDService().createJWK() +// viewModel?.did = DIDService().createDID(viewModel?.subJwk!!) +// +// viewModel?.displayText?.value = "Sub JWK : \n ${Gson().toJson(viewModel?.subJwk)}\n\n" +// viewModel?.displayText?.value = +// "${viewModel?.displayText?.value}Did : ${viewModel?.did}\n\n" +// } binding.addCredential.setOnClickListener { if (ContextCompat.checkSelfPermission( @@ -150,7 +150,7 @@ class MainActivity : AppCompatActivity() { viewModel?.displayText?.value = "${viewModel?.displayText?.value}Scanned data : $url\n\n" - viewModel?.issueCredential(url ?: "") + viewModel?.issueCredential(url ?: "",this) } REQUEST_CODE_SCAN_VERIFY -> { diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt index 4f0083c..6507908 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt @@ -1,8 +1,11 @@ package com.ewc.eudiwalletoidcandroid +import android.content.Context import android.net.Uri +import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration @@ -17,15 +20,19 @@ import com.ewc.eudi_wallet_oidc_android.services.sdjwt.SDJWTService import com.ewc.eudi_wallet_oidc_android.services.verification.VerificationService import com.google.gson.Gson import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.ArrayList import java.util.Timer import java.util.TimerTask class MainViewModel : ViewModel() { + private var format: String? = null + private var types: ArrayList = arrayListOf() var isLoading = MutableLiveData(false) var isPreAuthorised = MutableLiveData(false) @@ -40,28 +47,38 @@ class MainViewModel : ViewModel() { private var issuerConfig: IssuerWellKnownConfiguration? = null private var offerCredential: CredentialOffer? = null lateinit var did: String - lateinit var subJwk: ECKey + lateinit var subJwk: JWK init { isLoading.value = false } - fun issueCredential(url: String) { + fun issueCredential(url: String,context:Context) { isLoading.value = true CoroutineScope(Dispatchers.Main).launch { // Resolving credential offer offerCredential = IssueService().resolveCredentialOffer(url) - // Discovery - issuerConfig = - DiscoveryService().getIssuerConfig("${offerCredential?.credentialIssuer}/.well-known/openid-credential-issuer") - - authConfig = - DiscoveryService().getAuthConfig( - "${issuerConfig?.authorizationServer ?: issuerConfig?.issuer}/.well-known/openid-configuration" - ) + val wrappedResponse = DiscoveryService().getIssuerConfig("${offerCredential?.credentialIssuer}/.well-known/openid-credential-issuer") + if (wrappedResponse.issuerConfig != null) { + // Handle successful response + issuerConfig = wrappedResponse.issuerConfig + } else { + displayErrorMessage(context, wrappedResponse.errorResponse?.errorDescription) + return@launch + } + val wrappedAuthResponse = DiscoveryService().getAuthConfig( + "${issuerConfig?.authorizationServer ?: issuerConfig?.issuer}/.well-known/openid-configuration" + ) + if(wrappedAuthResponse.authConfig !=null){ + // Handle successful response + authConfig = wrappedAuthResponse.authConfig + } + else{ + displayErrorMessage(context, wrappedAuthResponse.errorResponse?.errorDescription) + return@launch + } - // Generating code verifier codeVerifier = CodeVerifierService().generateCodeVerifier() withContext(Dispatchers.Main) { @@ -118,18 +135,22 @@ class MainViewModel : ViewModel() { } } + private fun displayErrorMessage(context: Context, errorMessage: String?) { + val messageToShow = errorMessage?.takeIf { it.isNotBlank() } ?: "Unknown error" + Toast.makeText(context, messageToShow, Toast.LENGTH_SHORT).show() + isLoading.value = false + } + private suspend fun getCredential() { - val types = IssueService().getTypesFromCredentialOffer(offerCredential) - val format = IssueService().getFormatFromIssuerConfig(issuerConfig, types.lastOrNull() ?:"") + val credential = IssueService().processCredentialRequest( did, subJwk, - issuerConfig?.credentialIssuer, tokenResponse?.tokenResponse?.cNonce, offerCredential, - issuerConfig?.credentialEndpoint, + issuerConfig, tokenResponse?.tokenResponse?.accessToken, - format?:"jwt_vc" + format ?: "jwt_vc" ) withContext(Dispatchers.Main) { diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index 9f80c07..0d16cd3 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -72,7 +72,7 @@ publishing { register("release") { groupId = "com.github.decentraliseddataexchange" artifactId = "eudi-wallet-oidc-android" - version = "2024.5.1" + version = "2024.6.1" afterEvaluate { from(components["release"]) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt new file mode 100644 index 0000000..ce961f1 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt @@ -0,0 +1,6 @@ +package com.ewc.eudi_wallet_oidc_android + +object CryptographicAlgorithms { + final val ES256 = "ES256" + final val EdDSA = "EdDSA" +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt index c70b506..487c184 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt @@ -44,4 +44,8 @@ data class Jwt( @SerializedName("alg") var alg: ArrayList = arrayListOf() +) +data class WrappedAuthConfigResponse( + var authConfig: AuthorisationServerWellKnownConfiguration? = null, + var errorResponse: ErrorResponse? = null ) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt index 4c039e2..9e35957 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt @@ -1,12 +1,16 @@ package com.ewc.eudi_wallet_oidc_android.models import com.google.gson.annotations.SerializedName +data class CredentialTypeDefinition( + @SerializedName("type") var type: ArrayList? = arrayListOf() +) data class AuthorizationDetails( @SerializedName("type") var type: String? = "openid_credential", - @SerializedName("format") var format: String? = "jwt_vc", + @SerializedName("format") var format: String? = null, @SerializedName("types") var types: ArrayList? = arrayListOf(), - @SerializedName("locations") var locations: ArrayList? = arrayListOf() + @SerializedName("locations") var locations: ArrayList? = arrayListOf(), + @SerializedName("credential_definition") var credentialDefinition: CredentialTypeDefinition? = CredentialTypeDefinition() ) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt index dce9c1b..e270728 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt @@ -6,11 +6,17 @@ import com.google.gson.annotations.SerializedName data class CredentialRequest( @SerializedName("types") var types: ArrayList? = null, + @SerializedName("credential_definition") var credentialDefinition: CredentialDefinition? = null, + @SerializedName("vct") var vct: String? = null, @SerializedName("format") var format: String? = null, @SerializedName("proof") var proof: ProofV3? = null ) +data class CredentialDefinition( + @SerializedName("vct") var vct: String? = null, + @SerializedName("type") var type: ArrayList? = null +) data class ProofV3( @SerializedName("proof_type") var proofType: String? = null, 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/models/InputDescriptors.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt index f0c10cd..547840b 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt @@ -34,6 +34,7 @@ data class Filter( data class Contains( - @SerializedName("const") var const: String? = null - + @SerializedName("const") var const: String? = null, + @SerializedName("pattern") var pattern: String? = null, + @SerializedName("type") var type: String? = null ) \ No newline at end of file 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 a133fad..a9f57bf 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 @@ -17,7 +17,8 @@ data class CredentialsSupported( @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 Display( @@ -33,4 +34,8 @@ data class Image( @SerializedName("uri") var uri: String? = null, @SerializedName("url") var url: String? = null, @SerializedName("alt_text") var altText: String? = null +) +data class WrappedIssuerConfigResponse( + var issuerConfig: IssuerWellKnownConfiguration? = null, + var errorResponse: ErrorResponse? = null ) \ No newline at end of file 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/models/PresentationDefinition.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt index 68ecd15..9a58d4f 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt @@ -5,6 +5,7 @@ import com.google.gson.annotations.SerializedName data class PresentationDefinition( @SerializedName("id") var id: String? = null, + @SerializedName("name") var name: String? = null, @SerializedName("format") var format: Map? = mapOf(), @SerializedName("input_descriptors") var inputDescriptors: ArrayList? = arrayListOf() diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt new file mode 100644 index 0000000..3e2ca67 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt @@ -0,0 +1,10 @@ +package com.ewc.eudi_wallet_oidc_android.models +import com.google.gson.annotations.SerializedName +data class VPTokenResponse( + @SerializedName("location") var location: String? = null, +) + +data class WrappedVpTokenResponse( + var vpTokenResponse: VPTokenResponse? = null, + var errorResponse: ErrorResponse? = 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/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..4578bb9 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt @@ -0,0 +1,34 @@ +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 + + // return if expiry not present in the JWT + if (expirationTime == null){ + return false + } + 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..777e4ee --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt @@ -0,0 +1,260 @@ +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 +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.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 { + + /** + * 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 !=null && kid.startsWith("did:key:z")) { + processJWKFromKID(kid) + } else if ( kid !=null && kid.startsWith("did:ebsi:z")){ + processEbsiJWKFromKID(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") + } + } + + // 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 + * + * @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) + // 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) { + // 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 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() + } + } + + /** + * 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 066261a..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 @@ -1,11 +1,14 @@ package com.ewc.eudi_wallet_oidc_android.services.did +import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms import com.mediaparkpk.base58android.Base58 import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey +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 @@ -73,11 +76,112 @@ class DIDService : DIDServiceInterface { } /** - * Generate JWK of curve Ed25519 + * Create DID according to cryptographicAlgorithm * - * @return JWK + * @param jwk + * @param cryptographicAlgorithm + * @return */ - override fun createED25519JWK(): OctetKeyPair? { + override fun createDID(jwk: JWK, cryptographicAlgorithm: String?): String { + when (cryptographicAlgorithm) { + CryptographicAlgorithms.ES256 -> { + return createES256DID(jwk) + } + + CryptographicAlgorithms.EdDSA -> { + return createEdDSADID((jwk as OctetKeyPair).x) + } + + else -> { + return createES256DID(jwk) + } + } + } + + /** + * Create JWK according to cryptographicAlgorithm + * + * @param seed + * @param cryptographicAlgorithm + * @return + */ + override fun createJWK(seed: String?, cryptographicAlgorithm: String?): JWK { + when (cryptographicAlgorithm) { + CryptographicAlgorithms.ES256 -> { + return createES256JWK(seed) + } + + CryptographicAlgorithms.EdDSA -> { + return createEdDSAJWK(seed) + } + + else -> { + return createES256JWK(seed) + } + } + } + + + /** + * Create ES256 JWK + * + * @param seed + * @return + */ + override fun createES256JWK(seed: String?): JWK { + val keyPairGenerator = KeyPairGenerator.getInstance("EC") + if (seed != null) { + val seedBytes = seed.toByteArray(StandardCharsets.UTF_8) + keyPairGenerator.initialize(256, SecureRandom(seedBytes)) + } else { + keyPairGenerator.initialize(256) + } + val keyPair: KeyPair = keyPairGenerator.generateKeyPair() + + val publicKey = convertToECPublicKey(keyPair.public) + val privateKey = convertToECPrivateKey(keyPair.private) + + val ecKey = ECKey.Builder(Curve.P_256, publicKey) + .privateKey(privateKey).build() + + return ecKey + } + + + /** + * Create ES256 DID + * + * @param jwk + * @return + */ + override fun createES256DID(jwk: JWK): String { + val ecKey = jwk as ECKey + val publicKey = ecKey.toPublicJWK() + + val compactJson = + "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"${publicKey?.x}\",\"y\":\"${publicKey?.y}\"}" + + // UTF-8 encode the string + val encodedBytes: ByteArray? = compactJson.toByteArray(StandardCharsets.UTF_8) + + // Add multiCodec byte + val multiCodecBytes = addMultiCodecByte(encodedBytes) + + // Apply multiBase base58-btc encoding + val multiBaseEncoded = multiBaseEncode(multiCodecBytes!!) + + // Prefix the string with "did:key" + return "did:key:z$multiBaseEncoded" + } + + + /** + * Create ED25519 JWK + * + * @param seed + * @return + */ + override fun createEdDSAJWK(seed: String?): JWK { val jwk = OctetKeyPairGenerator(Curve.Ed25519) .keyID(UUID.randomUUID().toString()) .generate() @@ -91,7 +195,7 @@ class DIDService : DIDServiceInterface { * * @return DID */ - override fun createDidED25519(privateKeyX: Base64URL): String { + override fun createEdDSADID(privateKeyX: Base64URL): String { val startArray = byteArrayOf(0xed.toByte(), 0x01) val newArray = startArray + Base64URL(privateKeyX.toString()).decode() // 3. base58 encode the prefixed public key bytes. @@ -101,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 b40dc01..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 @@ -1,6 +1,8 @@ package com.ewc.eudi_wallet_oidc_android.services.did +import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.util.Base64URL @@ -22,7 +24,65 @@ interface DIDServiceInterface { */ fun createJWK(seed: String? = null): ECKey - fun createED25519JWK(): OctetKeyPair? + /** + * Create DID according to cryptographicAlgorithm + * + * @param jwk + * @param cryptographicAlgorithm + * @return + */ + fun createDID( + jwk: JWK, + cryptographicAlgorithm: String? = CryptographicAlgorithms.ES256): String + + /** + * Create JWK according to cryptographicAlgorithm + * + * @param seed + * @param cryptographicAlgorithm + * @return + */ + fun createJWK( + seed: String? = null, + cryptographicAlgorithm: String? = CryptographicAlgorithms.ES256): JWK + + /** + * Create ES256 JWK + * + * @param seed + * @return + */ + fun createES256JWK(seed: String?): JWK + + /** + * Create ES256 DID + * + * @param jwk + * @return + */ + fun createES256DID(jwk: JWK): String - fun createDidED25519(privateKeyX: Base64URL): String + /** + * Create ED25519 JWK + * + * @param seed + * @return + */ + fun createEdDSAJWK(seed: String?): JWK? + + /** + * Generate DID for the ED25519 + * @param privateKeyX - X value of the ED25519 jwk + * + * @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/discovery/DiscoveryService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryService.kt index 5eed5fc..76a19e6 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryService.kt @@ -1,7 +1,10 @@ package com.ewc.eudi_wallet_oidc_android.services.discovery import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfiguration +import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration +import com.ewc.eudi_wallet_oidc_android.models.WrappedAuthConfigResponse +import com.ewc.eudi_wallet_oidc_android.models.WrappedIssuerConfigResponse import com.ewc.eudi_wallet_oidc_android.services.UriValidationFailed import com.ewc.eudi_wallet_oidc_android.services.UrlUtils import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager @@ -12,31 +15,31 @@ class DiscoveryService : DiscoveryServiceInterface { * To fetch the Issue configuration * * @param credentialIssuerWellKnownURI - * @return IssuerWellKnownConfiguration + * @return WrappedIssuerConfigResponse */ - override suspend fun getIssuerConfig(credentialIssuerWellKnownURI: String?): IssuerWellKnownConfiguration? { + override suspend fun getIssuerConfig(credentialIssuerWellKnownURI: String?): WrappedIssuerConfigResponse { try { UrlUtils.validateUri(credentialIssuerWellKnownURI) val response = ApiManager.api.getService() ?.fetchIssuerConfig("$credentialIssuerWellKnownURI") return if (response?.isSuccessful == true) { - response.body() + WrappedIssuerConfigResponse(issuerConfig = response.body(), errorResponse = null) } else { - null + WrappedIssuerConfigResponse(issuerConfig = null, errorResponse = ErrorResponse(error = response?.code(), errorDescription = response?.message())) } } catch (exc: UriValidationFailed) { - return null + return WrappedIssuerConfigResponse(issuerConfig = null, errorResponse = ErrorResponse(error = null, errorDescription = "URI validation failed")) } } /** - * To fetch the authorisation server configuration + * To fetch the authorization server configuration * * @param authorisationServerWellKnownURI - * @return AuthorisationServerWellKnownConfiguration + * @return WrappedAuthConfigResponse */ - override suspend fun getAuthConfig(authorisationServerWellKnownURI: String?): AuthorisationServerWellKnownConfiguration? { + override suspend fun getAuthConfig(authorisationServerWellKnownURI: String?): WrappedAuthConfigResponse { try { UrlUtils.validateUri(authorisationServerWellKnownURI) @@ -44,12 +47,12 @@ class DiscoveryService : DiscoveryServiceInterface { ApiManager.api.getService() ?.fetchAuthConfig("$authorisationServerWellKnownURI") return if (response?.isSuccessful == true) { - response.body() + WrappedAuthConfigResponse(authConfig = response.body(), errorResponse = null) } else { - null + WrappedAuthConfigResponse(authConfig = null, errorResponse = ErrorResponse(error = response?.code(), errorDescription = response?.message())) } } catch (exc: UriValidationFailed) { - return null + return WrappedAuthConfigResponse(authConfig = null, errorResponse = ErrorResponse(error = null, errorDescription = "URI validation failed")) } } } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt index bc2220f..cddb4b3 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt @@ -2,9 +2,11 @@ package com.ewc.eudi_wallet_oidc_android.services.discovery import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration +import com.ewc.eudi_wallet_oidc_android.models.WrappedAuthConfigResponse +import com.ewc.eudi_wallet_oidc_android.models.WrappedIssuerConfigResponse interface DiscoveryServiceInterface { - suspend fun getIssuerConfig(credentialIssuerWellKnownURI:String?):IssuerWellKnownConfiguration? + suspend fun getIssuerConfig(credentialIssuerWellKnownURI:String?):WrappedIssuerConfigResponse? - suspend fun getAuthConfig(authorisationServerWellKnownURI:String?):AuthorisationServerWellKnownConfiguration? + suspend fun getAuthConfig(authorisationServerWellKnownURI:String?): WrappedAuthConfigResponse? } \ 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 diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index de6f056..c12792a 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -1,12 +1,15 @@ package com.ewc.eudi_wallet_oidc_android.services.issue import android.net.Uri +import android.util.Log import com.ewc.eudi_wallet_oidc_android.models.AuthorizationDetails import com.ewc.eudi_wallet_oidc_android.models.ClientMetaData +import com.ewc.eudi_wallet_oidc_android.models.CredentialDefinition import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV1 import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV2 import com.ewc.eudi_wallet_oidc_android.models.CredentialRequest +import com.ewc.eudi_wallet_oidc_android.models.CredentialTypeDefinition import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.Jwt @@ -14,6 +17,8 @@ import com.ewc.eudi_wallet_oidc_android.models.ProofV3 import com.ewc.eudi_wallet_oidc_android.models.VpFormatsSupported import com.ewc.eudi_wallet_oidc_android.models.WrappedCredentialResponse import com.ewc.eudi_wallet_oidc_android.models.WrappedTokenResponse +import com.ewc.eudi_wallet_oidc_android.services.UriValidationFailed +import com.ewc.eudi_wallet_oidc_android.services.UrlUtils import com.ewc.eudi_wallet_oidc_android.services.codeVerifier.CodeVerifierService import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager import com.google.gson.Gson @@ -21,10 +26,14 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.Ed25519Signer import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject import java.util.Date import java.util.UUID @@ -41,23 +50,28 @@ class IssueService : IssueServiceInterface { */ override suspend fun resolveCredentialOffer(data: String?): CredentialOffer? { if (data.isNullOrBlank()) return null - - val uri = Uri.parse(data) - val credentialOfferUri = uri.getQueryParameter("credential_offer_uri") - if (!credentialOfferUri.isNullOrBlank()) { - val response = ApiManager.api.getService()?.resolveCredentialOffer(credentialOfferUri) - return if (response?.isSuccessful == true) { - response.body() - } else { - null + try { + val uri = Uri.parse(data) + val credentialOfferUri = uri.getQueryParameter("credential_offer_uri") + UrlUtils.validateUri(credentialOfferUri) + if (!credentialOfferUri.isNullOrBlank()) { + val response = + ApiManager.api.getService()?.resolveCredentialOffer(credentialOfferUri) + return if (response?.isSuccessful == true) { + response.body() + } else { + null + } } - } - val credentialOfferString = uri.getQueryParameter("credential_offer") - if (!credentialOfferString.isNullOrBlank()) { - return Gson().fromJson(credentialOfferString, CredentialOffer::class.java) + val credentialOfferString = uri.getQueryParameter("credential_offer") + if (!credentialOfferString.isNullOrBlank()) { + return Gson().fromJson(credentialOfferString, CredentialOffer::class.java) + } + return null + } catch (exc: UriValidationFailed) { + return null } - return null } @@ -83,15 +97,7 @@ class IssueService : IssueServiceInterface { val scope = "openid" val state = UUID.randomUUID().toString() val clientId = did - val authorisationDetails = Gson().toJson( - arrayListOf( - AuthorizationDetails( - types = getTypesFromCredentialOffer(credentialOffer), - locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") - ) - ) - ) - + val authorisationDetails = buildAuthorizationRequest(credentialOffer) val redirectUri = "http://localhost:8080" val nonce = UUID.randomUUID().toString() @@ -142,11 +148,100 @@ class IssueService : IssueServiceInterface { } } + /** + * To process the authorisation request The authorisation request is to + * grant access to the credential endpoint + * + * @param did - DID created for the issuance + * @param subJwk - for singing the requests + * @param credentialOffer - To build the authorisation request + * @param codeVerifier - to build the authorisation request + * @param authorisationEndPoint - to build the authorisation request + * @return String - short-lived authorisation code + */ + override suspend fun processAuthorisationRequest( + did: String?, + subJwk: JWK?, + credentialOffer: CredentialOffer?, + codeVerifier: String, + authorisationEndPoint: String? + ): String? { + val responseType = "code" + val scope = "openid" + val state = UUID.randomUUID().toString() + val clientId = did + val authorisationDetails = buildAuthorizationRequest(credentialOffer) + + val redirectUri = "http://localhost:8080" + val nonce = UUID.randomUUID().toString() + + val codeChallenge = CodeVerifierService().generateCodeChallenge(codeVerifier) + val codeChallengeMethod = "S256" + val clientMetadata = Gson().toJson( + ClientMetaData( + vpFormatsSupported = VpFormatsSupported( + jwtVp = Jwt(arrayListOf("ES256")), jwtVc = Jwt(arrayListOf("ES256")) + ), responseTypesSupported = arrayListOf( + "vp_token", "id_token" + ), authorizationEndpoint = redirectUri + ) + ) + + val response = ApiManager.api.getService()?.processAuthorisationRequest( + authorisationEndPoint ?: "", + mapOf( + "response_type" to responseType, + "scope" to scope, + "state" to state, + "client_id" to (clientId ?: ""), + "authorization_details" to authorisationDetails, + "redirect_uri" to redirectUri, + "nonce" to nonce, + "code_challenge" to (codeChallenge ?: ""), + "code_challenge_method" to codeChallengeMethod, + "client_metadata" to clientMetadata, + "issuer_state" to (credentialOffer?.grants?.authorizationCode?.issuerState ?: "") + ), + ) + if (response?.code() == 502) { + throw Exception("Unexpected error. Please try again.") + } + val location: String? = if (response?.code() == 302) { + if (response.headers()["Location"]?.contains("error") == true || response.headers()["Location"]?.contains("error_description") == true) { + response.headers()["Location"] + } else { + response.headers()["Location"] + } + } else { + null + } + + + + return if(location != null && Uri.parse(location).getQueryParameter("error") != null) { + location + }else if (location != null && Uri.parse(location).getQueryParameter("code") != null + || Uri.parse(location).getQueryParameter("presentation_definition") != null + || (Uri.parse(location).getQueryParameter("request_uri") != null && + Uri.parse(location).getQueryParameter("response_type") == null && + Uri.parse(location).getQueryParameter("state") == null) + ) { + location + } else { + processAuthorisationRequestUsingIdToken( + did = did, + authorisationEndPoint = authorisationEndPoint, + location = location, + subJwk = subJwk + ) + } + } + private suspend fun processAuthorisationRequestUsingIdToken( did: String?, authorisationEndPoint: String?, location: String?, - subJwk: ECKey? + subJwk: JWK? ): String? { val claimsSet = JWTClaimsSet.Builder() @@ -159,15 +254,22 @@ class IssueService : IssueServiceInterface { .build() // Create JWT for ES256K alg - val jwsHeader = JWSHeader.Builder(JWSAlgorithm.ES256).type(JOSEObjectType.JWT) - .keyID("$did#${did?.replace("did:key:", "")}").jwk(subJwk?.toPublicJWK()).build() + val jwsHeader = + JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType.JWT) + .keyID("$did#${did?.replace("did:key:", "")}") + .build() val jwt = SignedJWT( jwsHeader, claimsSet ) // Sign with private EC key - jwt.sign(ECDSASigner(subJwk)) + jwt.sign( + if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner( + subJwk as ECKey + ) + ) val response = ApiManager.api.getService()?.sendIdTokenForCode( url = Uri.parse(location).getQueryParameter("redirect_uri") ?: "", @@ -183,6 +285,45 @@ class IssueService : IssueServiceInterface { } } + private fun buildAuthorizationRequest(credentialOffer: CredentialOffer?):String{ + val gson = Gson() + var credentialDefinitionNeeded = false + try { + val credentialOfferV1 = + gson.fromJson(gson.toJson(credentialOffer), CredentialOfferV1::class.java) + + if (credentialOfferV1?.credentials?.get(0)?.trustFramework == null) + credentialDefinitionNeeded = true + + } catch (e: Exception) { + credentialDefinitionNeeded = true + } + if (credentialDefinitionNeeded) { + return gson.toJson( + arrayListOf( + AuthorizationDetails( + format = "jwt_vc_json", + locations = arrayListOf(credentialOffer?.credentialIssuer ?: ""), + credentialDefinition = CredentialTypeDefinition( + type = getTypesFromCredentialOffer(credentialOffer) + ) + ) + ) + ) + + }else{ + return gson.toJson( + arrayListOf( + AuthorizationDetails( + format = "jwt_vc", + types = getTypesFromCredentialOffer(credentialOffer), + locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") + ) + ) + ) + } + } + /** * To process the token, * @@ -270,7 +411,7 @@ class IssueService : IssueServiceInterface { credentialOffer: CredentialOffer?, credentialIssuerEndPoint: String?, accessToken: String?, - format:String + format: String ): WrappedCredentialResponse? { // Add claims @@ -301,7 +442,7 @@ class IssueService : IssueServiceInterface { val body = CredentialRequest( types = getTypesFromCredentialOffer(credentialOffer), format = format, - ProofV3( + proof = ProofV3( proofType = "jwt", jwt = jwt.serialize() ) @@ -339,6 +480,169 @@ class IssueService : IssueServiceInterface { return credentialResponse } + /** + * To process the credential, credentials can be issued in two ways, + * intime and deferred + * + * If its intime, then we will receive the credential as the response + * If its deferred, then we will get he acceptance token and use this acceptance token to call deferred + * + * @param did + * @param subJwk + * @param nonce + * @param credentialOffer + * @param issuerConfig + * @param accessToken + * @param format + * + * @return credential response + */ + override suspend fun processCredentialRequest( + did: String?, + subJwk: JWK?, + nonce: String?, + credentialOffer: CredentialOffer?, + issuerConfig: IssuerWellKnownConfiguration?, + accessToken: String?, + format: String + ): WrappedCredentialResponse? { + + // Add claims + val claimsSet = JWTClaimsSet + .Builder() + .issueTime(Date()) + .expirationTime(Date(Date().time + 86400)) + .issuer(did) + .audience(issuerConfig?.credentialIssuer ?: "") + .claim("nonce", nonce).build() + + // Add header + val jwsHeader = JWSHeader + .Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("openid4vci-proof+jwt")) + .keyID("$did#${did?.replace("did:key:", "")}") +// .jwk(subJwk?.toPublicJWK()) + .build() + + + // Sign with private EC key + val jwt = SignedJWT( + jwsHeader, claimsSet + ) + jwt.sign( + if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner( + subJwk as ECKey + ) + ) + + // Construct credential request + val body = buildCredentialRequest( + credentialOffer = credentialOffer, + issuerConfig = issuerConfig, + format = format, + jwt = jwt.serialize() + ) + // API call + val response = ApiManager.api.getService()?.getCredential( + issuerConfig?.credentialEndpoint ?: "", + "application/json", + "Bearer $accessToken", + body + ) + + val credentialResponse = when { + (response?.code() ?: 0) >= 400 -> { + try { + WrappedCredentialResponse( + errorResponse = processError(response?.errorBody()?.string()) + ) + } catch (e: Exception) { + null + } + } + + response?.isSuccessful == true -> { + WrappedCredentialResponse( + credentialResponse = response.body() + ) + } + + else -> { + null + } + } + + return credentialResponse + } + + private fun buildCredentialRequest( + credentialOffer: CredentialOffer?, + issuerConfig: IssuerWellKnownConfiguration?, + format: String?, + jwt: String + ): CredentialRequest { + + val gson = Gson() + var credentialDefinitionNeeded = false + try { + val credentialOfferV1 = + gson.fromJson(gson.toJson(credentialOffer), CredentialOfferV1::class.java) + + if (credentialOfferV1?.credentials?.get(0)?.trustFramework == null) + credentialDefinitionNeeded = true + + } catch (e: Exception) { + credentialDefinitionNeeded = true + } + + if (credentialDefinitionNeeded) { + var types: ArrayList? = getTypesFromCredentialOffer(credentialOffer) + when (val data = getTypesFromIssuerConfig( + issuerConfig, + type = if (types?.isNotEmpty() == true) types.last() else "" + )) { + is ArrayList<*> -> { + return CredentialRequest( + credentialDefinition = CredentialDefinition(type = data as ArrayList), + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } + + is String -> { + return CredentialRequest( + vct = data as String, + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } + } + + return CredentialRequest( + credentialDefinition = CredentialDefinition(type = types), + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } else { + return CredentialRequest( + types = getTypesFromCredentialOffer(credentialOffer), + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } + } fun processError(err: String?): ErrorResponse? { // Known possibilities for error: @@ -346,6 +650,7 @@ class IssueService : IssueServiceInterface { // 2. {"error_description": "Validation is failed", } // 3. {"errors": [{ "message": "Validation is failed" }]} // 4. {"error": "Validation is failed"} + // 5. {"detail": "VC token expired"} val jsonObject = try { err?.let { JSONObject(it) } } catch (e: Exception) { @@ -381,6 +686,13 @@ class IssueService : IssueServiceInterface { ) } + jsonObject?.has("detail") == true -> { + ErrorResponse( + error = -1, + errorDescription = jsonObject.getString("detail") + ) + } + else -> { null } @@ -471,6 +783,103 @@ class IssueService : IssueServiceInterface { return format } + /** + * Get types from IssuerWellKnownConfiguration + * + * @param issuerConfig + * @param type + */ + override fun getTypesFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): Any? { + var types: ArrayList = ArrayList() + // Check if issuerConfig is null + if (issuerConfig == null) { + return null + } + try { + val credentialOfferJsonString = Gson().toJson(issuerConfig) + // Check if credentialOfferJsonString is null or empty + if (credentialOfferJsonString.isNullOrEmpty()) { + return null + } + val jsonObject = JSONObject(credentialOfferJsonString) + + val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null + when (credentialsSupported) { + is JSONObject -> { + try { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + val format = + if (credentialSupported.has("format")) credentialSupported.getString("format") else "" + + if (format == "vc+sd-jwt") { + return credentialSupported.getJSONObject("credential_definition") + .getString("vct") + } else { + val typeFromCredentialIssuer: JSONArray = + credentialSupported.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + return types + } + } catch (e: Exception) { + } + } + + is JSONArray -> { + try { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + val format = + if (jsonObject.has("format")) jsonObject.getString("format") else "" + + if (format == "vc+sd-jwt") { + return jsonObject.getJSONObject("credential_definition") + .getString("vct") + } else { + val typeFromCredentialIssuer: JSONArray = + jsonObject.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + return types + } + break + } + } + } + } catch (e: Exception) { + } + } + + else -> { + // Neither JSONObject nor JSONArray + println("Child is neither JSONObject nor JSONArray") + } + } + }catch (e: JSONException){ + Log.e("getTypesFromIssuerConfig", "Error parsing JSON", e) + } + + return types + } + /** * Get types from credential offer * @@ -497,4 +906,70 @@ class IssueService : IssueServiceInterface { return types } + + /** + * Get cryptographicSuits from issuer config + * + * @param issuerConfig + * @param type + * @return + */ + override fun getCryptoFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): ArrayList? { + var types: ArrayList = ArrayList() + val credentialOfferJsonString = Gson().toJson(issuerConfig) + val jsonObject = JSONObject(credentialOfferJsonString) + + val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null + when (credentialsSupported) { + is JSONObject -> { + try { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + val cryptographicSuitsSupported = + credentialSupported.getJSONArray("cryptographic_suites_supported") + for (i in 0 until cryptographicSuitsSupported.length()) { + // Get each JSONObject from the JSONArray + val type: String = cryptographicSuitsSupported.getString(i) + types.add(type) + } + } catch (e: Exception) { + } + } + + is JSONArray -> { + try { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + val cryptographicSuitsSupported = + jsonObject.getJSONArray("cryptographic_suites_supported") + for (i in 0 until cryptographicSuitsSupported.length()) { + // Get each JSONObject from the JSONArray + val type: String = cryptographicSuitsSupported.getString(i) + types.add(type) + } + break + } + } + } + } catch (e: Exception) { + } + } + + else -> { + // Neither JSONObject nor JSONArray + println("Child is neither JSONObject nor JSONArray") + } + } + + return types + } } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt index c49da64..70b0355 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt @@ -5,6 +5,7 @@ import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.WrappedCredentialResponse import com.ewc.eudi_wallet_oidc_android.models.WrappedTokenResponse import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK interface IssueServiceInterface { @@ -36,6 +37,25 @@ interface IssueServiceInterface { authorisationEndPoint: String? ): String? + /** + * To process the authorisation request + * The authorisation request is to grant access to the credential endpoint + * @param did - DID created for the issuance + * @param subJwk - for singing the requests + * @param credentialOffer - To build the authorisation request + * @param codeVerifier - to build the authorisation request + * @param authorisationEndPoint - to build the authorisation request + * + * @return String - Uri with query parameter code with value short-lived authorisation code + */ + suspend fun processAuthorisationRequest( + did: String?, + subJwk: JWK?, + credentialOffer: CredentialOffer?, + codeVerifier: String, + authorisationEndPoint: String? + ): String? + /** * To process the token, * @@ -74,6 +94,7 @@ interface IssueServiceInterface { * @param credentialOffer * @param credentialIssuerEndPoint * @param accessToken + * @param format * * @return credential response */ @@ -88,6 +109,33 @@ interface IssueServiceInterface { format: String ): WrappedCredentialResponse? + /** + * To process the credential, credentials can be issued in two ways, + * intime and deferred + * + * If its intime, then we will receive the credential as the response + * If its deferred, then we will get he acceptance token and use this acceptance token to call deferred + * + * @param did + * @param subJwk + * @param nonce + * @param credentialOffer + * @param issuerConfig + * @param accessToken + * @param format + * + * @return credential response + */ + suspend fun processCredentialRequest( + did: String?, + subJwk: JWK?, + nonce: String?, + credentialOffer: CredentialOffer?, + issuerConfig: IssuerWellKnownConfiguration?, + accessToken: String?, + format: String + ): WrappedCredentialResponse? + /** * For issuance of the deferred credential. * @param acceptanceToken - token which we got from credential request @@ -120,4 +168,28 @@ interface IssueServiceInterface { fun getTypesFromCredentialOffer( credentialOffer: CredentialOffer? ): ArrayList + + /** + * Get types from Issuer Config + * + * @param issuerConfig + * @param type + * @return + */ + fun getTypesFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): Any? + + /** + * Get types from Issuer Config + * + * @param issuerConfig + * @param type + * @return + */ + fun getCryptoFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): ArrayList? } \ No newline at end of file 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 diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt index efbbf6d..c3c5bd8 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt @@ -13,7 +13,10 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.Ed25519Signer import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import org.json.JSONArray @@ -106,6 +109,64 @@ class SDJWTService : SDJWTServiceInterface { } } + + /** + * Create SDJWT R + * + * @param credential + * @param presentationRequest + * @param subJwk + * @return + */ + override fun createSDJWTR( + credential: String?, + presentationRequest: PresentationRequest, + subJwk: JWK + ): String? { + try { + val presentationDefinition = + VerificationService().processPresentationDefinition(presentationRequest.presentationDefinition) + val processedCredentialWithRequiredDisclosures = + processDisclosuresWithPresentationDefinition( + credential, + presentationDefinition + ) + if (presentationDefinition.format?.containsKey("kb_jwt") == true) { + val iat = Date() + + val claimsSet = JWTClaimsSet.Builder() + .audience(presentationRequest.clientId) + .issueTime(iat) + .claim("nonce", UUID.randomUUID().toString()) + .claim( + "sd_hash", + SDJWTService().calculateSHA256Hash( + processedCredentialWithRequiredDisclosures + ) + ) + .build() + + // Create JWT for ES256K alg + val jwsHeader = JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("kb_jwt")) + .build() + + val jwt = SignedJWT( + jwsHeader, + claimsSet + ) + + // Sign with private EC key + jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk) else ECDSASigner(subJwk as ECKey)) + return "${processedCredentialWithRequiredDisclosures}~${jwt.serialize()}" + } + + return processedCredentialWithRequiredDisclosures + } catch (e: Exception) { + throw IllegalArgumentException("Error creating SD-JWT-R", e) + } + } + /** * Processes disclosures based on the provided credential and presentation definition. * @@ -134,10 +195,14 @@ class SDJWTService : SDJWTServiceInterface { // Filter disclosures based on requested parameters disclosures?.forEach { disclosure -> - val list = - JSONArray(Base64.decode(disclosure, Base64.URL_SAFE).toString(charset("UTF-8"))) - if (list.length() >= 2 && requestedParams.contains(list.optString(1))) { - issuedJwt = "$issuedJwt~$disclosure" + try { + val list = + JSONArray(Base64.decode(disclosure, Base64.URL_SAFE).toString(charset("UTF-8"))) + if (list.length() >= 2 && requestedParams.contains(list.optString(1))) { + issuedJwt = "$issuedJwt~$disclosure" + } + } catch (e: Exception) { + } } diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt index eb56ba5..86f56e0 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt @@ -4,6 +4,7 @@ import com.ewc.eudi_wallet_oidc_android.models.PresentationDefinition import com.ewc.eudi_wallet_oidc_android.models.PresentationRequest import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK interface SDJWTServiceInterface { @@ -15,6 +16,12 @@ interface SDJWTServiceInterface { subJwk: ECKey ): String? + fun createSDJWTR( + credential: String?, + presentationRequest: PresentationRequest, + subJwk: JWK + ): String? + fun processDisclosuresWithPresentationDefinition( credential: String?, presentationDefinition: PresentationDefinition diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt index 13f03dc..8e8ace2 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt @@ -3,10 +3,14 @@ package com.ewc.eudi_wallet_oidc_android.services.verification import android.net.Uri import android.util.Base64 import com.ewc.eudi_wallet_oidc_android.models.DescriptorMap +import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse import com.ewc.eudi_wallet_oidc_android.models.PathNested import com.ewc.eudi_wallet_oidc_android.models.PresentationDefinition import com.ewc.eudi_wallet_oidc_android.models.PresentationRequest import com.ewc.eudi_wallet_oidc_android.models.PresentationSubmission +import com.ewc.eudi_wallet_oidc_android.models.VPTokenResponse +import com.ewc.eudi_wallet_oidc_android.models.WrappedVpTokenResponse +import com.ewc.eudi_wallet_oidc_android.services.issue.IssueService import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager import com.ewc.eudi_wallet_oidc_android.services.sdjwt.SDJWTService import com.github.decentraliseddataexchange.presentationexchangesdk.PresentationExchange @@ -17,12 +21,16 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.Ed25519Signer import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.shaded.json.parser.ParseException import com.nimbusds.jwt.JWT import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT +import org.json.JSONArray import org.json.JSONObject import java.util.Date import java.util.UUID @@ -190,6 +198,96 @@ class VerificationService : VerificationServiceInterface { } } + /** + * Send VP token + * + * @param did + * @param subJwk + * @param presentationRequest + * @param credentialList + * @return + */ + override suspend fun sendVPToken( + did: String?, + subJwk: JWK?, + presentationRequest: PresentationRequest, + credentialList: List + ): WrappedVpTokenResponse? { + val iat = Date() + val jti = "urn:uuid:${UUID.randomUUID()}" + val claimsSet = JWTClaimsSet.Builder() + .audience(presentationRequest.clientId) + .issueTime(iat) + .expirationTime(Date(iat.time + 600000)) + .issuer(did) + .jwtID(jti) + .notBeforeTime(iat) + .claim("nonce", presentationRequest.nonce) + .subject(did) + .claim( + "vp", com.nimbusds.jose.shaded.json.JSONObject( + hashMapOf( + "@context" to listOf("https://www.w3.org/2018/credentials/v1"), + "holder" to did, + "id" to jti, + "type" to listOf("VerifiablePresentation"), + "verifiableCredential" to credentialList + ) + ) + ).build() + + // Create JWT for ES256K alg + val jwsHeader = + JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("JWT")) + .keyID("$did#${did?.replace("did:key:", "")}") + .jwk(subJwk?.toPublicJWK()) + .build() + + val jwt = SignedJWT( + jwsHeader, + claimsSet + ) + + // Sign with private EC key + jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk) else ECDSASigner(subJwk as ECKey)) + + val response = ApiManager.api.getService()?.sendVPToken( + presentationRequest.responseUri ?: presentationRequest.redirectUri ?: "", + mapOf( + "vp_token" to jwt.serialize(), + "presentation_submission" to Gson().toJson( + createPresentationSubmission( + presentationRequest + ) + ), + "state" to (presentationRequest.state ?: "") + ) + ) + + val tokenResponse = when { + response?.code() == 302 || response?.code() == 200 -> { + WrappedVpTokenResponse( + vpTokenResponse = VPTokenResponse( + location = response.headers()["Location"] + ?: "https://www.example.com?code=1" + ) + ) + } + + (response?.code() ?: 0) >= 400 -> { + WrappedVpTokenResponse( + errorResponse = IssueService().processError(response?.errorBody()?.string()) + ) + } + + else -> { + null + } + } + return tokenResponse + } + /** * Returns all the list of credentials matching for all input descriptors */ @@ -197,24 +295,43 @@ class VerificationService : VerificationServiceInterface { allCredentialList: List, presentationDefinition: PresentationDefinition ): List> { - //list of credentials matched for all input descriptors + val response: MutableList> = mutableListOf() + val pex = PresentationExchange() - val credentialList: ArrayList = arrayListOf() - for (item in allCredentialList) { - if (presentationDefinition.inputDescriptors?.get(0)?.constraints?.limitDisclosure != null && item?.contains( - "~" - ) == true - ) - credentialList.add(item) - else if (presentationDefinition.inputDescriptors?.get(0)?.constraints?.limitDisclosure == null && item?.contains( - "~" - ) != true - ) - credentialList.add(item) + presentationDefinition.inputDescriptors?.forEach { inputDescriptors -> + val credentialList = splitCredentialsBySdJWT(allCredentialList, inputDescriptors.constraints?.limitDisclosure != null) + val processedCredentials = processCredentialsToJsonString(credentialList) + val filteredCredentialList: MutableList = mutableListOf() + val inputDescriptor = Gson().toJson(inputDescriptors) + + val matches: List = + pex.matchCredentials(inputDescriptor, processedCredentials) + + for (match in matches) { + filteredCredentialList.add(credentialList[match.index] ?: "") + } + + response.add(filteredCredentialList) } - val response: MutableList> = mutableListOf() + return response + } + + private fun splitCredentialsBySdJWT( + allCredentials: List, + isSdJwt: Boolean + ): ArrayList { + val filteredCredentials: ArrayList = arrayListOf() + for (item in allCredentials) { + if (isSdJwt && item?.contains("~") == true) + filteredCredentials.add(item) + else if (!isSdJwt && item?.contains("~") == false) + filteredCredentials.add(item) + } + return filteredCredentials + } + private fun processCredentialsToJsonString(credentialList: ArrayList):List{ var processedCredentials: List = mutableListOf() for (cred in credentialList) { val split = cred?.split(".") @@ -237,24 +354,7 @@ class VerificationService : VerificationServiceInterface { else json.toString() ) } - - val pex = PresentationExchange() - - presentationDefinition.inputDescriptors?.forEach { inputDescriptors -> - val filteredCredentialList: MutableList = mutableListOf() - val inputDescriptor = Gson().toJson(inputDescriptors) - - val matches: List = - pex.matchCredentials(inputDescriptor, processedCredentials) - - for (match in matches) { - filteredCredentialList.add(credentialList[match.index] ?: "") - } - - response.add(filteredCredentialList) - } - - return response + return processedCredentials } /** @@ -302,11 +402,11 @@ class VerificationService : VerificationServiceInterface { val descriptor = DescriptorMap( id = inputDescriptors.id, path = "$", - format = presentationDefinition.format?.keys?.first(), + format = presentationDefinition.format?.keys?.firstOrNull() ?: "jwt_vp", pathNested = PathNested( id = inputDescriptors.id, format = "jwt_vc", - path = "$.verifiableCredential[$index]" + path = "$.vp.verifiableCredential[$index]" ) ) descriptorMap.add(descriptor) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt index 4ebce70..419eddf 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt @@ -2,11 +2,13 @@ package com.ewc.eudi_wallet_oidc_android.services.verification import com.ewc.eudi_wallet_oidc_android.models.PresentationDefinition import com.ewc.eudi_wallet_oidc_android.models.PresentationRequest +import com.ewc.eudi_wallet_oidc_android.models.WrappedVpTokenResponse import com.github.decentraliseddataexchange.presentationexchangesdk.PresentationExchange import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.google.gson.Gson import com.google.gson.internal.LinkedTreeMap import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK interface VerificationServiceInterface { @@ -38,6 +40,23 @@ interface VerificationServiceInterface { credentialList: List ): String? + + /** + * Send VP token + * + * @param did + * @param subJwk + * @param presentationRequest + * @param credentialList + * @return + */ + suspend fun sendVPToken( + did: String?, + subJwk: JWK?, + presentationRequest: PresentationRequest, + credentialList: List + ): WrappedVpTokenResponse? + /** * To filter the credential using the input descriptors * @param credentialList - list of all issued credentials