Skip to content

Commit

Permalink
Fix #42 #43: Credential validation for expiry and signature
Browse files Browse the repository at this point in the history
  • Loading branch information
josmilan committed Jul 17, 2024
1 parent e9f7614 commit 5827aac
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ewc.eudi_wallet_oidc_android.models

// Data class representing a JSON Web Key (JWK).
data class JwkKey(
val kty: String,
val kid: String,
val crv: String,
val x: String,
val y: String,
val use: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.ewc.eudi_wallet_oidc_android.models

// Data class representing a response containing a list of JSON Web Keys (JWKs).
data class JwksResponse(val keys: List<JwkKey>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation

import com.ewc.eudi_wallet_oidc_android.services.exceptions.ExpiryException
import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException

class CredentialValidator:CredentialValidatorInterface {

/**
* Validates a JWT credential by checking its expiration and signature.
*
* @param jwt
* @param jwksUri
* @return
*
* Returns true if the JWT is valid; otherwise, throws IllegalArgumentException with appropriate messages.
*/
@Throws(IllegalArgumentException::class)
override suspend fun validateCredential(jwt: String?, jwksUri: String?): Boolean {
try {
// Check if the JWT has expired
ExpiryValidator().isJwtExpired(jwt = jwt)

// Validate the JWT signature using the provided JWKS URI
SignatureValidator().validateSignature(jwt = jwt, jwksUri = jwksUri)

// If both checks pass, return true indicating the credential is valid
return true
} catch (expiryException: ExpiryException) {
// Throw IllegalArgumentException if JWT is expired
throw IllegalArgumentException("JWT token expired")
} catch (signatureException: SignatureException) {
// Throw IllegalArgumentException if JWT signature is invalid
throw IllegalArgumentException("JWT signature invalid")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation

interface CredentialValidatorInterface {
/**
* Validates a JWT credential by checking its expiration and signature.
*
* @param jwt
* @param jwksUri
* @return
*
* Returns true if the JWT is valid; otherwise, throws IllegalArgumentException with appropriate messages.
*/
@Throws(IllegalArgumentException::class)
suspend fun validateCredential(jwt: String?,jwksUri:String?):Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation

import com.ewc.eudi_wallet_oidc_android.services.exceptions.ExpiryException
import com.nimbusds.jwt.SignedJWT
import java.text.ParseException
import java.util.Date

class ExpiryValidator {
/**
* Checks if the provided JWT (JSON Web Token) has expired.
*
* @param jwt
* @return
*
* Returns true if the JWT is expired, false otherwise.
* Throws ExpiryException if parsing the JWT or checking expiration encounters errors.
*/
@Throws(ExpiryException::class)
fun isJwtExpired(jwt: String?): Boolean {
return try {
val signedJWT = SignedJWT.parse(jwt)
val expirationTime = signedJWT.jwtClaimsSet.expirationTime
expirationTime?.before(Date()) ?: throw ExpiryException("JWT token expired")
} catch (e: ParseException) {
throw ExpiryException("JWT token expired", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.ewc.eudi_wallet_oidc_android.services.credentialValidation

import com.ewc.eudi_wallet_oidc_android.models.JwkKey
import com.ewc.eudi_wallet_oidc_android.models.JwksResponse
import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException
import com.ewc.eudi_wallet_oidc_android.services.did.DIDService
import com.google.gson.Gson
import com.nimbusds.jose.JWSObject
import com.nimbusds.jose.crypto.ECDSAVerifier
import com.nimbusds.jose.jwk.Curve
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.JWK
import com.nimbusds.jose.util.Base64URL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.URL

class SignatureValidator {

/**
* Validates the signature of a JWT using a JWK fetched either from
* Kid or JWKS URI present in Authorisation configuration.
*
* @param jwt
* @param jwksUri
* @return
*
* Throws SignatureException if validation fails
*/
@Throws(SignatureException::class)
suspend fun validateSignature(jwt: String?,jwksUri:String?=null): Boolean {
return try {
jwt?.let {
val jwsObject = JWSObject.parse(jwt)
val header = jwsObject.header
val kid = header.keyID
// Check the format of kid and process accordingly
val response = if (kid.startsWith("did:key:z")) {
processJWKFromKID(kid)
} else {
processJWKFromJwksUri(kid,jwksUri)
}
if (response != null) {
val isSignatureValid = verifyJwtSignature(jwt, response.toJSONString())
isSignatureValid
} else {
throw SignatureException("Invalid signature")
}
} ?: throw SignatureException("Invalid signature")
} catch (e: IllegalArgumentException) {
throw SignatureException("Invalid signature")
}
}


/**
* Processes a JWK from a DID
*
* @param did
* @return
*/
private fun processJWKFromKID(did: String?): JWK? {
try {
if (did == null || !did.startsWith("did:key:z")) {
throw IllegalArgumentException("Invalid DID format")
}
// Extract the multiBaseEncoded part
val multiBaseEncoded = if (did.contains("#")) {
did.split("#")[0].substring("did:key:z".length)
} else {
did.substring("did:key:z".length)
}
// Call convertDIDToJWK function from DIDService
return DIDService().convertDIDToJWK(multiBaseEncoded)
} catch (e: IllegalArgumentException) {
// Handle specific exception if needed
throw IllegalArgumentException("Error converting DID to JWK", e)
} catch (e: Exception) {
// Handle other exceptions
throw IllegalArgumentException("Error converting DID to JWK", e)
}

}


/**
* Verifies the signature of a JWT using a JWK provided as JSON.
*
* @param jwt
* @param jwkJson
* @return
*/
@Throws(IllegalArgumentException::class)
private fun verifyJwtSignature(jwt: String, jwkJson: String): Boolean {
try {
// Parse the JWK from JSON
val jwk = ECKey.parse(jwkJson)

// Create a JWS object from the JWT string
val jwsObject = JWSObject.parse(jwt)

// Create a JWS verifier with the EC key
val verifier = ECDSAVerifier(jwk)

// Verify the JWS signature
return jwsObject.verify(verifier)
} catch (e: Exception) {
// Handle exceptions appropriately
e.printStackTrace()
throw IllegalArgumentException("Invalid signature")
}
}


/**
* Processes a JWK from a JWKS (JSON Web Key Set) URI.
*
* @param kid
* @param jwksUri
* @return
*/
private suspend fun processJWKFromJwksUri(kid: String?, jwksUri:String?): JWK? {
if (jwksUri != null) {
val jwkKey = fetchJwks(jwksUri =jwksUri, kid = kid)
return convertToJWK(jwkKey)
}
return null
}


/**
* Converts a JwkKey object to a JWK (JSON Web Key).
*
* @param jwkKey
* @return
*/
private fun convertToJWK(jwkKey: JwkKey?): JWK? {
return jwkKey?.let {
ECKey.Builder(Curve.P_256, Base64URL.from(it.x), Base64URL.from(it.y))
.keyID(it.kid)
.build()
}
}


/**
* Fetches a JwkKey object from a specified JWKS (JSON Web Key Set) URI.
*
* @param jwksUri
* @param kid
* @return
*/
private suspend fun fetchJwks(jwksUri: String, kid: String?): JwkKey? {
return withContext(Dispatchers.IO) {
try {
val url = URL(jwksUri)
val json = url.readText()
// Parse JSON into JwksResponse object
val jwksResponse = Gson().fromJson(json, JwksResponse::class.java)

// Find the JWK with "use" = "sig"
var jwkKey = jwksResponse.keys.firstOrNull { it.use == "sig" }

// If no "sig" key is found, find by kid
if (jwkKey == null && kid != null) {
jwkKey = jwksResponse.keys.firstOrNull { it.kid == kid }
}
return@withContext jwkKey
} catch (e: Exception) {
println(e.toString())
return@withContext null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.nimbusds.jose.jwk.JWK
import com.nimbusds.jose.jwk.OctetKeyPair
import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator
import com.nimbusds.jose.util.Base64URL
import com.nimbusds.jose.util.JSONObjectUtils
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.KeyPair
Expand Down Expand Up @@ -204,6 +205,41 @@ class DIDService : DIDServiceInterface {
return encoded
}

/**
* Converts a DID string to a JWK (JSON Web Key).
* @param did - Decentralized Identifier (DID) string
* @return JWK object
* @throws IllegalArgumentException if the DID format is invalid, decoding fails, or JSON parsing errors occur
*/
override fun convertDIDToJWK(did: String): JWK {
val multiCodecBytes = try {
Base58.decode(did)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException("Base58 decoding failed", e)
}

// Check the length of the decoded bytes
if (multiCodecBytes.size <= 3) {
throw IllegalArgumentException("Decoded bytes are too short to contain valid JSON")
}

// Decode JSON content
val compactJson =
String(multiCodecBytes.copyOfRange(3, multiCodecBytes.size), StandardCharsets.UTF_8)

// Parse JSON to retrieve x and y values
val jsonObject = JSONObjectUtils.parse(compactJson)
val x = jsonObject.get("x") as String
val y = jsonObject.get("y") as String

// Create ECKey using Curve.P_256 (or appropriate curve)
val ecKey = ECKey.Builder(Curve.P_256, Base64URL.from(x), Base64URL.from(y))
.build()

// Return as JWK
return ecKey
}

/**
* Convert the PrivateKey to ECPrivateKey
* @param privateKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,12 @@ interface DIDServiceInterface {
* @return DID
*/
fun createEdDSADID(privateKeyX: Base64URL): String

/**
* Converts a DID string to a JWK (JSON Web Key).
* @param did - Decentralized Identifier (DID) string
* @return JWK object
* @throws IllegalArgumentException if the DID format is invalid or conversion fails
*/
fun convertDIDToJWK(did:String):JWK
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ewc.eudi_wallet_oidc_android.services.exceptions

class ExpiryException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ewc.eudi_wallet_oidc_android.services.exceptions

class SignatureException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause)

0 comments on commit 5827aac

Please sign in to comment.