Skip to content

Commit

Permalink
Fix: Support for x5c signature validation on issuance
Browse files Browse the repository at this point in the history
  • Loading branch information
josmilan authored Dec 5, 2024
1 parent d53facb commit 1d8e94a
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ data class PresentationDefinition(

@SerializedName("id") var id: String? = null,
@SerializedName("name") var name: String? = null,
@SerializedName("purpose") var purpose: String? = null,
@SerializedName("format") var format: Map<String, Jwt>? = mapOf(),
@SerializedName("input_descriptors") var inputDescriptors: ArrayList<InputDescriptors>? = arrayListOf()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class CredentialValidator:CredentialValidatorInterface {
throw IllegalArgumentException("JWT token expired")
} catch (signatureException: SignatureException) {
// Throw IllegalArgumentException if JWT signature is invalid
throw IllegalArgumentException("JWT signature invalid")
throw IllegalArgumentException(signatureException.message ?: "JWT signature invalid")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyE
import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction.ProcessKeyJWKFromKID
import com.ewc.eudi_wallet_oidc_android.services.credentialValidation.publicKeyExtraction.ProcessWebJWKFromKID
import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException
import com.ewc.eudi_wallet_oidc_android.services.utils.X509SanRequestVerifier
import com.nimbusds.jose.JWSObject
import com.nimbusds.jose.crypto.ECDSAVerifier
import com.nimbusds.jose.jwk.ECKey
Expand All @@ -27,45 +28,69 @@ class SignatureValidator {
*/
@Throws(SignatureException::class) // Declare that this function might throw a SignatureException
suspend fun validateSignature(jwt: String?, jwksUri: String? = null): Boolean { // Suspended function to validate JWT signature, allowing optional JWKS URI
return try {
try {
jwt?.let { // Null-safe check: proceed if jwt is not null
val jwsObject = JWSObject.parse(jwt) // Parse the JWT string to a JWSObject
val header = jwsObject.header // Retrieve the header from the parsed JWT
val kid = header.keyID // Extract the 'kid' (key ID) from the JWT header
val algorithm = jwsObject.header.algorithm // Extract the algorithm used in the JWT header
val algorithm = jwsObject.header.algorithm

// Check the format of 'kid' and process it accordingly
val response = if (kid != null && kid.startsWith("did:key:z")) { // Check if 'kid' starts with "did:key:z"
ProcessKeyJWKFromKID().processKeyJWKFromKID(kid, algorithm) // Process as a key-based DID JWK (Decentralized Identifier)
} else if (kid != null && kid.startsWith("did:ebsi:z")) { // Check if 'kid' starts with "did:ebsi:z"
ProcessEbsiJWKFromKID().processEbsiJWKFromKID(kid) // Process as an EBSI-based DID JWK
} else if (kid != null && kid.startsWith("did:jwk")) { // Check if 'kid' starts with "did:jwk"
ProcessJWKFromKID().processJWKFromKID(kid) // Process as a JWK (JSON Web Key) DID
} else if(kid !=null && kid.startsWith("did:web")){
ProcessWebJWKFromKID().processWebJWKFromKID(kid)
// Check if 'kid' is null or blank and if 'x5c' is present in the header
if (kid.isNullOrBlank()) {
val x5c = jwsObject.header.toJSONObject()
if (x5c.contains("x5c")) {
var x5cChain: List<String>? = null
x5cChain = X509SanRequestVerifier.instance.extractX5cFromJWT(jwt)
if (x5cChain != null) {
return X509SanRequestVerifier.instance.validateSignatureWithCertificate(jwt, x5cChain, algorithm)
}
// If no valid x5cChain, throw exception
throw SignatureException("JWT signature x5c invalid or cannot be validated")
}
}
else {
ProcessJWKFromJwksUri().processJWKFromJwksUri(kid, jwksUri) // Process JWK using the provided JWKS URI

// Check the format of 'kid' and process it accordingly
val response = when {
kid != null && kid.startsWith("did:key:z") -> {
ProcessKeyJWKFromKID().processKeyJWKFromKID(kid, algorithm) // Process as a key-based DID JWK (Decentralized Identifier)
}
kid != null && kid.startsWith("did:ebsi:z") -> {
ProcessEbsiJWKFromKID().processEbsiJWKFromKID(kid) // Process as an EBSI-based DID JWK
}
kid != null && kid.startsWith("did:jwk") -> {
ProcessJWKFromKID().processJWKFromKID(kid) // Process as a JWK (JSON Web Key) DID
}
kid != null && kid.startsWith("did:web") -> {
ProcessWebJWKFromKID().processWebJWKFromKID(kid) // Process as a Web-based JWK DID
}
else -> {
ProcessJWKFromJwksUri().processJWKFromJwksUri(kid, jwksUri) // Process JWK using the provided JWKS URI
}
}

// If a valid JWK response is received, verify the JWT signature
if (response != null) {
val splitJwt = try {
jwt.split("~")[0]
} catch (e: Exception){
jwt
response?.let {
val splitJwt = try {
jwt.split("~")[0] // Try to remove any tilde (~) character in the JWT signature
} catch (e: Exception) {
jwt // If split fails, use the original JWT
}
val isSignatureValid = verifyJwtSignature(splitJwt, response.toJSONString()) // Verify signature using the JWK response
isSignatureValid // Return the result of the signature verification
} else {
throw SignatureException("Invalid signature") // Throw an exception if JWK response is null
}
} ?: throw SignatureException("Invalid signature") // Handle the case where JWT is null
} catch (e: IllegalArgumentException) { // Catch any IllegalArgumentException thrown during the process
throw SignatureException("Invalid signature") // Wrap the exception into a SignatureException
// Verify signature using the JWK response
return verifyJwtSignature(splitJwt, it.toJSONString())
} ?: throw SignatureException("JWT signature invalid") // Throw an exception if JWK response is null

} ?: throw SignatureException("JWT signature invalid") // Handle the case where JWT is null

} catch (e: IllegalArgumentException) {
// Handle any IllegalArgumentException thrown during the process
if (e.message?.contains("x5c") == true) {
throw e // Rethrow if it's related to 'x5c'
}
throw SignatureException("JWT signature invalid") // Wrap the exception into a SignatureException
}
}


/**
* Verifies the signature of a JWT using a JWK provided as JSON.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package com.ewc.eudi_wallet_oidc_android.services.utils

import android.util.Base64
import com.nimbusds.jose.JWSAlgorithm
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.security.KeyStore
import java.security.PublicKey
import java.security.Signature
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.TrustAnchor
import java.security.cert.X509Certificate
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import java.util.Base64 as base64

class X509SanRequestVerifier private constructor() {
Expand Down Expand Up @@ -63,22 +60,91 @@ class X509SanRequestVerifier private constructor() {
return dnsNames
}

fun validateSignatureWithCertificate(jwt: String, x5cChain: List<String>): Boolean {
val leafCertData = Base64.decode(x5cChain.firstOrNull() ?: "", Base64.DEFAULT)
val certificate = CertificateFactory.getInstance("X.509")
.generateCertificate(leafCertData.inputStream()) as X509Certificate
val publicKey = certificate.publicKey
fun validateSignatureWithCertificate(
jwt: String,
x5cChain: List<String>,
algorithm: JWSAlgorithm? = null
): Boolean {
return try {
// Decode the leaf certificate from the x5cChain
val leafCertData = Base64.decode(x5cChain.firstOrNull() ?: "", Base64.DEFAULT)
val certificate = CertificateFactory.getInstance("X.509")
.generateCertificate(leafCertData.inputStream()) as X509Certificate
val publicKey = certificate.publicKey

// Split JWT into its segments
val segments = jwt.split(".")
if (segments.size != 3) {
println("Invalid JWT format")
return false
}

val segments = jwt.split(".")
if (segments.size != 3) {
println("Invalid JWT format")
return false
val signedData = "${segments[0]}.${segments[1]}"

val signatureWithoutTilda = if (segments[2].contains("~")) {
// If the signature contains '~', split it and take the first part
segments[2].split("~")[0]
} else {
// If there's no '~', assign the signature as is
segments[2]
}

val signature: ByteArray = if (algorithm != null && algorithm.name.startsWith("ES")) {
convertRawSignatureToASN1DER(
// Base64.decode(segments[2], Base64.DEFAULT)
Base64.decode(base64UrlToBase64(signatureWithoutTilda), Base64.DEFAULT)
) ?: return false
} else {
Base64.decode(base64UrlToBase64(signatureWithoutTilda), Base64.DEFAULT)
}

// Validate signature before verifying
if (signature.isEmpty()) {
println("Signature is invalid")
return false
}


// Verify the signature
verifySignature(publicKey, signedData.toByteArray(), signature,algorithm)
} catch (e: Exception) {
println("Error during signature validation: ${e.message}")
false // Return false in case of an exception
}
}

private fun convertRawSignatureToASN1DER(rawSignature: ByteArray): ByteArray? {
return try {
val halfLength = rawSignature.size / 2
val r = rawSignature.sliceArray(0 until halfLength)
val s = rawSignature.sliceArray(halfLength until rawSignature.size)

fun asn1Length(length: Int): ByteArray {
return if (length < 128) {
byteArrayOf(length.toByte())
} else {
val lengthBytes = length.toBigInteger().toByteArray()
val trimmedLengthBytes = lengthBytes.dropWhile { it == 0.toByte() }.toByteArray()
byteArrayOf((0x80 or trimmedLengthBytes.size).toByte()) + trimmedLengthBytes
}
}

val signedData = "${segments[0]}.${segments[1]}"
val signature = Base64.decode(base64UrlToBase64(segments[2]), Base64.DEFAULT)
fun asn1Integer(data: ByteArray): ByteArray {
var bytes = data.toList()
// Add a leading zero if the MSB is set (to avoid it being interpreted as negative)
if (bytes.isNotEmpty() && (bytes[0].toInt() and 0x80) != 0) {
bytes = listOf(0x00.toByte()) + bytes
}
return byteArrayOf(0x02, bytes.size.toByte()) + bytes.toByteArray()
}

return verifySignature(publicKey, signedData.toByteArray(), signature)
val asn1R = asn1Integer(r)
val asn1S = asn1Integer(s)
val asn1Sequence = byteArrayOf(0x30) + asn1Length(asn1R.size + asn1S.size) + asn1R + asn1S
asn1Sequence
} catch (e: Exception) {
null // Return null on failure
}
}

private fun base64UrlToBase64(base64Url: String): String {
Expand All @@ -90,9 +156,18 @@ class X509SanRequestVerifier private constructor() {
return base64
}

private fun verifySignature(publicKey: PublicKey, data: ByteArray, signature: ByteArray): Boolean {
private fun verifySignature(publicKey: PublicKey, data: ByteArray, signature: ByteArray, algorithm: JWSAlgorithm? = null): Boolean {
return try {
val signatureInstance = Signature.getInstance("SHA256withRSA")
// Check if the algorithm is provided or not
val signatureInstance = if (algorithm != null && algorithm.name.startsWith("ES")) {
// If the algorithm starts with "ES", it's likely an ECDSA (e.g., ES256)
Signature.getInstance("SHA256withECDSA")
} else {
// Default to RSA algorithm (e.g., RS256, RS512)
Signature.getInstance("SHA256withRSA")
}

// Initialize signature verification with the public key
signatureInstance.initVerify(publicKey)
signatureInstance.update(data)
signatureInstance.verify(signature)
Expand All @@ -102,91 +177,8 @@ class X509SanRequestVerifier private constructor() {
}
}

// fun validateTrustChain(certificates: List<ByteArray>): Boolean {
// val certificateFactory = CertificateFactory.getInstance("X.509")
// val x509Certificates = certificates.map {
// certificateFactory.generateCertificate(it.inputStream()) as X509Certificate
// }
//
// val certPath = certificateFactory.generateCertPath(x509Certificates)
// val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
// trustManagerFactory.init(null as KeyStore?) // Use the default trust store
//
// // Retrieve the X509TrustManager to get the trusted issuers
// val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager
// val trustAnchors = trustManager.acceptedIssuers.map { TrustAnchor(it, null) }.toSet()
//
// val pkixParams = PKIXParameters(trustAnchors).apply {
// isRevocationEnabled = false // Disable revocation; adjust based on security needs
// }
//
// return try {
// val certPathValidator = CertPathValidator.getInstance("PKIX")
// certPathValidator.validate(certPath, pkixParams)
// true
// } catch (e: Exception) {
// println("Certificate path validation failed: ${e.message}")
// false
// }
// }


// fun validateTrustChain(x5cChain: List<String>): Boolean {
// try {
// // Convert the Base64 encoded certificates to X509Certificate objects
// val certificateFactory = CertificateFactory.getInstance("X.509")
// val certificates = x5cChain.mapNotNull { certBase64 ->
// val certData = base64.getDecoder().decode(certBase64)
// try {
// certificateFactory.generateCertificate(ByteArrayInputStream(certData)) as X509Certificate
// } catch (e: Exception) {
// println("Invalid certificate in chain: ${e.message}")
// null
// }
// }
//
// // If the list of certificates is empty or contains invalid certificates, return false
// if (certificates.isEmpty()) {
// println("No valid certificates found in the chain.")
// return false
// }
//
// // Initialize TrustManagerFactory to get the default trust managers
// val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
// trustManagerFactory.init(null as KeyStore?) // Uses the default system trust store
//
// // Find the first X509TrustManager
// val x509TrustManager = trustManagerFactory.trustManagers
// .filterIsInstance<X509TrustManager>()
// .firstOrNull()
// ?: throw Exception("No X509TrustManager found in the TrustManagerFactory")
//
// // Create the CertPath from the certificates
// val certPath = certificateFactory.generateCertPath(certificates)
//
// // Create PKIXParameters using the accepted issuers from the trust manager
// val acceptedIssuers = x509TrustManager.acceptedIssuers
// .map { TrustAnchor(it, null) }
// .toSet()
// val pkixParams = java.security.cert.PKIXParameters(acceptedIssuers).apply {
// isRevocationEnabled = false // Adjust this based on your requirements
// }
//
// // Validate the certification path using PKIX
// val certPathValidator = CertPathValidator.getInstance("PKIX")
// try {
// certPathValidator.validate(certPath, pkixParams)
// println("The certificate chain is trusted.")
// return true
// } catch (e: CertPathValidatorException) {
// println("Certificate path validation failed: ${e.message}")
// return false
// }
// } catch (e: Exception) {
// println("An error occurred during trust chain validation: ${e.message}")
// return false
// }
// }



@Throws(Exception::class)
fun validateTrustChain(x5cCertificates: List<String>): Boolean {
Expand Down

0 comments on commit 1d8e94a

Please sign in to comment.