diff --git a/vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt b/vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt index 4cdbf36e..cbd24d6d 100644 --- a/vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt +++ b/vc-verifier/kotlin/example/src/main/java/io/mosip/vccred/example/MainActivity.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow @@ -26,7 +27,12 @@ import androidx.compose.ui.unit.dp import io.mosip.vccred.example.ui.theme.VcverifierTheme import io.mosip.vercred.vcverifier.CredentialsVerifier import io.mosip.vercred.vcverifier.constants.CredentialFormat +import io.mosip.vercred.vcverifier.data.CacheEntry import io.mosip.vercred.vcverifier.data.VerificationResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -46,14 +52,21 @@ class MainActivity : ComponentActivity() { @Composable fun VerifyVC(modifier: Modifier = Modifier) { val verificationResult = remember { mutableStateOf(null) } + val walletCache = remember { ConcurrentHashMap() } + val ttlMillis: Long = 1 * 60 * 1000 + val scope = rememberCoroutineScope() Column(modifier = Modifier.padding(30.dp)) { Button( onClick = { - val thread = Thread{ - verificationResult.value = verifyVc() + scope.launch(Dispatchers.IO) { + verificationResult.value = null + val result = verifyVc(walletCache, ttlMillis) + withContext(Dispatchers.Main) { + verificationResult.value = result + } + walletCache.putAll(result.cachediff) } - thread.start() }, modifier = Modifier .padding(16.dp) @@ -94,9 +107,10 @@ fun VerifyVC(modifier: Modifier = Modifier) { } -fun verifyVc(): VerificationResult{ +fun verifyVc(walletCache: ConcurrentHashMap, ttlMillis: Long): VerificationResult { val credentialsVerifier = CredentialsVerifier() - return credentialsVerifier.verify(farmerVc, CredentialFormat.LDP_VC) + val result = credentialsVerifier.verify(farmerVc, CredentialFormat.LDP_VC, walletCache,ttlMillis) + return result } @Preview(showBackground = true) 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 d4493c78..292042c5 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 @@ -8,10 +8,12 @@ import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ERROR_M import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.EXCEPTION_DURING_VERIFICATION import io.mosip.vercred.vcverifier.credentialverifier.CredentialVerifierFactory import io.mosip.vercred.vcverifier.credentialverifier.VerifiableCredential +import io.mosip.vercred.vcverifier.data.CacheEntry import io.mosip.vercred.vcverifier.data.CredentialStatusResult import io.mosip.vercred.vcverifier.data.CredentialVerificationSummary import io.mosip.vercred.vcverifier.data.ValidationStatus import io.mosip.vercred.vcverifier.data.VerificationResult +import io.mosip.vercred.vcverifier.utils.Util import java.util.logging.Logger @@ -37,10 +39,20 @@ class CredentialsVerifier { logger.warning("Credential verification failed") return false } + return true } - fun verify(credential: String, credentialFormat: CredentialFormat): VerificationResult { + fun verify(credential: String, + credentialFormat: CredentialFormat, + walletCache: MutableMap? = null, + expiryTime: Long? = null): VerificationResult { + if (walletCache != null) { + Util.walletCache = walletCache + } + expiryTime?.let { + Util.ttlMillis = it + } val credentialVerifier = credentialVerifierFactory.get(credentialFormat) val validationStatus = credentialVerifier.validate(credential) if (validationStatus.validationMessage.isNotEmpty() && !validationStatus.validationErrorCode.contentEquals( @@ -50,7 +62,8 @@ class CredentialsVerifier { return VerificationResult( false, validationStatus.validationMessage, - validationStatus.validationErrorCode + validationStatus.validationErrorCode, + Util.walletCache ) } return try { @@ -59,19 +72,21 @@ class CredentialsVerifier { return VerificationResult( true, validationStatus.validationMessage, - validationStatus.validationErrorCode + validationStatus.validationErrorCode, + Util.walletCache ) } return VerificationResult( false, ERROR_MESSAGE_VERIFICATION_FAILED, - ERROR_CODE_VERIFICATION_FAILED + ERROR_CODE_VERIFICATION_FAILED, + Util.walletCache ) } catch (e: Exception) { val errorCode = validationStatus.validationErrorCode.takeIf { !it.isNullOrEmpty() } ?: ERROR_CODE_VERIFICATION_FAILED - VerificationResult(false, "$EXCEPTION_DURING_VERIFICATION${e.message}", errorCode) + VerificationResult(false, "$EXCEPTION_DURING_VERIFICATION${e.message}", errorCode,Util.walletCache) } } diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/PresentationVerifier.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/PresentationVerifier.kt index 33a66d59..47c36823 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/PresentationVerifier.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/PresentationVerifier.kt @@ -12,6 +12,7 @@ import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ED25519 import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.ED25519_PROOF_TYPE_2020 import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JSON_WEB_PROOF_TYPE_2020 import io.mosip.vercred.vcverifier.constants.Shared +import io.mosip.vercred.vcverifier.data.CacheEntry import io.mosip.vercred.vcverifier.data.PresentationVerificationResult import io.mosip.vercred.vcverifier.data.PresentationResultWithCredentialStatus import io.mosip.vercred.vcverifier.data.VCResult @@ -141,7 +142,7 @@ class PresentationVerifier { private fun getVCVerificationResults(verifiableCredentials: JSONArray): List { return verifiableCredentials.asIterable().map { item -> val verificationResult: VerificationResult = - credentialsVerifier.verify((item as JSONObject).toString(), CredentialFormat.LDP_VC) + credentialsVerifier.verify((item as JSONObject).toString(), CredentialFormat.LDP_VC, mutableMapOf()) val singleVCVerification: VerificationStatus = Util.getVerificationStatus(verificationResult) 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 40f33874..578dd81b 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 @@ -1,5 +1,6 @@ package io.mosip.vercred.vcverifier.credentialverifier.verifier +import com.apicatalog.jsonld.loader.DocumentLoader import com.nimbusds.jose.JWSObject import foundation.identity.jsonld.ConfigurableDocumentLoader import foundation.identity.jsonld.JsonLDObject @@ -32,7 +33,7 @@ class LdpVerifier { fun verify(credential: String): Boolean { logger.info("Received Credentials Verification - Start") - val confDocumentLoader: ConfigurableDocumentLoader = Util.getConfigurableDocumentLoader() + val confDocumentLoader: DocumentLoader = Util.getConfigurableDocumentLoader() val vcJsonLdObject: JsonLDObject = JsonLDObject.fromJson(credential) vcJsonLdObject.documentLoader = confDocumentLoader diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/data/Data.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/data/Data.kt index e1d9eaa0..9cf2f7c1 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/data/Data.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/data/Data.kt @@ -1,12 +1,13 @@ package io.mosip.vercred.vcverifier.data +import com.apicatalog.jsonld.document.JsonDocument import io.mosip.vercred.vcverifier.exception.StatusCheckException data class VerificationResult( var verificationStatus: Boolean, var verificationMessage: String = "", - var verificationErrorCode: String - + var verificationErrorCode: String, + val cachediff: Map ) data class PresentationVerificationResult( @@ -64,4 +65,9 @@ data class CredentialStatusResult( data class CredentialVerificationSummary( val verificationResult: VerificationResult, val credentialStatus: List -) \ No newline at end of file +) + +data class CacheEntry( + val document: JsonDocument, + val expiryTime: Long +) diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/Util.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/Util.kt index 4586f43b..922eeee2 100644 --- a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/Util.kt +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/Util.kt @@ -1,5 +1,6 @@ package io.mosip.vercred.vcverifier.utils +import com.apicatalog.jsonld.loader.DocumentLoader import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import foundation.identity.jsonld.ConfigurableDocumentLoader @@ -12,6 +13,7 @@ import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWS_ES2 import io.mosip.vercred.vcverifier.constants.CredentialVerifierConstants.JWS_ES256_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.data.CacheEntry import io.mosip.vercred.vcverifier.data.DataModel import io.mosip.vercred.vcverifier.data.VerificationResult import io.mosip.vercred.vcverifier.data.VerificationStatus @@ -26,11 +28,16 @@ import java.security.MessageDigest import java.security.PublicKey import java.security.cert.CertificateFactory import java.security.cert.X509Certificate +import java.util.concurrent.ConcurrentHashMap import kotlin.text.Charsets.UTF_8 object Util { - var documentLoader : ConfigurableDocumentLoader? = null + @Volatile + var documentLoader: DocumentLoader? = null + var walletCache: MutableMap = ConcurrentHashMap() + var ttlMillis: Long = 30 * 60 * 1000 + private val loaderLock = Any() val SUPPORTED_JWS_ALGORITHMS = setOf( JWS_PS256_SIGN_ALGO_CONST, @@ -43,16 +50,29 @@ object Util { return System.getProperty("java.vm.name")?.contains("Dalvik") ?: false } - fun getConfigurableDocumentLoader(): ConfigurableDocumentLoader { - return documentLoader ?: run { - val loader = ConfigurableDocumentLoader() - loader.isEnableHttps = true - loader.isEnableHttp = true - loader.isEnableFile = false - loader + fun getConfigurableDocumentLoader (): DocumentLoader { + documentLoader?.let { return it } + synchronized(loaderLock) { + documentLoader?.let { return it } + + val base = ConfigurableDocumentLoader().apply { + isEnableHttps = true + isEnableHttp = true + isEnableFile = false + } + + val loader = WalletAwareDocumentLoader( + ttlMillis = ttlMillis, + walletCache = walletCache, + delegate = base + ) + + documentLoader = loader + return loader } } + fun getVerificationStatus(verificationResult: VerificationResult): VerificationStatus { if (verificationResult.verificationStatus) { if (verificationResult.verificationErrorCode == CredentialValidatorConstants.ERROR_CODE_VC_EXPIRED) { diff --git a/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/WalletAwareDocumentLoader.kt b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/WalletAwareDocumentLoader.kt new file mode 100644 index 00000000..ec4e56dc --- /dev/null +++ b/vc-verifier/kotlin/vcverifier/src/main/java/io/mosip/vercred/vcverifier/utils/WalletAwareDocumentLoader.kt @@ -0,0 +1,38 @@ +package io.mosip.vercred.vcverifier.utils + + +import com.apicatalog.jsonld.document.Document +import com.apicatalog.jsonld.document.JsonDocument +import com.apicatalog.jsonld.loader.DocumentLoader +import com.apicatalog.jsonld.loader.DocumentLoaderOptions +import io.mosip.vercred.vcverifier.data.CacheEntry +import java.net.URI + +class WalletAwareDocumentLoader( + private val ttlMillis: Long, + private val walletCache: MutableMap, + private val delegate: DocumentLoader +) : DocumentLoader { + + override fun loadDocument(url: URI, options: DocumentLoaderOptions): Document { + val now = System.currentTimeMillis() + val urlStr = url.toString() + + walletCache[urlStr]?.let { entry -> + if (entry.expiryTime > now) return entry.document + walletCache.remove(urlStr) + } + + val fetched = delegate.loadDocument(url, options) + + if (fetched is JsonDocument) { + walletCache[urlStr] = CacheEntry( + document = fetched, + expiryTime = now + ttlMillis + ) + } + + return fetched + } +} + 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 a55e32b7..75f1aef3 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 @@ -16,6 +16,7 @@ import org.threeten.bp.ZoneOffset import org.threeten.bp.format.DateTimeFormatter import java.io.ByteArrayOutputStream import java.util.Date +import java.util.concurrent.ConcurrentHashMap class UtilsTest { @@ -233,7 +234,8 @@ class UtilsTest { val result = VerificationResult( verificationStatus = true, verificationMessage = "Valid VC", - verificationErrorCode = "" + verificationErrorCode = "", + ConcurrentHashMap() ) val status = Util.getVerificationStatus(result) @@ -245,7 +247,8 @@ class UtilsTest { val result = VerificationResult( verificationStatus = true, verificationMessage = "Expired", - verificationErrorCode = CredentialValidatorConstants.ERROR_CODE_VC_EXPIRED + verificationErrorCode = CredentialValidatorConstants.ERROR_CODE_VC_EXPIRED, + ConcurrentHashMap() ) val status = Util.getVerificationStatus(result) @@ -257,7 +260,8 @@ class UtilsTest { val result = VerificationResult( verificationStatus = false, verificationMessage = "Invalid signature", - verificationErrorCode = "SIGNATURE_INVALID" + verificationErrorCode = "SIGNATURE_INVALID", + ConcurrentHashMap() ) val status = Util.getVerificationStatus(result) diff --git a/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/WalletAwareDocumentLoaderTest.kt b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/WalletAwareDocumentLoaderTest.kt new file mode 100644 index 00000000..9ad868c6 --- /dev/null +++ b/vc-verifier/kotlin/vcverifier/src/test/java/io/mosip/vercred/vcverifier/utils/WalletAwareDocumentLoaderTest.kt @@ -0,0 +1,99 @@ +package io.mosip.vercred.vcverifier.utils + +import com.apicatalog.jsonld.document.JsonDocument +import com.apicatalog.jsonld.loader.DocumentLoader +import com.apicatalog.jsonld.loader.DocumentLoaderOptions +import io.mosip.vercred.vcverifier.data.CacheEntry +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.net.URI + +class WalletAwareDocumentLoaderTest { + + private fun jsonDoc(content: String): JsonDocument { + return JsonDocument.of(content.byteInputStream()) + } + + private fun mockDelegate(returned: JsonDocument): DocumentLoader { + val mock = mockk() + every { mock.loadDocument(any(), any()) } returns returned + return mock + } + + @Test + fun `cache hit - return cached document`() { + val ttl = 10_000L + val url = URI("https://example.com/context") + + val cachedDoc = jsonDoc("{\"cached\": true}") + val newDoc = jsonDoc("{\"new\": true}") + + val walletCache = mutableMapOf( + url.toString() to CacheEntry( + cachedDoc, + expiryTime = System.currentTimeMillis() + 5000 + ) + ) + + val loader = WalletAwareDocumentLoader( + ttlMillis = ttl, + walletCache = walletCache, + delegate = mockDelegate(newDoc) + ) + + val result = loader.loadDocument(url, DocumentLoaderOptions()) + + assertSame(cachedDoc, result) + } + + @Test + fun `expired cache - fetch new document and update cache`() { + val ttl = 10_000L + val url = URI("https://example.com/context") + + val expiredDoc = jsonDoc("{\"expired\": true}") + val freshDoc = jsonDoc("{\"fresh\": true}") + + val walletCache = mutableMapOf( + url.toString() to CacheEntry( + expiredDoc, + expiryTime = System.currentTimeMillis() - 1000 + ) + ) + + val loader = WalletAwareDocumentLoader( + ttlMillis = ttl, + walletCache = walletCache, + delegate = mockDelegate(freshDoc) + ) + + val result = loader.loadDocument(url, DocumentLoaderOptions()) + + assertSame(freshDoc, result) + assertEquals(freshDoc, walletCache[url.toString()]!!.document) + } + + @Test + fun `cache miss - fetch and store document`() { + val ttl = 10_000L + val url = URI("https://example.com/context") + + val fetchedDoc = jsonDoc("{\"loaded\": true}") + val walletCache = mutableMapOf() + + val loader = WalletAwareDocumentLoader( + ttlMillis = ttl, + walletCache = walletCache, + delegate = mockDelegate(fetchedDoc) + ) + + val result = loader.loadDocument(url, DocumentLoaderOptions()) + + assertSame(fetchedDoc, result) + assertTrue(walletCache.containsKey(url.toString())) + assertEquals(fetchedDoc, walletCache[url.toString()]!!.document) + } +} +