diff --git a/README.md b/README.md index 01629793..8cda425e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ To integrate **vc-verifier** library into a Maven project , include below addit To integrate **vc-verifier** library into a Gradle project , add below line in module level `build.gradle`. dependencies { - implementation("io.mosip:vc-verifier:{{version-number}}") + implementation("io.mosip:vc-verifier-aar:{{version-number}}") } To avoid Duplicate classes error while building the application, include the below exclusion strategy in the build.gradle file. diff --git a/vc-verifier/kotlin/vcverifier/build.gradle.kts b/vc-verifier/kotlin/vcverifier/build.gradle.kts index 44f2ee30..f951c8b6 100644 --- a/vc-verifier/kotlin/vcverifier/build.gradle.kts +++ b/vc-verifier/kotlin/vcverifier/build.gradle.kts @@ -100,6 +100,8 @@ tasks.register("jacocoTestReport", JacocoReport::class) { executionData.setFrom(files("${layout.buildDirectory.get()}/jacoco/testDebugUnitTest.exec")) } +tasks.register("prepareKotlinBuildScriptModel"){} + tasks.register("jarRelease") { duplicatesStrategy = DuplicatesStrategy.EXCLUDE dependsOn("assembleRelease") diff --git a/vc-verifier/kotlin/vcverifier/publish-artifact.gradle b/vc-verifier/kotlin/vcverifier/publish-artifact.gradle index a63cdd2a..2dd8287b 100644 --- a/vc-verifier/kotlin/vcverifier/publish-artifact.gradle +++ b/vc-verifier/kotlin/vcverifier/publish-artifact.gradle @@ -89,7 +89,7 @@ publishing { } groupId = "io.mosip" artifactId = "vcverifier-aar" - version = "1.2.0-SNAPSHOT" + version = "1.3.0-SNAPSHOT" if (project.gradle.startParameter.taskNames.any { it.contains('assembleRelease') }) { artifacts { aar { @@ -103,7 +103,7 @@ publishing { artifact(tasks.named("jarRelease").get()) groupId = "io.mosip" artifactId = "vcverifier-jar" - version = "1.2.0-SNAPSHOT" + version = "1.3.0-SNAPSHOT" artifact(tasks.named("javadocJar").get()) artifact(tasks.named("sourcesJar").get()) pom { diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/CredentialsVerifier.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/CredentialsVerifier.kt index b11b09bf..73e3b1b8 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/CredentialsVerifier.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/CredentialsVerifier.kt @@ -4,8 +4,8 @@ import io.mosip.vercred.vcverifier.constants.CredentialFormat import io.mosip.vercred.vcverifier.constants.CredentialFormat.LDP_VC import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CODE_VC_EXPIRED import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ERROR_CODE_VERIFICATION_FAILED -import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.EXCEPTION_DURING_VERIFICATION import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ERROR_MESSAGE_VERIFICATION_FAILED +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.EXCEPTION_DURING_VERIFICATION import io.mosip.vercred.vcverifier.credentialverifier.CredentialVerifierFactory import io.mosip.vercred.vcverifier.data.VerificationResult import java.util.logging.Logger diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/DidWebResolver.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/DidWebResolver.kt index 01b853f7..165d3eec 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/DidWebResolver.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/DidWebResolver.kt @@ -22,15 +22,13 @@ class DidWebResolver(private val didUrl: String) { private const val FRAGMENT = "(#.*)?" private val DID_MATCHER = "^did:$METHOD:$METHOD_ID$PARAMS$PATH$QUERY$FRAGMENT$".toRegex() private const val DOC_PATH = "/did.json" + private const val WELL_KNOWN_PATH = ".well-known" } fun resolve(): Map { val parsedDid = parseDidUrl() try { - val path = parsedDid.id.split(":").joinToString("/") { - URLDecoder.decode(it, StandardCharsets.UTF_8.name()) - } - val url = "https://$path$DOC_PATH" + val url = constructDIDUrl(parsedDid) return sendHTTPRequest(url, HTTP_METHOD.GET) ?: throw DidDocumentNotFound("Did document could not be fetched") } catch (e: Exception) { @@ -38,8 +36,20 @@ class DidWebResolver(private val didUrl: String) { } } - private fun parseDidUrl(): ParsedDID { + private fun constructDIDUrl(parsedDid: ParsedDID): String { + val idComponents = parsedDid.id.split(":").map { it } + val baseDomain = idComponents.first() + val path = idComponents.drop(1).joinToString("/") + val urlPath = if (path.isEmpty()) { + WELL_KNOWN_PATH + DOC_PATH + } else { + path + DOC_PATH + } + return "https://$baseDomain/$urlPath" + } + + private fun parseDidUrl(): ParsedDID { val matchResult = DID_MATCHER.find(didUrl) ?: throw UnsupportedDidUrl() val sections = matchResult.groupValues diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialValidatorConstants.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialValidatorConstants.kt index bf43ebef..3cd83558 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialValidatorConstants.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialValidatorConstants.kt @@ -83,12 +83,14 @@ object CredentialValidatorConstants { val ALGORITHMS_SUPPORTED = listOf( "PS256", "RS256", - "EdDSA" + "EdDSA", + "ES256K" ) val PROOF_TYPES_SUPPORTED = listOf( "RsaSignature2018", "Ed25519Signature2018", - "Ed25519Signature2020" + "Ed25519Signature2020", + "EcdsaSecp256k1Signature2019" ) } diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialVerifierConstants.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialVerifierConstants.kt index d845b0d5..ddba13db 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialVerifierConstants.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/constants/CredentialVerifierConstants.kt @@ -5,6 +5,8 @@ object CredentialVerifierConstants { const val PUBLIC_KEY_PEM = "publicKeyPem" const val PUBLIC_KEY_MULTIBASE = "publicKeyMultibase" + const val PUBLIC_KEY_JWK = "publicKeyJwk" + const val PUBLIC_KEY_HEX = "publicKeyHex" const val VERIFICATION_METHOD = "verificationMethod" const val KEY_TYPE = "type" @@ -15,22 +17,22 @@ object CredentialVerifierConstants { const val PS256_ALGORITHM = "SHA256withRSA/PSS" const val RS256_ALGORITHM = "SHA256withRSA" + const val EC_ALGORITHM = "SHA256withECDSA" const val ED25519_ALGORITHM = "Ed25519" const val RSA_ALGORITHM = "RSA" + const val SECP256K1 = "secp256k1" const val JWS_PS256_SIGN_ALGO_CONST = "PS256" const val JWS_RS256_SIGN_ALGO_CONST = "RS256" const val JWS_EDDSA_SIGN_ALGO_CONST = "EdDSA" - - const val RSA_SIGNATURE = "RsaSignature2018" - const val ED25519_SIGNATURE_2018 = "Ed25519Signature2018" - const val ED25519_SIGNATURE_2020 = "Ed25519Signature2020" + const val JWS_ES256K_SIGN_ALGO_CONST = "ES256K" const val RSA_KEY_TYPE = "RsaVerificationKey2018" const val ED25519_KEY_TYPE_2018 = "Ed25519VerificationKey2018" const val ED25519_KEY_TYPE_2020 = "Ed25519VerificationKey2020" + const val ES256K_KEY_TYPE_2019 = "EcdsaSecp256k1VerificationKey2019" - + const val JWK_KEY_TYPE_EC = "EC" const val EXCEPTION_DURING_VERIFICATION = "Exception during Verification: " const val ERROR_MESSAGE_VERIFICATION_FAILED = "Verification Failed" @@ -38,4 +40,6 @@ object CredentialVerifierConstants { // This is used to turn public key bytes into a buffer in DER format const val DER_PUBLIC_KEY_PREFIX = "302a300506032b6570032100" + + const val COMPRESSED_HEX_KEY_LENGTH = 33 } \ No newline at end of file diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/LdpVerifier.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/LdpVerifier.kt index 9d753387..2695c484 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/LdpVerifier.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/credentialverifier/verifier/LdpVerifier.kt @@ -8,6 +8,7 @@ import info.weboftrust.ldsignatures.canonicalizer.URDNA2015Canonicalizer import info.weboftrust.ldsignatures.util.JWSUtil import io.ipfs.multibase.Multibase import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWS_EDDSA_SIGN_ALGO_CONST +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWS_ES256K_SIGN_ALGO_CONST import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWS_PS256_SIGN_ALGO_CONST import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWS_RS256_SIGN_ALGO_CONST import io.mosip.vercred.vcverifier.exception.PublicKeyNotFoundException @@ -16,6 +17,7 @@ import io.mosip.vercred.vcverifier.exception.UnknownException import io.mosip.vercred.vcverifier.publicKey.PublicKeyGetterFactory import io.mosip.vercred.vcverifier.signature.SignatureVerifier import io.mosip.vercred.vcverifier.signature.impl.ED25519SignatureVerifierImpl +import io.mosip.vercred.vcverifier.signature.impl.ES256KSignatureVerifierImpl import io.mosip.vercred.vcverifier.signature.impl.PS256SignatureVerifierImpl import io.mosip.vercred.vcverifier.signature.impl.RS256SignatureVerifierImpl import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -32,7 +34,8 @@ class LdpVerifier { private val SIGNATURE_VERIFIER: Map = mapOf( JWS_PS256_SIGN_ALGO_CONST to PS256SignatureVerifierImpl(), JWS_RS256_SIGN_ALGO_CONST to RS256SignatureVerifierImpl(), - JWS_EDDSA_SIGN_ALGO_CONST to ED25519SignatureVerifierImpl() + JWS_EDDSA_SIGN_ALGO_CONST to ED25519SignatureVerifierImpl(), + JWS_ES256K_SIGN_ALGO_CONST to ES256KSignatureVerifierImpl() ) init { diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/Utils.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/Utils.kt index db865158..50fe73bb 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/Utils.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/Utils.kt @@ -1,39 +1,45 @@ package io.mosip.vercred.vcverifier.publicKey +import com.fasterxml.jackson.databind.ObjectMapper import io.ipfs.multibase.Base58 +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.COMPRESSED_HEX_KEY_LENGTH import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.DER_PUBLIC_KEY_PREFIX import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ED25519_ALGORITHM import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ED25519_KEY_TYPE_2018 import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ED25519_KEY_TYPE_2020 +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ES256K_KEY_TYPE_2019 +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWK_KEY_TYPE_EC import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.RSA_ALGORITHM import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.RSA_KEY_TYPE +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.SECP256K1 import io.mosip.vercred.vcverifier.exception.PublicKeyNotFoundException +import io.mosip.vercred.vcverifier.exception.PublicKeyTypeNotSupportedException +import io.mosip.vercred.vcverifier.utils.Encoder +import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.jce.spec.ECNamedCurveSpec import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.io.pem.PemReader import java.io.StringReader +import java.math.BigInteger import java.security.KeyFactory import java.security.PublicKey +import java.security.interfaces.ECPublicKey +import java.security.spec.ECParameterSpec +import java.security.spec.ECPublicKeySpec import java.security.spec.X509EncodedKeySpec private var provider: BouncyCastleProvider = BouncyCastleProvider() -fun isPublicKeyMultibase(publicKeyMultibase: String): Boolean { - //ref: https://w3c.github.io/vc-di-eddsa/#multikey - val rawPublicKeyWithHeader = Base58.decode(publicKeyMultibase.substring(1)) - return rawPublicKeyWithHeader.size > 2 && - rawPublicKeyWithHeader[0] == 0xed.toByte() && - rawPublicKeyWithHeader[1] == 0x01.toByte() -} - -fun isPemPublicKey(str: String) = str.contains("BEGIN PUBLIC KEY") - private val PUBLIC_KEY_ALGORITHM: Map = mapOf( RSA_KEY_TYPE to RSA_ALGORITHM, ED25519_KEY_TYPE_2018 to ED25519_ALGORITHM, - ED25519_KEY_TYPE_2020 to ED25519_ALGORITHM + ED25519_KEY_TYPE_2020 to ED25519_ALGORITHM, ) +private const val SECP256K1_PRIME_MODULUS = + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F" + fun getPublicKeyObjectFromPemPublicKey(publicKeyPem: String, keyType: String): PublicKey { try { val strReader = StringReader(publicKeyPem) @@ -48,6 +54,39 @@ fun getPublicKeyObjectFromPemPublicKey(publicKeyPem: String, keyType: String): P } } + +fun getPublicKeyFromJWK(jwkStr: String, keyType: String): PublicKey { + val objectMapper = ObjectMapper() + val jwk: Map = + objectMapper.readValue(jwkStr, Map::class.java) as Map + + return when (keyType) { + ES256K_KEY_TYPE_2019 -> getECPublicKey(jwk) + else -> throw PublicKeyTypeNotSupportedException("Unsupported key type: $keyType") + } +} + + +private fun getECPublicKey(jwk: Map): PublicKey { + val curve = jwk["crv"] ?: throw IllegalArgumentException("Missing 'crv' field for EC key") + val xBytes = Encoder().decodeFromBase64UrlFormatEncoded(jwk["x"]!!) + val yBytes = Encoder().decodeFromBase64UrlFormatEncoded(jwk["y"]!!) + + val x = BigInteger(1, xBytes) + val y = BigInteger(1, yBytes) + val ecPoint = java.security.spec.ECPoint(x, y) + + val ecSpec = when (curve) { + SECP256K1 -> ECNamedCurveTable.getParameterSpec(SECP256K1) + else -> throw IllegalArgumentException("Unsupported EC curve: $curve") + } + + val ecParameterSpec = ECNamedCurveSpec(curve, ecSpec.curve, ecSpec.g, ecSpec.n) + val pubKeySpec = ECPublicKeySpec(ecPoint, ecParameterSpec) + val keyFactory = KeyFactory.getInstance(JWK_KEY_TYPE_EC, provider) + return keyFactory.generatePublic(pubKeySpec) +} + fun getPublicKeyObjectFromPublicKeyMultibase(publicKeyPem: String, keyType: String): PublicKey { try { val rawPublicKeyWithHeader = Base58.decode(publicKeyPem.substring(1)) @@ -61,3 +100,52 @@ fun getPublicKeyObjectFromPublicKeyMultibase(publicKeyPem: String, keyType: Stri } } +fun getPublicKeyFromHex(hexKey: String, keyType: String): PublicKey { + return when (keyType) { + ES256K_KEY_TYPE_2019 -> getECPublicKeyFromHex(hexKey) + else -> throw PublicKeyTypeNotSupportedException("Unsupported key type: $keyType") + } +} + +fun getECPublicKeyFromHex(hexKey: String): PublicKey { + val keyFactory = KeyFactory.getInstance(JWK_KEY_TYPE_EC, provider) + val keyBytes = hexStringToByteArray(hexKey) + val ecPoint = decodeSecp256k1PublicKey(keyBytes) + val ecSpec = secp256k1Params() + val pubKeySpec = ECPublicKeySpec(ecPoint, ecSpec) + + return keyFactory.generatePublic(pubKeySpec) as ECPublicKey +} + +private fun hexStringToByteArray(hex: String): ByteArray { + return BigInteger(hex, 16).toByteArray().dropWhile { it == 0.toByte() }.toByteArray() +} + + +private fun decodeSecp256k1PublicKey(keyBytes: ByteArray): java.security.spec.ECPoint { + require(keyBytes.size == COMPRESSED_HEX_KEY_LENGTH) { "Invalid compressed public key length" } + + val x = BigInteger(1, keyBytes.copyOfRange(1, keyBytes.size)) + val y = recoverYCoordinate(x, keyBytes[0] == 3.toByte()) + + return java.security.spec.ECPoint(x, y) +} + +// Recover the Y-coordinate from X using the Secp256k1 curve equation +private fun recoverYCoordinate(x: BigInteger, odd: Boolean): BigInteger { + val p = BigInteger(SECP256K1_PRIME_MODULUS, 16) + val b = BigInteger.valueOf(7) + + val rhs = (x.modPow(BigInteger.valueOf(3), p).add(b)).mod(p) + val y = rhs.modPow(p.add(BigInteger.ONE).divide(BigInteger.valueOf(4)), p) + + return if (y.testBit(0) == odd) y else p.subtract(y) +} + +private fun secp256k1Params(): ECParameterSpec { + val params = ECNamedCurveTable.getParameterSpec(SECP256K1) + return ECNamedCurveSpec(SECP256K1, params.curve, params.g, params.n, params.h) +} + + + diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/DidWebPublicKeyGetter.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/DidWebPublicKeyGetter.kt index 07f69f00..340d9a48 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/DidWebPublicKeyGetter.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/DidWebPublicKeyGetter.kt @@ -1,45 +1,79 @@ package io.mosip.vercred.vcverifier.publicKey.impl +import com.fasterxml.jackson.databind.ObjectMapper import io.mosip.vercred.vcverifier.DidWebResolver import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.KEY_TYPE +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_HEX +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_JWK import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_MULTIBASE +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_PEM import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.VERIFICATION_METHOD import io.mosip.vercred.vcverifier.exception.PublicKeyNotFoundException import io.mosip.vercred.vcverifier.exception.PublicKeyTypeNotSupportedException import io.mosip.vercred.vcverifier.publicKey.PublicKeyGetter +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromHex +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromJWK import io.mosip.vercred.vcverifier.publicKey.getPublicKeyObjectFromPemPublicKey import io.mosip.vercred.vcverifier.publicKey.getPublicKeyObjectFromPublicKeyMultibase -import io.mosip.vercred.vcverifier.publicKey.isPemPublicKey -import io.mosip.vercred.vcverifier.publicKey.isPublicKeyMultibase import java.net.URI import java.security.PublicKey import java.util.logging.Logger -class DidWebPublicKeyGetter: PublicKeyGetter { +class DidWebPublicKeyGetter : PublicKeyGetter { private val logger = Logger.getLogger(DidWebPublicKeyGetter::class.java.name) - override fun get(verificationMethod: URI): PublicKey { + override fun get(verificationMethodUri: URI): PublicKey { try { - val didDocument = DidWebResolver(verificationMethod.toString()).resolve() - didDocument.let{ - val publicKeyStr = getKeyValue(it, PUBLIC_KEY_MULTIBASE) - val keyType = getKeyValue(it,KEY_TYPE) - return when { - isPemPublicKey(publicKeyStr) -> getPublicKeyObjectFromPemPublicKey(publicKeyStr, keyType) - isPublicKeyMultibase(publicKeyStr) -> getPublicKeyObjectFromPublicKeyMultibase(publicKeyStr, keyType) - else -> throw PublicKeyTypeNotSupportedException("Public Key type is not supported") - } + val didDocument = DidWebResolver(verificationMethodUri.toString()).resolve() + + val verificationMethods = didDocument[VERIFICATION_METHOD] as? List> + ?: throw PublicKeyNotFoundException("Verification method not found in DID document") + + val verificationMethod = verificationMethods.find { it["id"] == verificationMethodUri.toString() } + ?: throw PublicKeyNotFoundException("No verification methods available in DID document") + + val publicKeyStr = getKeyValue( + verificationMethod, arrayOf( + PUBLIC_KEY_PEM, PUBLIC_KEY_MULTIBASE, PUBLIC_KEY_JWK, PUBLIC_KEY_HEX + ) + ) + val keyType = getKeyValue(verificationMethod, arrayOf(KEY_TYPE)) + return when { + PUBLIC_KEY_JWK in verificationMethod -> getPublicKeyFromJWK( + publicKeyStr, keyType + ) + PUBLIC_KEY_HEX in verificationMethod -> getPublicKeyFromHex( + publicKeyStr, keyType + ) + PUBLIC_KEY_PEM in verificationMethod -> getPublicKeyObjectFromPemPublicKey( + publicKeyStr, keyType + ) + PUBLIC_KEY_MULTIBASE in verificationMethod -> getPublicKeyObjectFromPublicKeyMultibase( + publicKeyStr, keyType + ) + + else -> throw PublicKeyTypeNotSupportedException("Public Key type is not supported") } } catch (e: Exception) { - logger.severe("Error fetching public key string $e") - throw PublicKeyNotFoundException(e.message) + logger.severe("Error fetching public key: ${e.message}") + throw PublicKeyNotFoundException(e.message ?: "Unknown error") } } - //TODO: match the key instead of taking the 0th index data - private fun getKeyValue(responseObjectNode: Map, key: String): String { - val verificationMethodList = responseObjectNode[VERIFICATION_METHOD] as ArrayList> - return verificationMethodList[0][key].toString() + /** + * Extracts the first available value for a given list of keys. + */ + private fun getKeyValue(responseObjectNode: Map, keys: Array): String { + for (key in keys) { + responseObjectNode[key]?.let { value -> + return when (value) { + is String -> value + else -> ObjectMapper().writeValueAsString(value) + } + } + } + + throw PublicKeyNotFoundException("None of the provided keys were found in verification method") } -} \ No newline at end of file +} diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/HttpsPublicKeyGetter.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/HttpsPublicKeyGetter.kt index 6c92a2cd..54c093c6 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/HttpsPublicKeyGetter.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/publicKey/impl/HttpsPublicKeyGetter.kt @@ -1,16 +1,19 @@ package io.mosip.vercred.vcverifier.publicKey.impl import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.KEY_TYPE +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_HEX +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_JWK +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_MULTIBASE import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.PUBLIC_KEY_PEM import io.mosip.vercred.vcverifier.exception.PublicKeyNotFoundException import io.mosip.vercred.vcverifier.exception.PublicKeyTypeNotSupportedException import io.mosip.vercred.vcverifier.networkManager.HTTP_METHOD.GET import io.mosip.vercred.vcverifier.networkManager.NetworkManagerClient.Companion.sendHTTPRequest import io.mosip.vercred.vcverifier.publicKey.PublicKeyGetter +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromHex +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromJWK import io.mosip.vercred.vcverifier.publicKey.getPublicKeyObjectFromPemPublicKey import io.mosip.vercred.vcverifier.publicKey.getPublicKeyObjectFromPublicKeyMultibase -import io.mosip.vercred.vcverifier.publicKey.isPemPublicKey -import io.mosip.vercred.vcverifier.publicKey.isPublicKeyMultibase import java.net.URI import java.security.PublicKey import java.util.logging.Logger @@ -27,8 +30,19 @@ class HttpsPublicKeyGetter : PublicKeyGetter { val publicKeyStr = it[PUBLIC_KEY_PEM].toString() val keyType = it[KEY_TYPE].toString() return when { - isPemPublicKey(publicKeyStr) -> getPublicKeyObjectFromPemPublicKey(publicKeyStr, keyType) - isPublicKeyMultibase(publicKeyStr) -> getPublicKeyObjectFromPublicKeyMultibase(publicKeyStr, keyType) + PUBLIC_KEY_JWK in it -> getPublicKeyFromJWK( + publicKeyStr, keyType + ) + PUBLIC_KEY_HEX in it -> getPublicKeyFromHex( + publicKeyStr, keyType + ) + PUBLIC_KEY_PEM in it -> getPublicKeyObjectFromPemPublicKey( + publicKeyStr, keyType + ) + PUBLIC_KEY_MULTIBASE in it -> getPublicKeyObjectFromPublicKeyMultibase( + publicKeyStr, keyType + ) + else -> throw PublicKeyTypeNotSupportedException("Public Key type is not supported") } } diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/signature/impl/ES256KSignatureVerifierImpl.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/signature/impl/ES256KSignatureVerifierImpl.kt new file mode 100644 index 00000000..98724b04 --- /dev/null +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/signature/impl/ES256KSignatureVerifierImpl.kt @@ -0,0 +1,68 @@ +package io.mosip.vercred.vcverifier.signature.impl + +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants +import io.mosip.vercred.vcverifier.exception.SignatureVerificationException +import io.mosip.vercred.vcverifier.signature.SignatureVerifier +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.security.PublicKey +import java.security.Signature + +private const val ECDSA_SIGNATURE_LENGTH = 64 + +class ES256KSignatureVerifierImpl : SignatureVerifier { + override fun verify(publicKey: PublicKey, signData: ByteArray, signature: ByteArray?, provider: BouncyCastleProvider?): Boolean { + if (signature == null || signature.size != ECDSA_SIGNATURE_LENGTH) { + throw SignatureVerificationException("Invalid signature length: Expected 64 bytes for R || S format") + } + + try { + val derSignature = convertRawSignatureToDER(signature) // Convert to ASN.1 DER + + Signature.getInstance(CredentialVerifierConstants.EC_ALGORITHM, provider) + .apply { + initVerify(publicKey) + update(signData) + return verify(derSignature) + } + } catch (e: Exception) { + throw SignatureVerificationException("Error while doing signature verification using ES256K algorithm: ${e.message}") + } + } + + /** + * Converts a raw ECDSA (R || S) signature (64 bytes) into ASN.1 DER format. + * + * ASN.1 DER Format: + * - 0x30 (Sequence) + * - Total length + * - 0x02 (Integer marker) + Length of R + R value + * - 0x02 (Integer marker) + Length of S + S value + * + */ + private fun convertRawSignatureToDER(signature: ByteArray): ByteArray { + val r = BigInteger(1, signature.copyOfRange(0, ECDSA_SIGNATURE_LENGTH/2)) + val s = BigInteger(1, signature.copyOfRange(ECDSA_SIGNATURE_LENGTH/2, ECDSA_SIGNATURE_LENGTH)) + + val outputStream = ByteArrayOutputStream() + val derEncoder = java.io.DataOutputStream(outputStream) + + derEncoder.writeByte(0x30) + val seqBytes = ByteArrayOutputStream() + + seqBytes.write(0x02) + seqBytes.write(r.toByteArray().size) + seqBytes.write(r.toByteArray()) + + seqBytes.write(0x02) + seqBytes.write(s.toByteArray().size) + seqBytes.write(s.toByteArray()) + + val derSeq = seqBytes.toByteArray() + derEncoder.write(derSeq.size) + derEncoder.write(derSeq) + + return outputStream.toByteArray() + } +} diff --git a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/CredentialsVerifierTest.kt b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/CredentialsVerifierTest.kt index 8247c956..0088c5cf 100644 --- a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/CredentialsVerifierTest.kt +++ b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/CredentialsVerifierTest.kt @@ -55,6 +55,18 @@ class CredentialsVerifierTest { } + @Test + fun `should return true for valid credential verification success using ES256K`() { + val file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "ldp_vc/ES256KSignedMockVC.json") + val vc = String(Files.readAllBytes(file.toPath())) + + val verificationResult = CredentialsVerifier().verify(vc, LDP_VC) + + assertEquals("", verificationResult.verificationMessage) + assertTrue(verificationResult.verificationStatus) + assertEquals("", verificationResult.verificationErrorCode) + } + @Test @Timeout(value = 10, unit = TimeUnit.SECONDS) fun `should return false for invalid credential verification failure`() { @@ -66,4 +78,5 @@ class CredentialsVerifierTest { assertFalse(verify.verificationStatus) assertEquals(ERROR_CODE_VERIFICATION_FAILED, verify.verificationErrorCode) } + } \ No newline at end of file diff --git a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/PublicKeyUtilsTest.kt b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/PublicKeyUtilsTest.kt new file mode 100644 index 00000000..410e737d --- /dev/null +++ b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/PublicKeyUtilsTest.kt @@ -0,0 +1,40 @@ +package io.mosip.vercred.vcverifier + +import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ES256K_KEY_TYPE_2019 +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromHex +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromJWK +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.security.PublicKey +import java.security.interfaces.ECPublicKey + +class PublicKeyUtilsTest{ + @Test + fun `test EC secp256k1 public key extraction`() { + val jwkString = """ + { + "kty": "EC", + "crv": "secp256k1", + "x": "STRMr8BN3ToqGYWQExEm5-mjyiSqq9iGs600-4UMiZY", + "y": "wMC2jyYYA1UPz5TjeHRkSAZV6y6_C5oyCPsudWtFQPM" + } + """.trimIndent() + val publicKey: PublicKey = getPublicKeyFromJWK(jwkString,ES256K_KEY_TYPE_2019) + + assertNotNull(publicKey) + assertTrue(publicKey is ECPublicKey) + assertEquals("EC", publicKey.algorithm) + } + + @Test + fun `should correctly generate PublicKey from valid compressed secp256k1 hex`() { + // Valid compressed secp256k1 public key (33 bytes) + val compressedHexKey = "034ee0f670fc96bb75e8b89c068a1665007a41c98513d6a911b6137e2d16f1d300" + + val publicKey = getPublicKeyFromHex(compressedHexKey,ES256K_KEY_TYPE_2019) + + assertNotNull(publicKey, "Public key should not be null") + assertTrue(publicKey is ECPublicKey, "Returned key should be an instance of ECPublicKey") + assertEquals("EC", publicKey.algorithm) + } +} \ No newline at end of file diff --git a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/DidWebResolverTest.kt b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/DidWebResolverTest.kt index d12ba02e..b969bc65 100644 --- a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/DidWebResolverTest.kt +++ b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/DidWebResolverTest.kt @@ -51,4 +51,47 @@ class DidWebResolverTest { val exception = assertThrows { DidWebResolver(didUrl).resolve() } assertEquals("Given did url is not supported", exception.message) } -} + + @Test + fun `should resolve DID with only domain to well-known path`() { + + val didUrl = "did:web:example.com" + val mockResponse = mapOf("id" to didUrl) + + mockkObject(NetworkManagerClient.Companion) + every { + sendHTTPRequest( + "https://example.com/.well-known/did.json", + HTTP_METHOD.GET + ) + } returns mockResponse + + val resolvedDoc = DidWebResolver(didUrl).resolve() + assertEquals(resolvedDoc, mapOf("id" to didUrl)) + + } + + @Test + fun `should resolve DID with multiple path components to correct URL`() { + val didUrl = "did:web:example.com:user:alice" + val mockResponse = mapOf("id" to didUrl) + + mockkObject(NetworkManagerClient.Companion) + every { sendHTTPRequest("https://example.com/user/alice/did.json", HTTP_METHOD.GET) } returns mockResponse + + val resolvedDoc = DidWebResolver(didUrl).resolve() + assertEquals(resolvedDoc, mapOf("id" to didUrl)) + } + + @Test + fun `should resolve DID with single path component to correct URL`() { + val didUrl = "did:web:example.com:path1" + val mockResponse = mapOf("id" to didUrl) + + mockkObject(NetworkManagerClient.Companion) + every { sendHTTPRequest("https://example.com/path1/did.json", HTTP_METHOD.GET) } returns mockResponse + + val resolvedDoc = DidWebResolver(didUrl).resolve() + assertEquals(resolvedDoc, mapOf("id" to didUrl)) + } +} \ No newline at end of file diff --git a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/UtilsTest.kt b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/UtilsTest.kt index f264aeb0..0f5f49ef 100644 --- a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/UtilsTest.kt +++ b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/UtilsTest.kt @@ -1,17 +1,26 @@ package io.mosip.vercred.vcverifier.utils +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromHex +import io.mosip.vercred.vcverifier.publicKey.getPublicKeyFromJWK +import io.mosip.vercred.vcverifier.publicKey.impl.DidWebPublicKeyGetter import io.mosip.vercred.vcverifier.utils.DateUtils.dateFormats +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.spec.ECParameterSpec import org.json.JSONArray import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.io.ByteArrayOutputStream +import java.net.URI +import java.security.PublicKey import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import java.security.interfaces.ECPublicKey class UtilsTest { @@ -200,4 +209,6 @@ class UtilsTest { val result = dateUtils.isFutureDateWithTolerance("") assertFalse(result) } + + } \ No newline at end of file diff --git a/vc-verifier/kotlin/vcverifier/src/test/resources/ldp_vc/ES256KSignedMockVC.json b/vc-verifier/kotlin/vcverifier/src/test/resources/ldp_vc/ES256KSignedMockVC.json new file mode 100644 index 00000000..7188632b --- /dev/null +++ b/vc-verifier/kotlin/vcverifier/src/test/resources/ldp_vc/ES256KSignedMockVC.json @@ -0,0 +1,37 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://piyush7034.github.io/my-files/farmer.json" + ], + "issuer": "did:web:vharsh.github.io:DID:echarsh", + "type": [ + "VerifiableCredential", + "FarmerCredential" + ], + "issuanceDate": "2025-02-04T08:07:11.686Z", + "expirationDate": "2027-02-04T08:07:11.685Z", + "credentialSubject": { + "id": "did:jwk:eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsInVzZSI6InNpZyIsImtpZCI6ImhXM0JtVnVWY3ZSR3U5S0FIWllzYnRGS0otallWUFUwZW96S2FrT3FGclEiLCJhbGciOiJSUzI1NiIsIm4iOiJoRHJYTnhpdENlbjdqM2dWNEI2N0NraTdnOHhIbWhUSm1CNjFuRDV2S183a2ExRWVXNTRuVkpYREctdWxrVndDQVJUb2pjTmxlYm03TkZzVTItSDFzdS1SdmQ2NHBhdVBoUGxUSHE2c0c3aW1mSk9QUnhHRGt5WWlDTjZlYWo5VHlWSms5c3pydHVvWXl1blZqWWZyV08zUTl6MkhMUTZON0NUT0cwT1N2TzlzUlZFNFhrY3R6MFkxNGhxeEMzaTZnYllzT3hjZzRMLWFyOW1rQkxmODctekd0elhicjlXeGp3Qjg4aU5VRjgtd0lsM3AwLWpWOG9UdzRDaTlreWxpZkVCb2N6OTJPU0tpM2FCV0JWSHgzZm10ZE12ZVBYbkRHTEhwUlF0UnZjelhxRnR0Mm9SRjNLQTZieEtsdmFXd0dlYnRnMDhhTXhGazAtWFdxNzV3VncifQ==", + "fullName": "${fullName}", + "mobileNumber": "${mobileNumber}", + "dateOfBirth": "25-05-1990", + "gender": "${gender}", + "state": "${state}", + "district": "${district}", + "villageOrTown": "${villageOrTown}", + "postalCode": "${postalCode}", + "landArea": "3 hectares", + "landOwnershipType": "Owner", + "primaryCropType": "Maize", + "secondaryCropType": "Rice", + "face": "${face}", + "farmerID": "${farmerID}" + }, + "proof": { + "type": "EcdsaSecp256k1Signature2019", + "created": "2025-02-04T02:37:11Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:web:vharsh.github.io:DID:echarsh#key-0", + "jws": "eyJ4NXQjUzI1NiI6InBZYXh6VWtrVnZKSzQ2X3NCUFRNNEhaSHdGTWItNUYxZzV0MEFjam5ONlkiLCJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJraWQiOiJmU3ZrX0RkdUJtTDJUdG5hTUZPYjBOUE9pT3BFOVlOQWhlX3RBM1J4Q3g0IiwiYWxnIjoiRVMyNTZLIn0..SPVFEQp_VJE9a6y_7iMDMXw1iwIPk0d3W_9nPv2UMyekhRyb59OW-DIIHWGZb7YawYwSZLX0ZxA6SdLGtOhqpQ" + } +}