diff --git a/build.gradle.kts b/build.gradle.kts index 2e5ffd7..22df2c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,12 @@ plugins { id("maven-publish") - kotlin("multiplatform") version "1.7.10" - kotlin("plugin.serialization") version "1.7.10" + kotlin("multiplatform") version "1.9.23" + kotlin("plugin.serialization") version "1.9.23" id("io.gitlab.arturbosch.detekt") version "1.21.0" } group = "com.sphereon.vdx" -version = "1.0.0-SNAPSHOT" +version = "1.1.0-SNAPSHOT" detekt { @@ -63,14 +63,17 @@ kotlin { val dssVersion = "5.11.1" val kotlinSerializationVersion = "1.4.0-RC" - val kotlinDateTimeVersion = "0.4.0" + val kotlinDateTimeVersion = "0.5.0" val bcVersion = "1.71" val commonMain by getting { dependencies { api("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinDateTimeVersion") + api("io.github.microutils:kotlin-logging:3.0.5") implementation("io.matthewnelson.kotlin-components:encoding-base64:1.1.3") + implementation("com.mayakapps.kache:kache:2.1.0-beta05") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") } } val commonTest by getting { @@ -95,17 +98,18 @@ kotlin { implementation("eu.europa.ec.joinup.sd-dss:dss-pades-pdfbox:$dssVersion") implementation("eu.europa.ec.joinup.sd-dss:dss-crl-parser-x509crl:$dssVersion") api("org.bouncycastle:bcprov-debug-jdk18on:$bcVersion") - api("javax.cache:cache-api:1.1.1") implementation("javax.xml.bind:jaxb-api:2.3.0") api("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1") implementation("javax.annotation:javax.annotation-api:1.3.2") implementation("javax.activation:activation:1.1.1") api("org.glassfish.jaxb:jaxb-runtime:2.3.6") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.2") - api("io.github.microutils:kotlin-logging-jvm:2.1.23") + + api("io.github.microutils:kotlin-logging-jvm:3.0.5") // todo separate into separate project probably - api("com.sphereon.vdx:eidas-signature-client-rest-jersey3:1.0.0-SNAPSHOT") + api("com.sphereon.vdx:eidas-signature-client-rest-jersey3:1.1.0-SNAPSHOT") implementation(project.dependencies.platform("com.azure:azure-sdk-bom:1.2.4")) implementation("com.azure:azure-identity") @@ -122,10 +126,6 @@ kotlin { implementation("org.bouncycastle:bcpkix-jdk18on:$bcVersion") implementation("io.mockk:mockk:1.12.4") - -// implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.2") - - implementation("org.ehcache:ehcache:3.8.1") /* implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}") implementation("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}") diff --git a/gradle.properties b/gradle.properties index b18e3ad..8af57df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,2 @@ kotlin.code.style=official -kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.native.enableDependencyPropagation=false kotlin.js.generate.executable.default=false diff --git a/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/Algorithms.kt b/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/Algorithms.kt index 3321046..499303a 100644 --- a/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/Algorithms.kt +++ b/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/Algorithms.kt @@ -29,6 +29,8 @@ enum class CryptoAlg(val internalName: String, val oid: String, val padding: Str RSA("RSA", "1.2.840.113549.1.1.1", "RSA/ECB/PKCS1Padding"), + SHA256_WITH_RSA("SHA256withRSA", "1.2.840.113549.1.1.11", ""), // PKCS#1 v1.5 is implied for SHA256withRSA + // DSA("DSA", "1.2.840.10040.4.1", "DSA"), ECDSA("ECDSA", "1.2.840.10045.2.1", "ECDSA"), @@ -44,5 +46,11 @@ enum class CryptoAlg(val internalName: String, val oid: String, val padding: Str ED448("Ed448", "1.3.101.113", "Ed448"), HMAC("HMAC", "", ""); + + companion object { + fun from(name: String): CryptoAlg = entries.find { it.internalName == name } + ?: throw IllegalArgumentException("Algorithm $name not found") + + } } diff --git a/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/KeyProviderType.kt b/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/KeyProviderType.kt index 72c467e..47dc420 100644 --- a/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/KeyProviderType.kt +++ b/src/commonMain/kotlin/com/sphereon/vdx/ades/enums/KeyProviderType.kt @@ -4,5 +4,5 @@ package com.sphereon.vdx.ades.enums * The Key Provider Types supported */ enum class KeyProviderType { - PKCS11, PKCS12, REST, JKS, AZURE_KEYVAULT + PKCS11, PKCS12, REST, JKS, AZURE_KEYVAULT, DIGIDENTITY } diff --git a/src/commonMain/kotlin/com/sphereon/vdx/ades/pki/CacheService.kt b/src/commonMain/kotlin/com/sphereon/vdx/ades/pki/CacheService.kt new file mode 100644 index 0000000..beddf41 --- /dev/null +++ b/src/commonMain/kotlin/com/sphereon/vdx/ades/pki/CacheService.kt @@ -0,0 +1,77 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package com.sphereon.vdx.ades.pki + +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy +import com.mayakapps.kache.ObjectKache +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +private const val MAX_CACHE_SIZE = 1024L +private val logger = KotlinLogging.logger {} + +open class CacheService( + private val cacheName: String, + private val cacheEnabled: Boolean? = true, + private val cacheTTLInSeconds: Long? = 600, +) { + private var cache: ObjectKache? = null + + init { + initCache() + } + + fun get(key: K): V? { + return runBlocking { + getAsync(key) + } + } + + suspend fun getAsync(key: K): V? { + if (!isEnabled()) { + return null + } + val value = cache!!.get(key) + logger.debug { "Cache ${value?.let { "HIT" } ?: "MIS"} for key '$key' in cache '$cacheName'" } + return value + } + + fun isEnabled(): Boolean { + return cacheEnabled == true && cache != null + } + + fun put(key: K, value: V): V { + return runBlocking { + putAsync(key, value) + } + } + + suspend fun putAsync(key: K, value: V): V { + logger.entry(key, value) + if (isEnabled()) { + logger.trace { "Caching value for key $key" } + cache!!.put(key, value) + if (cache!!.get(key) == null) { + throw RuntimeException("Item was not placed in the cache") + } + } + logger.exit(value) + return value + } + + private fun initCache() { + logger.info { "Cache '$cacheName' is ${if (cacheEnabled == true) "" else "NOT"} being enabled..." } + if (cacheEnabled == true) { + if (cache == null) { + cache = InMemoryKache(maxSize = MAX_CACHE_SIZE) { + strategy = KacheStrategy.LRU + expireAfterAccessDuration = cacheTTLInSeconds!!.toDuration(DurationUnit.SECONDS) + } + logger.info { "Cache '$cacheName' now is enabled" } + } + } + } +} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/AbstractCacheObjectSerializer.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/AbstractCacheObjectSerializer.kt deleted file mode 100644 index 2290a9b..0000000 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/AbstractCacheObjectSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.cbor.Cbor -import java.nio.ByteBuffer -import java.util.* -import javax.cache.configuration.Configuration - -@OptIn(ExperimentalSerializationApi::class) -abstract class AbstractCacheObjectSerializer(private val serializer: KSerializer) { - - fun serialize(obj: V?): ByteBuffer? { - return if (obj == null) { - null - } else { - val bytes = Cbor.encodeToByteArray(serializer, obj) - ByteBuffer.wrap(bytes) - } - } - - fun equals(obj: V?, binary: ByteBuffer?): Boolean { - return Objects.equals(obj, read(binary)) - } - - fun read(binary: ByteBuffer?): V? { - if (binary == null) { - return null - } - return Cbor.decodeFromByteArray(serializer, binary.array()) - } - - abstract fun cacheConfiguration(cacheTTLInSeconds: Long? = 30): Configuration - -} - diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AbstractKeyProviderService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AbstractKeyProviderService.kt index e769e68..7fbf1c7 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AbstractKeyProviderService.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AbstractKeyProviderService.kt @@ -1,6 +1,5 @@ package com.sphereon.vdx.ades.pki -import AbstractCacheObjectSerializer import com.sphereon.vdx.ades.enums.CryptoAlg import com.sphereon.vdx.ades.enums.DigestAlg import com.sphereon.vdx.ades.enums.MaskGenFunction @@ -16,6 +15,10 @@ import com.sphereon.vdx.ades.sign.util.toJavaPublicKey import eu.europa.esig.dss.enumerations.SignatureAlgorithm import eu.europa.esig.dss.spi.DSSSecurityProvider import mu.KotlinLogging +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.DERNull +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.DigestInfo import java.security.GeneralSecurityException import java.security.spec.MGF1ParameterSpec import java.security.spec.PSSParameterSpec @@ -29,11 +32,11 @@ private val logger = KotlinLogging.logger {} */ abstract class AbstractKeyProviderService( override val settings: KeyProviderSettings, - cacheObjectSerializer: AbstractCacheObjectSerializer? ) : IKeyProviderService { + @Suppress("LeakingThis") protected val cacheService: CacheService = - CacheService("Keys", settings.config.cacheEnabled, settings.config.cacheTTLInSeconds, cacheObjectSerializer) + CacheService("Keys", settings.config.cacheEnabled, settings.config.cacheTTLInSeconds) override fun createSignature(signInput: SignInput, keyEntry: IKeyEntry): Signature { @@ -66,6 +69,7 @@ abstract class AbstractKeyProviderService( else signature.algorithm.toDSS().jceId, DSSSecurityProvider.getSecurityProviderName() ) + if (signature.algorithm.maskGenFunction != null) { val digestJavaName: String = signature.algorithm.digestAlgorithm?.toDSS()!!.javaName val parameterSpec = PSSParameterSpec( @@ -79,12 +83,11 @@ abstract class AbstractKeyProviderService( } javaSig.initVerify(publicKey.toJavaPublicKey()) - /*val digest = if (signInput.signMode == SignMode.DIGEST || signature.algorithm.digestAlgorithm == null) { - signInput.input + if (signInput.signMode == SignMode.DIGEST && signature.algorithm.digestAlgorithm != null) { + javaSig.update(derEncode(signature.algorithm.digestAlgorithm, signInput)) } else { - DSSUtils.digest(signature.algorithm.digestAlgorithm.toDSS(), signInput.input) - }*/ - javaSig.update(signInput.input) + javaSig.update(signInput.input) + } val verify = javaSig.verify(signature.value) logger.info { "Signature with date '${signature.date}' and provider '${signature.providerId}' for input '${signInput.name}' was ${if (verify) "VALID" else "INVALID"}" } logger.exit(verify) @@ -95,7 +98,27 @@ abstract class AbstractKeyProviderService( } } - protected abstract fun createSignatureImpl(signInput: SignInput, keyEntry: IKeyEntry, mgf: MaskGenFunction? = null): Signature + /* + When we have predigested value instead the content that has yet to be digested, we need to make sure + we DER encode the hash because in RSA_RAW / NONEwithRSA mode BouncyCastle will no longer take care of this, + hence the verification will fail. + */ + private fun derEncode( + digestAlgorithm: DigestAlg, + signInput: SignInput + ): ByteArray { + val asN1ObjectIdentifier = ASN1ObjectIdentifier(digestAlgorithm.oid) + val algId = AlgorithmIdentifier(asN1ObjectIdentifier, DERNull.INSTANCE) + val dInfo = DigestInfo(algId, signInput.input) + return dInfo.getEncoded("DER") + } + + protected abstract fun createSignatureImpl( + signInput: SignInput, + keyEntry: IKeyEntry, + mgf: MaskGenFunction? = null + ): Signature + protected fun isDigestMode(signInput: SignInput) = signInput.signMode == SignMode.DIGEST && signInput.digestAlgorithm != DigestAlg.NONE } diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/CacheService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/CacheService.kt deleted file mode 100644 index 1472bc8..0000000 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/CacheService.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.sphereon.vdx.ades.pki - -import AbstractCacheObjectSerializer -import mu.KotlinLogging -import java.util.concurrent.TimeUnit -import javax.cache.Cache -import javax.cache.Caching -import javax.cache.configuration.Configuration -import javax.cache.configuration.MutableConfiguration -import javax.cache.expiry.AccessedExpiryPolicy -import javax.cache.expiry.Duration - -private val logger = KotlinLogging.logger {} - -open class CacheService( - private val cacheName: String, - private val cacheEnabled: Boolean? = true, - private val cacheTTLInSeconds: Long? = 60, - private val serializer: AbstractCacheObjectSerializer? -) { - private var cache: Cache? = null - - - init { - initCache() - } - - - fun get(key: K): V? { - if (!isEnabled()) { - return null - } - val v = cache!!.get(key) - logger.debug { "Cache ${v?.let { "HIT" } ?: "MIS"} for key '$key' in cache '$cacheName'" } - return v - } - - fun isEnabled(): Boolean { - return cacheEnabled == true && cache != null - } - - fun put(key: K, value: V): V { - logger.entry(key, value) - if (isEnabled()) { - logger.trace { "Caching value for key $key" } - cache!!.put(key, value) - } - logger.exit(value) - return value - } - - private fun initCache() { - logger.info { "Cache '$cacheName' is ${if (cacheEnabled == true) "" else "NOT"} being enabled..." } - if (cacheEnabled == true) { - val cachingProvider = Caching.getCachingProvider() - val cacheManager = cachingProvider.cacheManager - - if (cache == null) { - if (cacheManager.cacheNames.contains(cacheName)) { - this.cache = cacheManager.getCache(cacheName) - } else { - // We get the config from the serializer of any. Default to a mutable config. This might clutter the logs with warnings in case of Ehcache, - // as our Kotlin data classes are cross-platform and cannot implement Java's Serializable interface - val config: Configuration = serializer?.cacheConfiguration(cacheTTLInSeconds) - ?: MutableConfiguration().setExpiryPolicyFactory( - AccessedExpiryPolicy.factoryOf( - Duration(TimeUnit.SECONDS, cacheTTLInSeconds!!) - ) - ) - - cache = cacheManager.createCache(cacheName, config) - logger.info { "Cache '$cacheName' now is enabled" } - } - } - } - } -} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyProviderServiceFactory.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyProviderServiceFactory.kt index b7d9beb..67fb41c 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyProviderServiceFactory.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyProviderServiceFactory.kt @@ -1,38 +1,56 @@ package com.sphereon.vdx.ades.pki -import AbstractCacheObjectSerializer import com.sphereon.vdx.ades.SignClientException import com.sphereon.vdx.ades.enums.KeyProviderType -import com.sphereon.vdx.ades.model.IKeyEntry import com.sphereon.vdx.ades.model.KeyProviderSettings +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultClientConfig +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultKeyProviderService +import com.sphereon.vdx.ades.pki.digidentity.DigidentityKeyProviderService +import com.sphereon.vdx.ades.pki.digidentity.DigidentityProviderConfig +import com.sphereon.vdx.ades.pki.restclient.RestClientConfig +import com.sphereon.vdx.ades.pki.restclient.RestClientKeyProviderService object KeyProviderServiceFactory { + data class CreateOptions( + var restClientConfig: RestClientConfig? = null, + var azureKeyvaultClientConfig: AzureKeyvaultClientConfig? = null, + var digidentityProviderConfig: DigidentityProviderConfig? = null, + ) + fun createFromConfig( settings: KeyProviderSettings, - restClientConfig: RestClientConfig? = null, - azureKeyvaultClientConfig: AzureKeyvaultClientConfig? = null, - cacheObjectSerializer: AbstractCacheObjectSerializer? = null + createOptions: CreateOptions.() -> Unit = {} ): IKeyProviderService { + val options = CreateOptions().apply(createOptions) + return when (settings.config.type) { KeyProviderType.REST -> { RestClientKeyProviderService( settings, - restClientConfig - ?: throw SignClientException("Cannot create REST key provider without providing a REST client config"), - cacheObjectSerializer + options.restClientConfig + ?: throw SignClientException("Cannot create REST key provider without providing a REST client config") ) } + KeyProviderType.AZURE_KEYVAULT -> { AzureKeyvaultKeyProviderService( settings, - azureKeyvaultClientConfig - ?: throw SignClientException("Cannot create a Azure Keyvault key provider without providing a Azure Keyvault client config"), - cacheObjectSerializer + options.azureKeyvaultClientConfig + ?: throw SignClientException("Cannot create a Azure Keyvault key provider without providing a Azure Keyvault client config") + ) + } + + KeyProviderType.DIGIDENTITY -> { + DigidentityKeyProviderService( + settings, + options.digidentityProviderConfig + ?: throw SignClientException("Cannot create a Digidentity key provider without providing a Digidentity provider config") ) } + else -> { - LocalKeyProviderService(settings, cacheObjectSerializer) + LocalKeyProviderService(settings) } } } diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyvaultMapper.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyvaultMapper.kt index f334890..721cb3d 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyvaultMapper.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/KeyvaultMapper.kt @@ -4,7 +4,14 @@ import com.azure.core.credential.TokenCredential import com.azure.core.http.policy.ExponentialBackoffOptions import com.azure.core.util.ClientOptions import com.azure.core.util.Header -import com.azure.identity.* +import com.azure.identity.ClientCertificateCredential +import com.azure.identity.ClientCertificateCredentialBuilder +import com.azure.identity.ClientSecretCredential +import com.azure.identity.ClientSecretCredentialBuilder +import com.azure.identity.InteractiveBrowserCredential +import com.azure.identity.InteractiveBrowserCredentialBuilder +import com.azure.identity.UsernamePasswordCredential +import com.azure.identity.UsernamePasswordCredentialBuilder import com.azure.security.keyvault.certificates.models.KeyVaultCertificate import com.azure.security.keyvault.keys.cryptography.models.SignatureAlgorithm import com.azure.security.keyvault.keys.models.KeyVaultKey @@ -13,6 +20,14 @@ import com.sphereon.vdx.ades.SigningException import com.sphereon.vdx.ades.enums.CryptoAlg import com.sphereon.vdx.ades.model.IKeyEntry import com.sphereon.vdx.ades.model.KeyEntry +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultClientConfig +import com.sphereon.vdx.ades.pki.azure.CertificateCredentialOpts +import com.sphereon.vdx.ades.pki.azure.CredentialMode +import com.sphereon.vdx.ades.pki.azure.CredentialOpts +import com.sphereon.vdx.ades.pki.azure.ExponentialBackoffRetryOpts +import com.sphereon.vdx.ades.pki.azure.InteractiveBrowserCredentialOpts +import com.sphereon.vdx.ades.pki.azure.SecretCredentialOpts +import com.sphereon.vdx.ades.pki.azure.UsernamePasswordCredentialOpts import com.sphereon.vdx.ades.sign.util.CertificateUtil import com.sphereon.vdx.ades.sign.util.toCertificate import com.sphereon.vdx.ades.sign.util.toKey @@ -21,7 +36,7 @@ import java.security.cert.X509Certificate import java.time.Duration fun AzureKeyvaultClientConfig.toClientOptions(): ClientOptions? { - if ((headers == null || headers.isEmpty()) && applicationId == null) { + if (headers.isNullOrEmpty() && applicationId == null) { return null } return ClientOptions().setApplicationId(applicationId).setHeaders(headers?.map { Header(it.name, it.values) }) diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/LocalKeyProviderService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/LocalKeyProviderService.kt index 3485dcc..358f427 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/LocalKeyProviderService.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/LocalKeyProviderService.kt @@ -1,6 +1,5 @@ package com.sphereon.vdx.ades.pki -import AbstractCacheObjectSerializer import com.sphereon.vdx.ades.SigningException import com.sphereon.vdx.ades.enums.DigestAlg import com.sphereon.vdx.ades.enums.MaskGenFunction @@ -14,8 +13,8 @@ import eu.europa.esig.dss.token.AbstractKeyStoreTokenConnection import eu.europa.esig.dss.token.KSPrivateKeyEntry -class LocalKeyProviderService(settings: KeyProviderSettings, cacheObjectSerializer: AbstractCacheObjectSerializer? = null) : - AbstractKeyProviderService(settings, cacheObjectSerializer) { +class LocalKeyProviderService(settings: KeyProviderSettings) : + AbstractKeyProviderService(settings) { // TODO: Create provider so we can move this to the abstract class and even move createSignatureImpl there private val tokenConnection = ConnectionFactory.connection(settings) diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultClientConfig.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultClientConfig.kt similarity index 98% rename from src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultClientConfig.kt rename to src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultClientConfig.kt index 1605d4c..c851ab4 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultClientConfig.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultClientConfig.kt @@ -1,4 +1,4 @@ -package com.sphereon.vdx.ades.pki +package com.sphereon.vdx.ades.pki.azure @kotlinx.serialization.Serializable data class AzureKeyvaultClientConfig( diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultKeyProviderService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultKeyProviderService.kt similarity index 95% rename from src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultKeyProviderService.kt rename to src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultKeyProviderService.kt index d0bace0..e10b478 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultKeyProviderService.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultKeyProviderService.kt @@ -1,6 +1,5 @@ -package com.sphereon.vdx.ades.pki +package com.sphereon.vdx.ades.pki.azure -import AbstractCacheObjectSerializer import com.azure.core.http.policy.RetryOptions import com.azure.core.http.policy.RetryPolicy import com.azure.security.keyvault.certificates.CertificateAsyncClient @@ -14,6 +13,7 @@ import com.sphereon.vdx.ades.SigningException import com.sphereon.vdx.ades.enums.KeyProviderType import com.sphereon.vdx.ades.enums.MaskGenFunction import com.sphereon.vdx.ades.model.* +import com.sphereon.vdx.ades.pki.* import com.sphereon.vdx.ades.sign.util.* import mu.KotlinLogging import java.util.* @@ -25,8 +25,7 @@ private val logger = KotlinLogging.logger {} open class AzureKeyvaultKeyProviderService( settings: KeyProviderSettings, val keyvaultConfig: AzureKeyvaultClientConfig, - cacheObjectSerializer: AbstractCacheObjectSerializer? = null -) : AbstractKeyProviderService(settings, cacheObjectSerializer) { +) : AbstractKeyProviderService(settings) { private val keyClient: KeyAsyncClient private val certClient: CertificateAsyncClient? @@ -72,14 +71,14 @@ open class AzureKeyvaultKeyProviderService( override fun getKeys(): List { val res = keyClient.listPropertiesOfKeys().filter { it.isEnabled }.map { - getKey("${it.id}${KEY_NAME_VERSION_SEP}${it.version}")!! + getKey("${it.id}$KEY_NAME_VERSION_SEP${it.version}")!! } return res.toIterable().toList() } override fun getKey(kid: String): IKeyEntry? { - logger.exit(kid) + logger.entry(kid) val cachedKey = cacheService.get(kid) if (cachedKey != null) { logger.debug { "Cache hit for key entry with id $kid" } diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultTokenConnection.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultTokenConnection.kt similarity index 94% rename from src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultTokenConnection.kt rename to src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultTokenConnection.kt index caa25b7..b602c95 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultTokenConnection.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/azure/AzureKeyvaultTokenConnection.kt @@ -1,4 +1,4 @@ -package com.sphereon.vdx.ades.pki +package com.sphereon.vdx.ades.pki.azure import com.azure.core.http.policy.RetryOptions import com.azure.core.http.policy.RetryPolicy @@ -9,6 +9,10 @@ import com.azure.security.keyvault.keys.cryptography.models.SignatureAlgorithm import com.sphereon.vdx.ades.enums.SignMode import com.sphereon.vdx.ades.model.SignInput import com.sphereon.vdx.ades.model.Signature +import com.sphereon.vdx.ades.pki.toAzureSignatureAlgorithm +import com.sphereon.vdx.ades.pki.toClientOptions +import com.sphereon.vdx.ades.pki.toExponentialBackoffOptions +import com.sphereon.vdx.ades.pki.toTokenCredential import com.sphereon.vdx.ades.sign.AbstractSignatureTokenConnection import com.sphereon.vdx.ades.sign.util.toDSS import eu.europa.esig.dss.token.DSSPrivateKeyEntry diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityESignApi.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityESignApi.kt new file mode 100644 index 0000000..7120c0e --- /dev/null +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityESignApi.kt @@ -0,0 +1,80 @@ +package com.sphereon.vdx.ades.pki.digidentity + +import com.fasterxml.jackson.annotation.JsonProperty +import com.sphereon.vdx.ades.rest.client.ApiClient +import com.sphereon.vdx.ades.rest.client.ApiException +import jakarta.ws.rs.core.GenericType + +private const val TYPE_SIGN = "sign" +private const val CONTENT_TYPE_JSON = "application/vnd.api+json" +private const val AUTH_NAME_OAUTH2 = "oauth2" + +class DigidentityESignApi(private val apiClient: ApiClient) { + + + data class Data( + val id: String? = null, // Nullable for requests + val type: String, + val attributes: T + ) + + class SignRequestAttributes( + @JsonProperty("hash_to_sign") val hashToSign: String + ) + + + class SignRequest(hashToSign: String) { + val data: Data = Data( + type = TYPE_SIGN, + attributes = SignRequestAttributes(hashToSign = hashToSign) + ) + } + + data class SignResponse(val data: Data) + + data class SignResponseAttributes( + val signature: String, + val certificate: String, + @JsonProperty("hash_to_sign") val hash_to_sign: String + ) + + data class SignResult( + val kid: String, + val signature: String, + val certificate: String, + val hashToSign: String + ) + + // Example of converting a response to a SignResult + private fun signResultFrom(data: Data): SignResult { + return SignResult( + kid = data.id ?: throw IllegalArgumentException("ID is missing in response"), + signature = data.attributes.signature, + certificate = data.attributes.certificate, + hashToSign = data.attributes.hash_to_sign + ) + } + + + @Throws(ApiException::class) + fun signHash(kid: String, hash: String): SignResult { + val signRequest = SignRequest(hash) + val localVarReturnType: GenericType = object : GenericType() {} + val response = apiClient.invokeAPI( + "sign", + "/auto_signers/${kid}/sign", + "POST", + emptyList(), + signRequest, + emptyMap(), + emptyMap(), + emptyMap(), + CONTENT_TYPE_JSON, + CONTENT_TYPE_JSON, + arrayOf(AUTH_NAME_OAUTH2), + localVarReturnType, + false + ) + return signResultFrom(response.data.data) + } +} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityKeyProviderService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityKeyProviderService.kt new file mode 100644 index 0000000..cda60d7 --- /dev/null +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityKeyProviderService.kt @@ -0,0 +1,126 @@ +package com.sphereon.vdx.ades.pki.digidentity + +import com.sphereon.vdx.ades.PKIException +import com.sphereon.vdx.ades.SignClientException +import com.sphereon.vdx.ades.enums.KeyProviderType +import com.sphereon.vdx.ades.enums.MaskGenFunction +import com.sphereon.vdx.ades.enums.SignatureAlg +import com.sphereon.vdx.ades.model.IKeyEntry +import com.sphereon.vdx.ades.model.KeyProviderSettings +import com.sphereon.vdx.ades.model.SignInput +import com.sphereon.vdx.ades.model.Signature +import com.sphereon.vdx.ades.pki.restclient.AbstractRestClientKeyProviderService +import com.sphereon.vdx.ades.pki.restclient.OAuth2Config +import com.sphereon.vdx.ades.pki.restclient.RestClientConfig +import com.sphereon.vdx.ades.sign.util.CertificateUtil +import com.sphereon.vdx.ades.sign.util.isActive +import com.sphereon.vdx.ades.sign.util.toDSS +import com.sphereon.vdx.ades.sign.util.toDigest +import eu.europa.esig.dss.spi.DSSUtils +import mu.KotlinLogging + +private const val API_KEY_HEADER = "Api-Key" + +private val logger = KotlinLogging.logger {} + +private const val DUMMY_HASH = "0000000000000000000000000000000000000000000000000000000000000000" + +private val CLEANUP_REGEX = "[\\r\\n\\t]+".toRegex() + +@OptIn(ExperimentalStdlibApi::class) +open class DigidentityKeyProviderService( + settings: KeyProviderSettings, + val providerConfig: DigidentityProviderConfig, +) : AbstractRestClientKeyProviderService(settings, restClientConfigFrom(providerConfig)) { + + private var esignApi: DigidentityESignApi + + init { + assertSettings() + esignApi = DigidentityESignApi(apiClient) + } + + + override fun createSignatureImpl(signInput: SignInput, keyEntry: IKeyEntry, mgf: MaskGenFunction?): Signature { + logger.entry(signInput, keyEntry, mgf) + logger.info { "Creating signature with date '${signInput.signingDate}' provider Id '${settings.id}', key Id '${keyEntry.kid}' and sign input '${signInput.name}'..." } + + val isDigest = isDigestMode(signInput) + val digest = if (isDigest) { + val digest = signInput.toDigest().hexValue + if (digest.length != 64) { + throw IllegalArgumentException("Invalid hash supplied to be signed") + } + digest.lowercase() // Digidentity API crashes when we send uppercase hex chars + } else { + DSSUtils.digest(signInput.digestAlgorithm!!.toDSS(), signInput.input).toHexString() + } + val signResponse = esignApi.signHash(keyEntry.kid, digest) + val signatureClean = signResponse.signature.replace(CLEANUP_REGEX, "") + + val signature = Signature( + value = java.util.Base64.getDecoder().decode(signatureClean), + algorithm = SignatureAlg.RSA_SHA256, // The only supported algo atm + signMode = signInput.signMode, + keyEntry = keyEntry, + providerId = signResponse.kid, + date = signInput.signingDate + ) + logger.info { "Signature created with date '${signInput.signingDate}' provider Id '${settings.id}', key Id '${keyEntry.kid}' and sign input '${signInput.name}'" } + logger.exit(signature) + return signature + } + + override fun getKeys(): List { + throw PKIException("Retrieving multiple certificates using the Digidenity client is not possible currently") + } + + override fun getKey(kid: String): IKeyEntry? { + logger.entry(kid) + try { + val cachedKey = cacheService.get(kid) + if (cachedKey != null && cachedKey.certificate?.isActive() == true) { + logger.debug { "Cache hit for key entry with id $kid" } + return cachedKey + } + logger.debug { "Cache miss for key entry with id $kid" } + + // Dummy sign action to fetch certificate + val signResponse = esignApi.signHash(kid, DUMMY_HASH) + val certData = signResponse.certificate.toByteArray() + val x509Certificate = CertificateUtil.toX509Certificate(certData) + val keyEntry = CertificateUtil.toKeyEntry(x509Certificate, signResponse.kid) + cacheService.put(kid, keyEntry) + return keyEntry + } finally { + logger.exit(kid) + } + } + + private fun assertSettings() { + if (settings.config.type != KeyProviderType.DIGIDENTITY) { + throw SignClientException("Cannot create a Digidentity Provider without mode set to DIGIDENTITY. Supplied mode: ${settings.config.type}") + } + } +} + + +fun restClientConfigFrom(providerConfig: DigidentityProviderConfig): RestClientConfig { + val credentialOpts = providerConfig.credentialOpts + if (credentialOpts.credentialMode != DigidentityCredentialMode.SERVICE_CLIENT_SECRET) { + throw SignClientException("Cannot create a Digidentity Provider; only credentialMode currently supported is SERVICE_CLIENT_SECRET. Supplied mode: ${credentialOpts.credentialMode}") + } + + return credentialOpts.secretCredentialOpts?.let { + return RestClientConfig( + baseUrl = providerConfig.baseUrl, + oAuth2 = OAuth2Config( + tokenUrl = it.tokenUrl, + clientId = it.clientId, + clientSecret = it.clientSecret, + ), + defaultHeaders = mapOf(Pair(API_KEY_HEADER, it.apiKey)) + ) + } + ?: throw SignClientException("Cannot create a Digidentity Provider; secretCredentialOpts is empty and the only credentialMode currently supported is SERVICE_CLIENT_SECRET which requires secretCredentialOpts") +} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityProviderConfig.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityProviderConfig.kt new file mode 100644 index 0000000..5248a01 --- /dev/null +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/digidentity/DigidentityProviderConfig.kt @@ -0,0 +1,31 @@ +package com.sphereon.vdx.ades.pki.digidentity + +import com.sphereon.vdx.ades.pki.azure.CredentialType + +@kotlinx.serialization.Serializable +data class DigidentityProviderConfig( + val baseUrl: String, + val autoSignerId: String?, + val credentialOpts: DigidentityCredentialOpts +) + +@kotlinx.serialization.Serializable +data class DigidentityCredentialOpts( + val credentialMode: DigidentityCredentialMode, + val secretCredentialOpts: DigidentitySecretCredentialOpts? = null, +) + +/** + * Authenticate with client secret & API key. + */ +@kotlinx.serialization.Serializable +data class DigidentitySecretCredentialOpts( + val clientId: String, + val clientSecret: String, + val apiKey: String, + val tokenUrl: String = "https://auth.digidentity-preproduction.eu/oauth2/token.json" +) + +enum class DigidentityCredentialMode(val credentialType: CredentialType) { + SERVICE_CLIENT_SECRET(CredentialType.SERVICE), +} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/AbstractRestClientKeyProviderService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/AbstractRestClientKeyProviderService.kt new file mode 100644 index 0000000..ce78805 --- /dev/null +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/AbstractRestClientKeyProviderService.kt @@ -0,0 +1,84 @@ +package com.sphereon.vdx.ades.pki.restclient + +import com.sphereon.vdx.ades.SignClientException +import com.sphereon.vdx.ades.model.KeyProviderSettings +import com.sphereon.vdx.ades.pki.AbstractKeyProviderService +import com.sphereon.vdx.ades.rest.client.ApiClient +import com.sphereon.vdx.ades.rest.client.auth.Authentication +import com.sphereon.vdx.ades.rest.client.auth.HttpBearerAuth +import com.sphereon.vdx.ades.rest.client.auth.OAuth + +private const val BEARER_LITERAL = "bearer" +private const val OAUTH2_LITERAL = "oauth2" + +abstract class AbstractRestClientKeyProviderService( + settings: KeyProviderSettings, + private val restClientConfig: RestClientConfig +) : AbstractKeyProviderService(settings) { + + protected val apiClient: ApiClient + + init { + assertConfig() + val authMap = initAuth() + apiClient = newApiClient(authMap) + + } + + fun oAuth(): OAuth { + return if (apiClient.authentications.containsKey(OAUTH2_LITERAL)) apiClient.getAuthentication(OAUTH2_LITERAL) as OAuth + else throw SignClientException( + "OAuth2 authentication not configured for REST client" + ) + } + + fun bearerAuth(): HttpBearerAuth { + return if (apiClient.authentications.containsKey(BEARER_LITERAL)) apiClient.getAuthentication(BEARER_LITERAL) as HttpBearerAuth + else throw SignClientException( + "Bearer auth not configured for REST client" + ) + } + + private fun newApiClient(authMap: MutableMap): ApiClient { + val apiClient = KotlinApiClient(authMap) + + apiClient.basePath = restClientConfig.baseUrl + if (restClientConfig.connectTimeoutInMS != null) { + apiClient.connectTimeout = restClientConfig.connectTimeoutInMS + } + if (restClientConfig.readTimeoutInMS != null) { + apiClient.readTimeout = restClientConfig.readTimeoutInMS + } + restClientConfig.defaultHeaders?.forEach { + apiClient.addDefaultHeader(it.key, it.value) + } + return apiClient + } + + private fun initAuth(): MutableMap { + val authMap = mutableMapOf() + if (restClientConfig.oAuth2 != null) { + val auth = OAuth( + restClientConfig.baseUrl ?: throw SignClientException("Base url for the REST Signature service has not been set"), + restClientConfig.oAuth2.tokenUrl + ) + auth.setFlow(restClientConfig.oAuth2.flow) + auth.setScope(restClientConfig.oAuth2.scope) + auth.setCredentials(restClientConfig.oAuth2.clientId, restClientConfig.oAuth2.clientSecret, restClientConfig.oAuth2.debug) + restClientConfig.oAuth2.accessToken?.let { auth.setAccessToken(it) } + authMap[OAUTH2_LITERAL] = auth + } + if (restClientConfig.bearerAuth != null) { + val auth = HttpBearerAuth(restClientConfig.bearerAuth.schema) + auth.bearerToken = restClientConfig.bearerAuth.bearerToken + authMap[BEARER_LITERAL] = auth + } + return authMap + } + + private fun assertConfig() { + if (restClientConfig.baseUrl == null) { + throw SignClientException("Cannot create a REST certificate Service Provider without a base URL") + } + } +} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/KotlinApiClient.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/KotlinApiClient.kt new file mode 100644 index 0000000..cc010ff --- /dev/null +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/KotlinApiClient.kt @@ -0,0 +1,11 @@ +package com.sphereon.vdx.ades.pki.restclient + +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import com.sphereon.vdx.ades.rest.client.ApiClient +import com.sphereon.vdx.ades.rest.client.auth.Authentication + +class KotlinApiClient(authMap: MutableMap?) : ApiClient(authMap) { + init { + json.mapper.registerKotlinModule() + } +} diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/RestClientConfig.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/RestClientConfig.kt similarity index 85% rename from src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/RestClientConfig.kt rename to src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/RestClientConfig.kt index 1b835d5..7ee1adc 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/RestClientConfig.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/RestClientConfig.kt @@ -1,4 +1,4 @@ -package com.sphereon.vdx.ades.pki +package com.sphereon.vdx.ades.pki.restclient import com.sphereon.vdx.ades.rest.client.auth.OAuthFlow @@ -8,7 +8,8 @@ data class RestClientConfig( val connectTimeoutInMS: Int? = 5000, val readTimeoutInMS: Int? = 10000, val oAuth2: OAuth2Config? = null, - val bearerAuth: BearerTokenConfig? = null + val bearerAuth: BearerTokenConfig? = null, + val defaultHeaders: Map? = null ) @kotlinx.serialization.Serializable diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/RestClientKeyProviderService.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/RestClientKeyProviderService.kt similarity index 53% rename from src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/RestClientKeyProviderService.kt rename to src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/RestClientKeyProviderService.kt index f1d217c..e39818a 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/RestClientKeyProviderService.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/pki/restclient/RestClientKeyProviderService.kt @@ -1,22 +1,14 @@ -package com.sphereon.vdx.ades.pki +package com.sphereon.vdx.ades.pki.restclient -import AbstractCacheObjectSerializer import com.sphereon.vdx.ades.PKIException import com.sphereon.vdx.ades.SignClientException import com.sphereon.vdx.ades.enums.CryptoAlg import com.sphereon.vdx.ades.enums.KeyProviderType import com.sphereon.vdx.ades.enums.MaskGenFunction import com.sphereon.vdx.ades.enums.SignatureAlg -import com.sphereon.vdx.ades.model.KeyProviderSettings -import com.sphereon.vdx.ades.model.IKeyEntry -import com.sphereon.vdx.ades.model.SignInput -import com.sphereon.vdx.ades.model.Signature -import com.sphereon.vdx.ades.model.KeyEntry -import com.sphereon.vdx.ades.rest.client.ApiClient +import com.sphereon.vdx.ades.model.* import com.sphereon.vdx.ades.rest.client.api.KeysApi import com.sphereon.vdx.ades.rest.client.api.SigningApi -import com.sphereon.vdx.ades.rest.client.auth.HttpBearerAuth -import com.sphereon.vdx.ades.rest.client.auth.OAuth import com.sphereon.vdx.ades.rest.client.model.ConfigKeyBinding import com.sphereon.vdx.ades.rest.client.model.CreateSignature import com.sphereon.vdx.ades.rest.client.model.DigestAlgorithm @@ -26,22 +18,13 @@ import com.sphereon.vdx.ades.sign.util.toCertificate import com.sphereon.vdx.ades.sign.util.toKey import org.apache.http.HttpStatus -private const val BEARER_LITERAL = "bearer" -private const val OAUTH2_LITERAL = "oauth2" - open class RestClientKeyProviderService( settings: KeyProviderSettings, - val restClientConfig: RestClientConfig, - cacheObjectSerializer: AbstractCacheObjectSerializer? = null -) : - AbstractKeyProviderService(settings, cacheObjectSerializer) { - - private val apiClient: ApiClient + restClientConfig: RestClientConfig, +) : AbstractRestClientKeyProviderService(settings, restClientConfig) { init { assertRestSettings() - apiClient = newApiClient() - initAuth() } override fun createSignatureImpl(signInput: SignInput, keyEntry: IKeyEntry, mgf: MaskGenFunction?): Signature { @@ -55,9 +38,10 @@ open class RestClientKeyProviderService( .signMode(SignMode.valueOf(signInput.signMode.name)) .digestAlgorithm(signInput.digestAlgorithm?.name?.let { DigestAlgorithm.valueOf(it) }) .signingDate(java.time.Instant.ofEpochSecond(signInput.signingDate.epochSeconds)) - .binding(ConfigKeyBinding() - .kid(keyEntry.kid) - .keyProviderId(settings.id) + .binding( + ConfigKeyBinding() + .kid(keyEntry.kid) + .keyProviderId(settings.id) ) ) ) @@ -103,20 +87,6 @@ open class RestClientKeyProviderService( return key } - fun oAuth(): OAuth { - return if (apiClient.authentications.containsKey(OAUTH2_LITERAL)) apiClient.getAuthentication(OAUTH2_LITERAL) as OAuth - else throw SignClientException( - "OAuth2 authentication not configured for REST client" - ) - } - - fun bearerAuth(): HttpBearerAuth { - return if (apiClient.authentications.containsKey(BEARER_LITERAL)) apiClient.getAuthentication(BEARER_LITERAL) as HttpBearerAuth - else throw SignClientException( - "Bearer auth not configured for REST client" - ) - } - fun newKeysApi(): KeysApi { return KeysApi(apiClient) } @@ -125,49 +95,11 @@ open class RestClientKeyProviderService( return SigningApi(apiClient) } - private fun newApiClient(): ApiClient { - val apiClient = ApiClient() - - apiClient.basePath = restClientConfig.baseUrl - if (restClientConfig.connectTimeoutInMS != null) { - apiClient.connectTimeout = restClientConfig.connectTimeoutInMS - } - if (restClientConfig.readTimeoutInMS != null) { - apiClient.readTimeout = restClientConfig.readTimeoutInMS - } - return apiClient - } - - private fun initAuth() { - if (restClientConfig.oAuth2 != null) { - val auth = OAuth( - restClientConfig.baseUrl ?: throw SignClientException("Base url for the REST Signature service has not been set"), - restClientConfig.oAuth2.tokenUrl - ) - auth.setFlow(restClientConfig.oAuth2.flow) - auth.setScope(restClientConfig.oAuth2.scope) - auth.setCredentials(restClientConfig.oAuth2.clientId, restClientConfig.oAuth2.clientSecret, restClientConfig.oAuth2.debug) - auth.setAccessToken(restClientConfig.oAuth2.accessToken) - apiClient.authentications[OAUTH2_LITERAL] = auth - } - if (restClientConfig.bearerAuth != null) { - val auth = HttpBearerAuth(restClientConfig.bearerAuth.schema) - auth.bearerToken = restClientConfig.bearerAuth.bearerToken - apiClient.authentications[BEARER_LITERAL] = auth - } - } - private fun assertRestSettings() { if (settings.config.type != KeyProviderType.REST) { throw SignClientException( "Cannot create a REST certificate Service Provider without mode set to REST. Current mode: ${settings.config.type}" ) } - /*if (restClientConfig == null) { - throw SignClientException("Cannot create a REST certificate Service Provider without a REST config") - }*/ - if (restClientConfig.baseUrl == null) { - throw SignClientException("Cannot create a REST certificate Service Provider without a base URL") - } } } diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/CertificateUtil.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/CertificateUtil.kt index dfea16b..8dde081 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/CertificateUtil.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/CertificateUtil.kt @@ -56,12 +56,16 @@ object CertificateUtil { fun toKeyEntry(x509Certificate: X509Certificate, kid: String): IKeyEntry { val certificate = toCertificate(x509Certificate) + val x509Chain = mutableListOf() + x509Chain.add(x509Certificate) + x509Chain.addAll(downloadExtraCertificates(x509Certificate)) + return KeyEntry( kid = kid, publicKey = x509Certificate.toPublicKey(), certificate = certificate, - encryptionAlgorithm = CryptoAlg.valueOf(x509Certificate.sigAlgName) - // TODO: certChain + encryptionAlgorithm = CryptoAlg.from(x509Certificate.sigAlgName), + certificateChain = x509Chain.map { it.toCertificate() } ) } diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/ConnectionFactory.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/ConnectionFactory.kt index 06ba33f..51aa101 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/ConnectionFactory.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/ConnectionFactory.kt @@ -5,7 +5,7 @@ import com.sphereon.vdx.ades.enums.KeyProviderType import com.sphereon.vdx.ades.model.KeyProviderConfig import com.sphereon.vdx.ades.model.KeyProviderSettings import com.sphereon.vdx.ades.model.PasswordInputCallback -import com.sphereon.vdx.ades.pki.AzureKeyvaultClientConfig +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultClientConfig import eu.europa.esig.dss.token.SignatureTokenConnection import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.Security diff --git a/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/Mapper.kt b/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/Mapper.kt index 424d74f..1449543 100644 --- a/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/Mapper.kt +++ b/src/jvmMain/kotlin/com/sphereon/vdx/ades/sign/util/Mapper.kt @@ -35,9 +35,9 @@ import com.sphereon.vdx.ades.model.SignatureParameters import com.sphereon.vdx.ades.model.VisualSignatureFieldParameters import com.sphereon.vdx.ades.model.VisualSignatureParameters import com.sphereon.vdx.ades.model.VisualSignatureTextParameters -import com.sphereon.vdx.ades.pki.AzureKeyvaultClientConfig -import com.sphereon.vdx.ades.pki.AzureKeyvaultTokenConnection import com.sphereon.vdx.ades.pki.DSSWrappedKeyEntry +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultClientConfig +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultTokenConnection import com.sphereon.vdx.pkcs7.PKCS7Service import com.sphereon.vdx.pkcs7.PKCS7SignatureParameters import eu.europa.esig.dss.AbstractSignatureParameters @@ -73,6 +73,7 @@ import eu.europa.esig.dss.token.DSSPrivateKeyEntry import eu.europa.esig.dss.token.KSPrivateKeyEntry import eu.europa.esig.dss.token.Pkcs11SignatureToken import eu.europa.esig.dss.token.Pkcs12SignatureToken +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.toJavaInstant import java.awt.Color @@ -86,8 +87,7 @@ import java.security.cert.X509Certificate import java.security.spec.AlgorithmParameterSpec import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec -import java.util.Date -import java.util.TimeZone +import java.util.* fun DigestAlg.toDSS(): DigestAlgorithm { @@ -190,7 +190,7 @@ fun X509Certificate.toCertificate(): Certificate { } fun PublicKey.toKey(): Key { - return Key(algorithm = CryptoAlg.valueOf(algorithm), value = encoded, format = format) + return Key(algorithm = CryptoAlg.from(algorithm), value = encoded, format = format) } fun X509Certificate.toPublicKey(): Key { @@ -200,6 +200,10 @@ fun X509Certificate.toPublicKey(): Key { fun Certificate.toX509Certificate(): X509Certificate { return CertificateUtil.toX509Certificate(this) } +fun Certificate.isActive(): Boolean { + val now = Clock.System.now() + return now in notBefore..notAfter +} fun Pkcs11Parameters.toPkcs11SignatureToken(): Pkcs11SignatureToken { return Pkcs11SignatureToken("FIXME") diff --git a/src/jvmTest/kotlin/AbstractAdESTest.kt b/src/jvmTest/kotlin/AbstractAdESTest.kt index 2cfd474..6f75684 100644 --- a/src/jvmTest/kotlin/AbstractAdESTest.kt +++ b/src/jvmTest/kotlin/AbstractAdESTest.kt @@ -25,8 +25,7 @@ abstract class AbstractAdESTest { id = "pkcs12", providerConfig, passwordInputCallback - ), - KeyEntryCacheSerializer() + ) ) } diff --git a/src/jvmTest/kotlin/KeyEntryCacheSerializer.kt b/src/jvmTest/kotlin/KeyEntryCacheSerializer.kt deleted file mode 100644 index b4ef83f..0000000 --- a/src/jvmTest/kotlin/KeyEntryCacheSerializer.kt +++ /dev/null @@ -1,28 +0,0 @@ -import com.sphereon.vdx.ades.model.IKeyEntry -import com.sphereon.vdx.ades.model.PrivateKeyEntry -import kotlinx.serialization.KSerializer -import org.ehcache.config.builders.CacheConfigurationBuilder -import org.ehcache.config.builders.ExpiryPolicyBuilder -import org.ehcache.config.builders.ResourcePoolsBuilder -import org.ehcache.config.units.MemoryUnit -import org.ehcache.jsr107.Eh107Configuration -import org.ehcache.spi.serialization.Serializer -import java.time.Duration -import javax.cache.configuration.Configuration - - -class KeyEntryCacheSerializer : Serializer, - AbstractCacheObjectSerializer(serializer = PrivateKeyEntry.serializer() as KSerializer) { - override fun cacheConfiguration(cacheTTLInSeconds: Long?): Configuration { - return Eh107Configuration.fromEhcacheCacheConfiguration( - CacheConfigurationBuilder.newCacheConfigurationBuilder( - String::class.java, IKeyEntry::class.java, ResourcePoolsBuilder - .heap(10) - .offheap(5, MemoryUnit.MB) - ) - .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(cacheTTLInSeconds ?: 30))) - .withValueSerializer(this) - - ) - } -} diff --git a/src/jvmTest/kotlin/RestClientSignatureServiceTest.kt b/src/jvmTest/kotlin/RestClientSignatureServiceTest.kt index febe53d..c8549ca 100644 --- a/src/jvmTest/kotlin/RestClientSignatureServiceTest.kt +++ b/src/jvmTest/kotlin/RestClientSignatureServiceTest.kt @@ -8,8 +8,8 @@ import com.sphereon.vdx.ades.model.KeyEntry import com.sphereon.vdx.ades.model.OrigData import com.sphereon.vdx.ades.model.SignInput import com.sphereon.vdx.ades.model.Signature -import com.sphereon.vdx.ades.pki.RestClientConfig -import com.sphereon.vdx.ades.pki.RestClientKeyProviderService +import com.sphereon.vdx.ades.pki.restclient.RestClientConfig +import com.sphereon.vdx.ades.pki.restclient.RestClientKeyProviderService import com.sphereon.vdx.ades.rest.client.JSON import com.sphereon.vdx.ades.rest.client.api.SigningApi import com.sphereon.vdx.ades.rest.client.model.* @@ -21,7 +21,8 @@ import io.mockk.spyk import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import java.time.Instant -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertNotNull class RestClientSignatureServiceTest { @Test diff --git a/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultCertificateProviderServiceTest.kt b/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultCertificateProviderServiceTest.kt index 55a7281..02b1b3b 100644 --- a/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultCertificateProviderServiceTest.kt +++ b/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/AzureKeyvaultCertificateProviderServiceTest.kt @@ -1,7 +1,6 @@ package com.sphereon.vdx.ades.pki import AbstractAdESTest -import KeyEntryCacheSerializer import com.sphereon.vdx.ades.enums.CryptoAlg import com.sphereon.vdx.ades.enums.DigestAlg import com.sphereon.vdx.ades.enums.KeyProviderType @@ -27,6 +26,12 @@ import com.sphereon.vdx.ades.model.TimestampParameters import com.sphereon.vdx.ades.model.VisualSignatureFieldParameters import com.sphereon.vdx.ades.model.VisualSignatureParameters import com.sphereon.vdx.ades.model.VisualSignatureTextParameters +import com.sphereon.vdx.ades.pki.azure.AzureKeyvaultClientConfig +import com.sphereon.vdx.ades.pki.azure.CredentialMode +import com.sphereon.vdx.ades.pki.azure.CredentialOpts +import com.sphereon.vdx.ades.pki.azure.ExponentialBackoffRetryOpts +import com.sphereon.vdx.ades.pki.azure.HSMType +import com.sphereon.vdx.ades.pki.azure.SecretCredentialOpts import com.sphereon.vdx.ades.sign.KidSignatureService import com.sphereon.vdx.ades.sign.util.toX509Certificate import eu.europa.esig.dss.enumerations.TokenExtractionStrategy @@ -53,11 +58,9 @@ class AzureKeyvaultCertificateProviderServiceTest : AbstractAdESTest() { @Test fun `Given a KID the Azure Keyvault Certificate Provider Service should return a key`() { - val keyProvider = KeyProviderServiceFactory.createFromConfig( - constructCertificateProviderSettings(true), - azureKeyvaultClientConfig = constructKeyvaultClientConfig(), - cacheObjectSerializer = KeyEntryCacheSerializer() - ) + val keyProvider = KeyProviderServiceFactory.createFromConfig(constructCertificateProviderSettings(true)) { + azureKeyvaultClientConfig = constructKeyvaultClientConfig() + } val key = keyProvider.getKey("esignum:3f98a9a740fb41b79e3679cce7a34ba6") assertNotNull(key) @@ -96,9 +99,9 @@ class AzureKeyvaultCertificateProviderServiceTest : AbstractAdESTest() { val logoData = OrigData(value = logo.readBytes(), name = "sphereon.png", mimeType = "image/png") - val keyProvider = KeyProviderServiceFactory.createFromConfig( - constructCertificateProviderSettings(false), azureKeyvaultClientConfig = constructKeyvaultClientConfig() - ) + val keyProvider = KeyProviderServiceFactory.createFromConfig(constructCertificateProviderSettings(false)) { + azureKeyvaultClientConfig = constructKeyvaultClientConfig() + } val signingService = KidSignatureService(keyProvider) val kid = "esignum:3f98a9a740fb41b79e3679cce7a34ba6" val signatureConfiguration = SignatureConfiguration( @@ -226,9 +229,9 @@ class AzureKeyvaultCertificateProviderServiceTest : AbstractAdESTest() { val logoData = OrigData(value = logo.readBytes(), name = "sphereon.png", mimeType = "image/png") - val keyProvider = KeyProviderServiceFactory.createFromConfig( - constructCertificateProviderSettings(false), azureKeyvaultClientConfig = constructKeyvaultClientConfig() - ) + val keyProvider = KeyProviderServiceFactory.createFromConfig(constructCertificateProviderSettings(false)) { + azureKeyvaultClientConfig = constructKeyvaultClientConfig() + } val signingService = KidSignatureService(keyProvider) val kid = "esignum:3f98a9a740fb41b79e3679cce7a34ba6" val signatureConfiguration = SignatureConfiguration( diff --git a/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/DigidentityProviderTest.kt b/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/DigidentityProviderTest.kt new file mode 100644 index 0000000..8234571 --- /dev/null +++ b/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/DigidentityProviderTest.kt @@ -0,0 +1,360 @@ +package com.sphereon.vdx.ades.pki + +import AbstractAdESTest +import com.sphereon.vdx.ades.enums.* +import com.sphereon.vdx.ades.model.* +import com.sphereon.vdx.ades.pki.digidentity.DigidentityCredentialMode +import com.sphereon.vdx.ades.pki.digidentity.DigidentityCredentialOpts +import com.sphereon.vdx.ades.pki.digidentity.DigidentityProviderConfig +import com.sphereon.vdx.ades.pki.digidentity.DigidentitySecretCredentialOpts +import com.sphereon.vdx.ades.sign.KidSignatureService +import com.sphereon.vdx.ades.sign.util.toX509Certificate +import eu.europa.esig.dss.enumerations.TokenExtractionStrategy +import eu.europa.esig.dss.model.InMemoryDocument +import eu.europa.esig.dss.model.x509.CertificateToken +import eu.europa.esig.dss.pades.validation.PDFDocumentValidator +import eu.europa.esig.dss.spi.x509.CommonTrustedCertificateSource +import eu.europa.esig.dss.validation.CommonCertificateVerifier +import eu.europa.esig.dss.validation.executor.ValidationLevel +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import org.junit.jupiter.api.Test +import java.io.ByteArrayOutputStream +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class DigidentityProviderTest : AbstractAdESTest() { + @Test + fun `Given a KID the Azure Keyvault Certificate Provider Service should return a key`() { + val keyProvider = KeyProviderServiceFactory.createFromConfig(constructCertificateProviderSettings(true)) { + digidentityProviderConfig = constructProviderConfig() + } + val key = keyProvider.getKey("9b2b85df-9149-4440-a7a7-67953a38b832") + + assertNotNull(key) + assertEquals("9b2b85df-9149-4440-a7a7-67953a38b832", key.kid) + assertNotNull(key.publicKey) + assertEquals("X.509", key.publicKey.format) + assertEquals(CryptoAlg.RSA, key.publicKey.algorithm) + assertEquals("D5D0075C981C4462BAB99737E9BE0C49F750BB63", key.certificate?.fingerPrint) + assertEquals( + "C=NL, O=Digidentity B.V., OID.2.5.4.97=NTRNL-27322631, CN=TEST Digidentity Business Qualified CA", + key.certificate?.issuerDN + ) + assertEquals( + "C=NL, O=Regional Sanjoflex, OID.2.5.4.97=NTRNL-90002768, CN=Regional Sanjoflex", + key.certificate?.subjectDN + ) + assertEquals("70155151975609048911381342004623025095", key.certificate?.serialNumber) + assertNotNull(key.certificate?.keyUsage) + assertEquals(9, key.certificate?.keyUsage!!.size) + assertEquals(false, key.certificate?.keyUsage!!["digitalSignature"]) // TODO Double-check if this shouldn't be true + assertEquals(true, key.certificate?.keyUsage!!["nonRepudiation"]) + assertEquals(LocalDateTime.parse("2024-02-19T11:05:18").toInstant(TimeZone.UTC), key.certificate?.notBefore) + assertEquals(LocalDateTime.parse("2025-02-18T11:05:17").toInstant(TimeZone.UTC), key.certificate?.notAfter) // TODO Hmmz this will assure the build will fail next year + + assertNotNull(key.certificateChain) + assertEquals(3, key.certificateChain!!.size) + // We already tested a certificate above. So we only test for proper order of the cert chain here + assertEquals("D5D0075C981C4462BAB99737E9BE0C49F750BB63", key.certificateChain!![0].fingerPrint) + assertEquals("9F9CFCE17EA78D9510D9A598453DC05BCC532053", key.certificateChain!![1].fingerPrint) + assertEquals("719AFB0F5D19A3F3FD64E7D7065E9147328EBA6C", key.certificateChain!![2].fingerPrint) + } + + + + @Test + fun `PAdES - Given an input with signmode DOCUMENT and DIGEST the sign method should sign the document `() { + val pdfDocInput = this::class.java.classLoader.getResource("test-unsigned.pdf") + val logo = this::class.java.classLoader.getResource("logo.png") + val pdfData = OrigData(value = pdfDocInput.readBytes(), name = "test-unsigned.pdf") + val logoData = OrigData(value = logo.readBytes(), name = "sphereon.png", mimeType = "image/png") + + + val keyProvider = KeyProviderServiceFactory.createFromConfig(constructCertificateProviderSettings(true)) { + digidentityProviderConfig = constructProviderConfig() + } + val signingService = KidSignatureService(keyProvider) + val kid = "9b2b85df-9149-4440-a7a7-67953a38b832" + val signatureConfiguration = SignatureConfiguration( + + signatureParameters = SignatureParameters( + signaturePackaging = SignaturePackaging.ENVELOPED, + digestAlgorithm = DigestAlg.SHA256, + encryptionAlgorithm = CryptoAlg.RSA, + signatureAlgorithm = SignatureAlg.RSA_SHA256, + signatureLevelParameters = SignatureLevelParameters( + signatureLevel = SignatureLevel.PAdES_BASELINE_LT, bLevelParameters = BLevelParams( +// signingDate = Instant.parse(SIGDATE) + ) + ), + signatureFormParameters = SignatureFormParameters( + padesSignatureFormParameters = PadesSignatureFormParameters( + mode = PdfSignatureMode.CERTIFICATION, + signerName = "Test Case", + contactInfo = "support@sphereon.com", + reason = "Test", + location = "Online", + signatureSize = 15000, + signatureSubFilter = PdfSignatureSubFilter.ETSI_CADES_DETACHED.specName, + signingTimeZone = "GMT-3", + visualSignatureParameters = VisualSignatureParameters( + fieldParameters = VisualSignatureFieldParameters( +// fieldId = "SigNK", + originX = 50f, + originY = 400f, + ), image = logoData, +// rotation = VisualSignatureRotation.ROTATE_90, + textParameters = VisualSignatureTextParameters( + text = "Niels Klomp\r\nCTO", signerTextPosition = SignerTextPosition.TOP + ) + + ) + + ) + ) + ), timestampParameters = TimestampParameters( + tsaUrl = "http://timestamping.ensuredca.com/", baselineLTAArchiveTimestampParameters = TimestampParameterSettings( + digestAlgorithm = DigestAlg.SHA256 + ) + ) + ) + val signInput = signingService.determineSignInput( + origData = pdfData, kid = kid, signMode = SignMode.DOCUMENT, signatureConfiguration = signatureConfiguration + ) + +// println(Json { prettyPrint = true; serializersModule = serializers }.encodeToString(signInput)) + + // Let's first create a signature of the document/data without creating a digest + val signatureData = signingService.createSignature(signInput, SignatureAlg.RSA_SHA256) + assertNotNull(signatureData) + assertEquals(SignMode.DOCUMENT, signatureData.signMode) + assertEquals(SignatureAlg.RSA_SHA256, signatureData.algorithm) + + // Let's create a digest ourselves and sign that as well + val digestInput = signingService.digest(signInput) + val signatureDigest = signingService.createSignature(digestInput, SignatureAlg.RSA_SHA256) + assertNotNull(signatureDigest) + assertEquals(SignMode.DIGEST, signatureDigest.signMode) + assertEquals(SignatureAlg.RSA_SHA256, signatureDigest.algorithm) + + assertContentEquals(signatureData.value, signatureDigest.value) + assertEquals(signatureData.keyEntry.certificate!!.fingerPrint, signatureDigest.keyEntry.certificate!!.fingerPrint) + assertEquals(signatureData.keyEntry.certificateChain!![2].fingerPrint, signatureDigest.keyEntry.certificateChain!![2].fingerPrint) + + val signOutputData = signingService.sign(pdfData, signatureData, signatureConfiguration) + assertNotNull(signOutputData) + + val signOutputDigest = signingService.sign(pdfData, signatureDigest, signatureConfiguration) + assertNotNull(signOutputDigest) + + + InMemoryDocument(signOutputDigest.value, signOutputData.name).save("" + System.currentTimeMillis() + "-sphereon-signed.pdf") + + val validSignatureData = signingService.isValidSignature(signInput, signatureData) + assertTrue(validSignatureData) + + val validSignatureDigest = signingService.isValidSignature(digestInput, signatureDigest) + assertTrue(validSignatureDigest) + + assertTrue(signingService.isValidSignature(signInput, signatureData, signatureData.keyEntry.publicKey)) + assertTrue(signingService.isValidSignature(digestInput, signatureDigest, signatureDigest.keyEntry.publicKey)) + val documentValidator = PDFDocumentValidator( + InMemoryDocument( + signOutputData.value, signOutputData.name + ) + ) + documentValidator.setValidationLevel(ValidationLevel.BASIC_SIGNATURES) + documentValidator.setTokenExtractionStrategy(TokenExtractionStrategy.EXTRACT_CERTIFICATES_AND_REVOCATION_DATA) + + val origDoc = documentValidator.getOriginalDocuments(documentValidator.signatures.first()).first() + ByteArrayOutputStream().use { baos -> + origDoc.writeTo(baos) + assertContentEquals(pdfData.value, baos.toByteArray()) + } + + val certVerifier = CommonCertificateVerifier() + + // Create an instance of a trusted certificate source + val trustedCertSource = CommonTrustedCertificateSource() + // Include the chain, but not the signing cert itself + signatureDigest.keyEntry.certificateChain!!.subList(1, 3).map { trustedCertSource.addCertificate( + CertificateToken(it.toX509Certificate()) + ) } + // Add trust anchors (trusted list, keystore,...) to a list of trusted certificate sources + certVerifier.addTrustedCertSources(trustedCertSource) + documentValidator.setCertificateVerifier(certVerifier) + + assertEquals(1, documentValidator.signatures.size) + + /*val diagData = documentValidator.diagnosticData FIXME offlineCertificateVerifier cannot be null! + assertEquals(1, diagData.signatures.size) + assertEquals(7, diagData.usedCertificates.size)*/ + + assertContentEquals(signatureDigest.value, documentValidator.signatures.first().signatureValue) + + } + + @Test + fun `PKCS7 - Given an input with signmode DOCUMENT and DIGEST the sign method should sign the document `() { + val pdfDocInput = this::class.java.classLoader.getResource("test-unsigned.pdf") + val logo = this::class.java.classLoader.getResource("logo.png") + val pdfData = OrigData(value = pdfDocInput.readBytes(), name = "test-unsigned.pdf") + val logoData = OrigData(value = logo.readBytes(), name = "sphereon.png", mimeType = "image/png") + + + val keyProvider = KeyProviderServiceFactory.createFromConfig(constructCertificateProviderSettings(false)) { + digidentityProviderConfig = constructProviderConfig() + } + val signingService = KidSignatureService(keyProvider) + val kid = "9b2b85df-9149-4440-a7a7-67953a38b832" + val signatureConfiguration = SignatureConfiguration( + + signatureParameters = SignatureParameters( + signaturePackaging = SignaturePackaging.ENVELOPED, + digestAlgorithm = DigestAlg.SHA256, + encryptionAlgorithm = CryptoAlg.RSA, + signatureAlgorithm = SignatureAlg.RSA_SHA256, + signatureLevelParameters = SignatureLevelParameters( + signatureLevel = SignatureLevel.PKCS7_LT, bLevelParameters = BLevelParams( +// signingDate = Instant.parse(SIGDATE) + ) + ), + signatureFormParameters = SignatureFormParameters( + pkcs7SignatureFormParameters = Pkcs7SignatureFormParameters( + mode = PdfSignatureMode.CERTIFICATION, + signerName = "Test Case", + contactInfo = "support@sphereon.com", + reason = "Test", + location = "Online", + signatureSize = 15000, // FIXME, this value gets lost somehow + signatureSubFilter = PdfSignatureSubFilter.ADBE_PKCS7_DETACHED.specName, + signingTimeZone = "GMT-3", + visualSignatureParameters = VisualSignatureParameters( + fieldParameters = VisualSignatureFieldParameters( +// fieldId = "SigNK", + originX = 50f, + originY = 400f, + ), image = logoData, +// rotation = VisualSignatureRotation.ROTATE_90, + textParameters = VisualSignatureTextParameters( + text = "Niels Klomp\r\nCTO", signerTextPosition = SignerTextPosition.TOP + ) + + ) + + ) + ) + ), timestampParameters = TimestampParameters( + tsaUrl = "http://timestamping.ensuredca.com/", baselineLTAArchiveTimestampParameters = TimestampParameterSettings( + digestAlgorithm = DigestAlg.SHA256 + ) + ) + ) + val signInput = signingService.determineSignInput( + origData = pdfData, kid = kid, signMode = SignMode.DOCUMENT, signatureConfiguration = signatureConfiguration + ) + +// println(Json { prettyPrint = true; serializersModule = serializers }.encodeToString(signInput)) + + // Let's first create a signature of the document/data without creating a digest + val signatureData = signingService.createSignature(signInput, SignatureAlg.RSA_SHA256) + assertNotNull(signatureData) + assertEquals(SignMode.DOCUMENT, signatureData.signMode) + assertEquals(SignatureAlg.RSA_SHA256, signatureData.algorithm) + + // Let's create a digest ourselves and sign that as well + val digestInput = signingService.digest(signInput) + val signatureDigest = signingService.createSignature(digestInput, SignatureAlg.RSA_SHA256) + assertNotNull(signatureDigest) + assertEquals(SignMode.DIGEST, signatureDigest.signMode) + assertEquals(SignatureAlg.RSA_SHA256, signatureDigest.algorithm) + + assertContentEquals(signatureData.value, signatureDigest.value) + assertEquals(signatureData.keyEntry.certificate!!.fingerPrint, signatureDigest.keyEntry.certificate!!.fingerPrint) + assertEquals(signatureData.keyEntry.certificateChain!![2].fingerPrint, signatureDigest.keyEntry.certificateChain!![2].fingerPrint) + + val signOutputData = signingService.sign(pdfData, signatureData, signatureConfiguration) + assertNotNull(signOutputData) + + val signOutputDigest = signingService.sign(pdfData, signatureDigest, signatureConfiguration) + assertNotNull(signOutputDigest) + + + InMemoryDocument(signOutputDigest.value, signOutputData.name).save("" + System.currentTimeMillis() + "-sphereon-signed.pdf") + + val validSignatureData = signingService.isValidSignature(signInput, signatureData) + assertTrue(validSignatureData) + + val validSignatureDigest = signingService.isValidSignature(digestInput, signatureDigest) + assertTrue(validSignatureDigest) + + assertTrue(signingService.isValidSignature(signInput, signatureData, signatureData.keyEntry.publicKey)) + assertTrue(signingService.isValidSignature(digestInput, signatureDigest, signatureDigest.keyEntry.publicKey)) + val documentValidator = PDFDocumentValidator( + InMemoryDocument( + signOutputData.value, signOutputData.name + ) + ) + documentValidator.setValidationLevel(ValidationLevel.BASIC_SIGNATURES) + documentValidator.setTokenExtractionStrategy(TokenExtractionStrategy.EXTRACT_CERTIFICATES_AND_REVOCATION_DATA) + + val origDoc = documentValidator.getOriginalDocuments(documentValidator.signatures.first()).first() + ByteArrayOutputStream().use { baos -> + origDoc.writeTo(baos) + assertContentEquals(pdfData.value, baos.toByteArray()) + } + + val certVerifier = CommonCertificateVerifier() + + // Create an instance of a trusted certificate source + val trustedCertSource = CommonTrustedCertificateSource() + // Include the chain, but not the signing cert itself + signatureDigest.keyEntry.certificateChain!!.subList(1, 3).map { trustedCertSource.addCertificate( + CertificateToken(it.toX509Certificate()) + ) } + // Add trust anchors (trusted list, keystore,...) to a list of trusted certificate sources + certVerifier.addTrustedCertSources(trustedCertSource) + documentValidator.setCertificateVerifier(certVerifier) + + assertEquals(1, documentValidator.signatures.size) + + val diagData = documentValidator.diagnosticData + assertEquals(1, diagData.signatures.size) + assertEquals(8, diagData.usedCertificates.size) + + + assertContentEquals(signatureDigest.value, documentValidator.signatures.first().signatureValue) + } + + + private fun constructProviderConfig(): DigidentityProviderConfig { + return DigidentityProviderConfig( + baseUrl = "https://api.digidentity-preproduction.eu/v1", + autoSignerId = "9b2b85df-9149-4440-a7a7-67953a38b832", + credentialOpts = DigidentityCredentialOpts( + credentialMode = DigidentityCredentialMode.SERVICE_CLIENT_SECRET, + secretCredentialOpts = DigidentitySecretCredentialOpts( + clientId = System.getenv("DG_CLIENT_ID"), + clientSecret = System.getenv("DG_CLIENT_SECRET"), + apiKey = System.getenv("DG_API_KEY"), + ) + ) + ) + } + + private fun constructCertificateProviderSettings( + enableCache: Boolean? = true + ): KeyProviderSettings { + return KeyProviderSettings( + id = "7e13564x-88am-0621-p4l5-56e7312344as", + config = KeyProviderConfig( + cacheEnabled = enableCache, + type = KeyProviderType.DIGIDENTITY + ) + ) + } +} diff --git a/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/RestCertificateProviderServiceTest.kt b/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/RestCertificateProviderServiceTest.kt index d4152c5..1effdbe 100644 --- a/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/RestCertificateProviderServiceTest.kt +++ b/src/jvmTest/kotlin/com/sphereon/vdx/ades/pki/RestCertificateProviderServiceTest.kt @@ -5,6 +5,8 @@ import com.sphereon.vdx.ades.enums.KeyProviderType import com.sphereon.vdx.ades.enums.SignMode import com.sphereon.vdx.ades.model.* import com.sphereon.vdx.ades.model.SignInput +import com.sphereon.vdx.ades.pki.restclient.RestClientConfig +import com.sphereon.vdx.ades.pki.restclient.RestClientKeyProviderService import com.sphereon.vdx.ades.rest.client.ApiResponse import com.sphereon.vdx.ades.rest.client.JSON import com.sphereon.vdx.ades.rest.client.api.KeysApi @@ -15,12 +17,15 @@ import com.sphereon.vdx.ades.rest.client.model.Signature import io.mockk.every import io.mockk.mockk import io.mockk.spyk -import kotlinx.datetime.* +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import java.time.Instant -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertNotNull class RestCertificateProviderServiceTest { @Test