From 491d497ab2591950a036487d9b09cbaf703b6fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 10 Jul 2023 11:41:22 +0200 Subject: [PATCH 01/11] add source links to docs (#7) * add source links to docs * fix source links pointing to wrong module --- vclib-aries/build.gradle.kts | 12 ++++++++++++ vclib-openid/build.gradle.kts | 12 ++++++++++++ vclib/build.gradle.kts | 16 +++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/vclib-aries/build.gradle.kts b/vclib-aries/build.gradle.kts index 44e78dc8b..ac018e54b 100644 --- a/vclib-aries/build.gradle.kts +++ b/vclib-aries/build.gradle.kts @@ -21,6 +21,18 @@ val dokkaOutputDir = "$buildDir/dokka" tasks.dokkaHtmlPartial{ dependsOn(":vclib:transformIosMainCInteropDependenciesMetadataForIde") dependsOn(":vclib-openid:transformIosMainCInteropDependenciesMetadataForIde") + dokkaSourceSets { + configureEach { + sourceLink { + localDirectory.set(file("src/$name/kotlin")) + remoteUrl.set( + uri("https://github.com/a-sit-plus/kmm-vc-library/tree/main/${project.name}/src/$name/kotlin").toURL() + ) + // Suffix which is used to append the line number to the URL. Use #L for GitHub + remoteLineSuffix.set("#L") + } + } + } } tasks.dokkaHtml { dependsOn(":vclib:transformIosMainCInteropDependenciesMetadataForIde") //task dependency bug workaround diff --git a/vclib-openid/build.gradle.kts b/vclib-openid/build.gradle.kts index 84ab275a0..bf0de650b 100644 --- a/vclib-openid/build.gradle.kts +++ b/vclib-openid/build.gradle.kts @@ -21,6 +21,18 @@ val dokkaOutputDir = "$buildDir/dokka" tasks.dokkaHtmlPartial{ dependsOn(":vclib-aries:transformIosMainCInteropDependenciesMetadataForIde") dependsOn(":vclib:transformIosMainCInteropDependenciesMetadataForIde") + dokkaSourceSets { + configureEach { + sourceLink { + localDirectory.set(file("src/$name/kotlin")) + remoteUrl.set( + uri("https://github.com/a-sit-plus/kmm-vc-library/tree/main/${project.name}/src/$name/kotlin").toURL() + ) + // Suffix which is used to append the line number to the URL. Use #L for GitHub + remoteLineSuffix.set("#L") + } + } + } } tasks.dokkaHtml { diff --git a/vclib/build.gradle.kts b/vclib/build.gradle.kts index 7c89c96e7..b99d5c510 100644 --- a/vclib/build.gradle.kts +++ b/vclib/build.gradle.kts @@ -16,14 +16,28 @@ version = artifactVersion val dokkaOutputDir = "$buildDir/dokka" -tasks.dokkaHtmlPartial{ +tasks.dokkaHtmlPartial { + dependsOn(":vclib:transformIosMainCInteropDependenciesMetadataForIde") + dokkaSourceSets { + configureEach { + sourceLink { + localDirectory.set(file("src/$name/kotlin")) + remoteUrl.set( + uri("https://github.com/a-sit-plus/kmm-vc-library/tree/main/${project.name}/src/$name/kotlin").toURL() + ) + // Suffix which is used to append the line number to the URL. Use #L for GitHub + remoteLineSuffix.set("#L") + } + } + } } tasks.dokkaHtml { dependsOn("transformIosMainCInteropDependenciesMetadataForIde") //wor around bug outputDirectory.set(file(dokkaOutputDir)) } + val deleteDokkaOutputDir by tasks.register("deleteDokkaOutputDirectory") { delete(dokkaOutputDir) } From ec618a0923950c4850fcf7f62daf9266cb700509 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Wed, 2 Aug 2023 12:11:35 +0200 Subject: [PATCH 02/11] Build: Publish under iOS? --- .github/workflows/build-ios.yml | 4 +--- .github/workflows/publish.yml | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 714eb63ae..a7e88f737 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -13,10 +13,8 @@ jobs: distribution: 'temurin' java-version: '11' - name: Build kotlinx.serialization - run: ./gradlew build :build -x :kotlinx-serialization-cbor:animalsnifferMain -x :kotlinx-serialization-cbor:jvmApiCheck -x :kotlinx-serialization-cbor:jsLegacyNode publishToMavenLocal + run: ./gradlew publishToMavenLocal working-directory: kotlinx.serialization - - name: Build klibs - run: ./gradlew iosArm64MainKlibrary iosX64MainKlibrary - name: Build XCFrameworks run: ./gradlew assembleVcLibKmmXCFramework assembleVcLibAriesKmmXCFramework assembleVcLibOpenIdKmmXCFramework - name: Upload debug XCFramework vclib diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b522cc9a0..8b71d4047 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,6 +12,9 @@ jobs: with: distribution: 'temurin' java-version: '11' + - name: Build kotlinx.serialization + run: ./gradlew publishToMavenLocal + working-directory: kotlinx.serialization - name: Publish to Sonatype run: ./gradlew clean publishToSonatype closeSonatypeStagingRepository env: From 042dac260e9100b512dc0fbfa52ba11bd6821ee8 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 3 Aug 2023 12:27:49 +0200 Subject: [PATCH 03/11] ISO: Transport issuer certificate in COSE structures --- vclib/build.gradle.kts | 2 +- .../wallet/lib/agent/CryptoService.kt | 6 + .../asitplus/wallet/lib/agent/HolderAgent.kt | 8 +- .../asitplus/wallet/lib/agent/IssuerAgent.kt | 4 +- .../at/asitplus/wallet/lib/agent/Validator.kt | 31 +++-- .../asitplus/wallet/lib/cbor/CoseService.kt | 33 +++-- .../wallet/lib/agent/DefaultCryptoService.kt | 14 ++ .../wallet/lib/agent/DefaultCryptoService.kt | 127 ++++++++++++------ 8 files changed, 155 insertions(+), 70 deletions(-) diff --git a/vclib/build.gradle.kts b/vclib/build.gradle.kts index e3586e1ac..41e91347a 100644 --- a/vclib/build.gradle.kts +++ b/vclib/build.gradle.kts @@ -53,7 +53,7 @@ kotlin { val iosSimulatorArm64Main by getting { dependsOn(iosMain) } val jvmMain by getting { dependencies { - implementation(bouncycastle("bcprov")) + implementation(bouncycastle("bcpkix")) } } val jvmTest by getting { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index 077ecf1f5..622ddc2c9 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -49,6 +49,11 @@ interface CryptoService { val coseAlgorithm: CoseAlgorithm + /** + * May be used in [at.asitplus.wallet.lib.cbor.CoseService] to transport the signing key for a COSE structure + */ + val certificate: ByteArray + fun toJsonWebKey(): JsonWebKey fun toCoseKey(): CoseKey @@ -75,6 +80,7 @@ interface VerifierCryptoService { expect object CryptoUtils { fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? + fun extractCoseKeyFromX509Cert(it: ByteArray): CoseKey? } data class AuthenticatedCiphertext(val ciphertext: ByteArray, val authtag: ByteArray) { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt index 6ea5b2a76..2f6455f9e 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt @@ -2,8 +2,6 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.cbor.CoseHeader -import at.asitplus.wallet.lib.cbor.CoseKey -import at.asitplus.wallet.lib.cbor.CoseKeyType import at.asitplus.wallet.lib.cbor.CoseService import at.asitplus.wallet.lib.cbor.DefaultCoseService import at.asitplus.wallet.lib.data.VerifiableCredentialJws @@ -100,8 +98,10 @@ class HolderAgent( } } credentialList.filterIsInstance().forEach { cred -> - // TODO Where to get the ISO issuer key? - when (val result = validator.verifyIsoCred(cred.issuerSigned, CoseKey(type = CoseKeyType.EC2))) { + val issuerKey = cred.issuerSigned.issuerAuth.unprotectedHeader?.certificateChain?.let { + CryptoUtils.extractCoseKeyFromX509Cert(it) + } + when (val result = validator.verifyIsoCred(cred.issuerSigned, issuerKey)) { is Verifier.VerifyCredentialResult.InvalidStructure -> rejected += result.input is Verifier.VerifyCredentialResult.Revoked -> rejected += result.input is Verifier.VerifyCredentialResult.SuccessIso -> acceptedIso += result.issuerSigned diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt index 6f0a4d407..be74c9838 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt @@ -107,6 +107,7 @@ class IssuerAgent( is CredentialToBeIssued.Iso -> { val expirationDate = credential.expiration val timePeriod = timePeriodProvider.getTimePeriodFor(issuanceDate) + // is unused, but needed to store the credential itself val statusListIndex = issuerCredentialStore.storeGetNextIndex( credential.issuerSignedItems, issuanceDate, @@ -139,9 +140,10 @@ class IssuerAgent( ), issuerAuth = coseService.createSignedCose( protectedHeader = CoseHeader(algorithm = CoseAlgorithm.ES256), - unprotectedHeader = null, // TODO transport issuer certificate + unprotectedHeader = null, payload = mso.serializeForIssuerAuth(), addKeyId = false, + addCertificate = true, ).getOrThrow() ) return Issuer.IssuedCredentialResult( diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt index 4cfec573d..723b2aadf 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt @@ -194,11 +194,16 @@ class Validator( } val issuerSigned = doc.issuerSigned val issuerAuth = issuerSigned.issuerAuth - // TODO Get Issuer Key somewhere - // if (verifierCoseService.verifyCose(issuerAuth, CoseKey(CoseKeyType.EC2)).getOrNull() != true) { - // return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) - // .also { Napier.w("IssuerAuth not verified: $issuerAuth") } - // } + + val issuerKey = issuerAuth.unprotectedHeader?.certificateChain?.let { + CryptoUtils.extractCoseKeyFromX509Cert(it) + } ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) + .also { Napier.w("Got no issuer key in $issuerAuth") } + + if (verifierCoseService.verifyCose(issuerAuth, issuerKey).getOrNull() != true) { + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) + .also { Napier.w("IssuerAuth not verified: $issuerAuth") } + } val mso = issuerSigned.getIssuerAuthPayloadAsMso() ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) @@ -215,7 +220,6 @@ class Validator( val deviceSignature = doc.deviceSigned.deviceAuth.deviceSignature ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) .also { Napier.w("DeviceSignature is null: ${doc.deviceSigned.deviceAuth}") } - // TODO Does the challenge need to be included in deviceSignature somehow? if (verifierCoseService.verifyCose(deviceSignature, walletKey).getOrNull() != true) { return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) @@ -295,14 +299,17 @@ class Validator( * * @param it The [IssuerSigned] structure from ISO 18013-5 */ - fun verifyIsoCred(it: IssuerSigned, issuerKey: CoseKey): Verifier.VerifyCredentialResult { + fun verifyIsoCred(it: IssuerSigned, issuerKey: CoseKey?): Verifier.VerifyCredentialResult { Napier.d("Verifying ISO Cred $it") + if (issuerKey == null) { + Napier.w("ISO: No issuer key") + return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeBase16()) + } val result = verifierCoseService.verifyCose(it.issuerAuth, issuerKey) - // TODO How to get the correct issuer key!? - //if (result.getOrNull() != true) { - // Napier.w("ISO: Could not verify credential", result.exceptionOrNull()) - // return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeBase16()) - //} + if (result.getOrNull() != true) { + Napier.w("ISO: Could not verify credential", result.exceptionOrNull()) + return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeBase16()) + } return Verifier.VerifyCredentialResult.SuccessIso(it) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt index ebff9dbb4..4a08f0808 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt @@ -14,14 +14,19 @@ import kotlinx.serialization.cbor.ByteStringWrapper interface CoseService { /** - * Appends correct values for [CoseHeader.kid], [CoseHeader.algorithm], - * if the corresponding options are set + * Creates and signs a new [CoseSigned] object, + * appends correct value for [CoseHeader.algorithm] into [protectedHeader]. + * + * @param addKeyId whether to set [CoseHeader.kid] in [protectedHeader] + * @param addCertificate whether to set [CoseHeader.certificateChain] in [unprotectedHeader] + * */ suspend fun createSignedCose( protectedHeader: CoseHeader, unprotectedHeader: CoseHeader? = null, payload: ByteArray? = null, addKeyId: Boolean = true, + addCertificate: Boolean = false, ): KmmResult } @@ -31,6 +36,11 @@ interface VerifierCoseService { } +/** + * Constant from RFC 9052 - CBOR Object Signing and Encryption (COSE) + */ +private const val SIGNATURE1_STRING = "Signature1" + class DefaultCoseService(private val cryptoService: CryptoService) : CoseService { override suspend fun createSignedCose( @@ -38,14 +48,21 @@ class DefaultCoseService(private val cryptoService: CryptoService) : CoseService unprotectedHeader: CoseHeader?, payload: ByteArray?, addKeyId: Boolean, + addCertificate: Boolean, ): KmmResult { - var copy = protectedHeader.copy(algorithm = cryptoService.coseAlgorithm) + var copyProtectedHeader = protectedHeader.copy(algorithm = cryptoService.coseAlgorithm) if (addKeyId) - copy = copy.copy(kid = cryptoService.identifier.encodeToByteArray()) + copyProtectedHeader = copyProtectedHeader.copy(kid = cryptoService.identifier.encodeToByteArray()) + + val copyUnprotectedHeader = if (addCertificate) { + (unprotectedHeader ?: CoseHeader()).copy(certificateChain = cryptoService.certificate) + } else { + unprotectedHeader + } val signatureInput = CoseSignatureInput( - contextString = "Signature1", - protectedHeader = ByteStringWrapper(copy), + contextString = SIGNATURE1_STRING, + protectedHeader = ByteStringWrapper(copyProtectedHeader), externalAad = byteArrayOf(), payload = payload, ).serialize() @@ -56,7 +73,7 @@ class DefaultCoseService(private val cryptoService: CryptoService) : CoseService } val rawSignature = signature.extractSignatureValues(cryptoService.coseAlgorithm.signatureValueLength) return KmmResult.success( - CoseSigned(ByteStringWrapper(copy), unprotectedHeader, payload, rawSignature) + CoseSigned(ByteStringWrapper(copyProtectedHeader), copyUnprotectedHeader, payload, rawSignature) ) } } @@ -70,7 +87,7 @@ class DefaultVerifierCoseService( */ override fun verifyCose(coseSigned: CoseSigned, signer: CoseKey): KmmResult { val signatureInput = CoseSignatureInput( - contextString = "Signature1", + contextString = SIGNATURE1_STRING, protectedHeader = ByteStringWrapper(coseSigned.protectedHeader.value), externalAad = byteArrayOf(), payload = coseSigned.payload, diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 2cc285426..f505e5964 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -13,6 +13,7 @@ import at.asitplus.wallet.lib.jws.JweEncryption import at.asitplus.wallet.lib.jws.JwkType import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsExtensions.convertToAsn1Signature +import io.ktor.http.content.ByteArrayContent import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.MemScope @@ -61,6 +62,7 @@ actual class DefaultCryptoService : CryptoService { private val publicKey: SecKeyRef private val jsonWebKey: JsonWebKey private val coseKey: CoseKey + final override val certificate: ByteArray actual constructor() { val query = CFDictionaryCreateMutable(null, 2, null, null).apply { @@ -73,6 +75,7 @@ actual class DefaultCryptoService : CryptoService { val data = CFBridgingRelease(publicKeyData) as NSData this.jsonWebKey = JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray())!! this.coseKey = CoseKey.fromAnsiX963Bytes(CoseKeyType.EC2, CoseEllipticCurve.P256, data.toByteArray())!! + this.certificate = byteArrayOf() // TODO How to create a self-signed certificate in Kotlin/iOS? } override suspend fun sign(input: ByteArray): KmmResult { @@ -228,6 +231,17 @@ actual object CryptoUtils { } } + actual fun extractCoseKeyFromX509Cert(it: ByteArray): CoseKey? { + memScoped { + val certData = CFBridgingRetain(toData(it)) as CFDataRef + val certificate = SecCertificateCreateWithData(null, certData) + val publicKey = SecCertificateCopyKey(certificate) + val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) + val data = CFBridgingRelease(publicKeyData) as NSData + return CoseKey.fromAnsiX963Bytes(CoseKeyType.EC2, CoseEllipticCurve.P256, data.toByteArray()) + } + } + } data class DefaultEphemeralKeyHolder(val publicKey: SecKeyRef, val privateKey: SecKeyRef? = null) : EphemeralKeyHolder { diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 4ef8dbe23..ffe3af3f8 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -3,19 +3,26 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.cbor.CoseEllipticCurve +import at.asitplus.wallet.lib.cbor.CoseEllipticCurve.P256 import at.asitplus.wallet.lib.cbor.CoseKey -import at.asitplus.wallet.lib.cbor.CoseKeyType +import at.asitplus.wallet.lib.cbor.CoseKeyType.EC2 import at.asitplus.wallet.lib.jws.EcCurve +import at.asitplus.wallet.lib.jws.EcCurve.SECP_256_R_1 import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm import at.asitplus.wallet.lib.jws.JweEncryption -import at.asitplus.wallet.lib.jws.JwkType +import at.asitplus.wallet.lib.jws.JwkType.EC import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsExtensions.convertToAsn1Signature import at.asitplus.wallet.lib.jws.JwsExtensions.ensureSize +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.JCEECPublicKey import org.bouncycastle.jce.spec.ECPublicKeySpec +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.bouncycastle.util.io.pem.PemReader import java.io.InputStream import java.math.BigInteger @@ -25,53 +32,75 @@ import java.security.KeyPairGenerator import java.security.MessageDigest import java.security.PublicKey import java.security.Signature +import java.security.cert.Certificate import java.security.cert.CertificateFactory import java.security.interfaces.ECPublicKey import java.security.spec.X509EncodedKeySpec +import java.time.Instant +import java.util.Date import javax.crypto.Cipher import javax.crypto.KeyAgreement import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec +import kotlin.math.absoluteValue +import kotlin.random.Random +import kotlin.time.Duration.Companion.days + actual open class DefaultCryptoService : CryptoService { - private val ecCurve: EcCurve = EcCurve.SECP_256_R_1 + private val ecCurve: EcCurve = SECP_256_R_1 private val keyPair: KeyPair private val jsonWebKey: JsonWebKey private val coseKey: CoseKey + final override val certificate: ByteArray actual constructor() { this.keyPair = KeyPairGenerator.getInstance("EC").also { it.initialize(ecCurve.keyLengthBits) }.genKeyPair() val ecPublicKey = keyPair.public as ECPublicKey - this.jsonWebKey = JsonWebKey.fromCoordinates( - type = JwkType.EC, - curve = EcCurve.SECP_256_R_1, - x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), - y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - )!! - this.coseKey = CoseKey.fromCoordinates( - type = CoseKeyType.EC2, - curve = CoseEllipticCurve.P256, - x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), - y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - )!! + val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! + this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! + this.certificate = generateSelfSignedCertificate() } constructor(keyPair: KeyPair) { this.keyPair = keyPair val ecPublicKey = keyPair.public as ECPublicKey - this.jsonWebKey = JsonWebKey.fromCoordinates( - type = JwkType.EC, - curve = EcCurve.SECP_256_R_1, - x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), - y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - )!! - this.coseKey = CoseKey.fromCoordinates( - type = CoseKeyType.EC2, - curve = CoseEllipticCurve.P256, - x = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), - y = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - )!! + val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! + this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! + this.certificate = generateSelfSignedCertificate() + } + + constructor(keyPair: KeyPair, certificate: Certificate) { + this.keyPair = keyPair + val ecPublicKey = keyPair.public as ECPublicKey + val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! + this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! + this.certificate = certificate.encoded + } + + private fun generateSelfSignedCertificate(): ByteArray { + val notBeforeDate = Date.from(Instant.now()) + val notAfterDate = Date.from(Instant.now().plusSeconds(30.days.inWholeSeconds)) + val serialNumber: BigInteger = BigInteger.valueOf(Random.nextLong().absoluteValue) + val issuer = X500Name("CN=DefaultCryptoService") + val builder = X509v3CertificateBuilder( + /* issuer = */ issuer, + /* serial = */ serialNumber, + /* notBefore = */ notBeforeDate, + /* notAfter = */ notAfterDate, + /* subject = */ issuer, + /* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(keyPair.public.encoded) + ) + val contentSigner: ContentSigner = JcaContentSignerBuilder(JwsAlgorithm.ES256.jcaName).build(keyPair.private) + val certificateHolder = builder.build(contentSigner) + return certificateHolder.encoded } override val jwsAlgorithm = JwsAlgorithm.ES256 @@ -219,12 +248,15 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { actual object CryptoUtils { - actual fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? = try { + actual fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? = kotlin.runCatching { val pubKey = CertificateFactory.getInstance("X.509").generateCertificate(it.inputStream()).publicKey - if (pubKey is ECPublicKey) JsonWebKey.fromJcaKey(pubKey, EcCurve.SECP_256_R_1) else null - } catch (e: Throwable) { - null - } + if (pubKey is ECPublicKey) JsonWebKey.fromJcaKey(pubKey, SECP_256_R_1) else null + }.getOrNull() + + actual fun extractCoseKeyFromX509Cert(it: ByteArray): CoseKey? = kotlin.runCatching { + val pubKey = CertificateFactory.getInstance("X.509").generateCertificate(it.inputStream()).publicKey + if (pubKey is ECPublicKey) CoseKey.fromJcaKey(pubKey, P256) else null + }.getOrNull() } @@ -263,29 +295,28 @@ val JweAlgorithm.jcaName val EcCurve.jcaName get() = when (this) { - EcCurve.SECP_256_R_1 -> "secp256r1" + SECP_256_R_1 -> "secp256r1" } val CoseEllipticCurve.jcaName get() = when (this) { - CoseEllipticCurve.P256 -> "P-256" + P256 -> "P-256" CoseEllipticCurve.P384 -> "P-384" CoseEllipticCurve.P521 -> "P-521" } fun JsonWebKey.getPublicKey(): PublicKey { - val parameterSpec = ECNamedCurveTable.getParameterSpec(curve?.jcaName ?: "P-256") - val x = BigInteger(1, x) - val y = BigInteger(1, y) - val ecPoint = parameterSpec.curve.createPoint(x, y) - val ecPublicKeySpec = ECPublicKeySpec(ecPoint, parameterSpec) - return JCEECPublicKey("EC", ecPublicKeySpec) + return toPublicKey(curve?.jcaName, x, y) } fun CoseKey.getPublicKey(): PublicKey { - val parameterSpec = ECNamedCurveTable.getParameterSpec(curve?.jcaName ?: "P-256") - val x = BigInteger(1, x) - val y = BigInteger(1, y) + return toPublicKey(curve?.jcaName, x, y) +} + +private fun toPublicKey(curveName: String?, xCoordinate: ByteArray?, yCoordinate: ByteArray?): JCEECPublicKey { + val parameterSpec = ECNamedCurveTable.getParameterSpec(curveName ?: "P-256") + val x = BigInteger(1, xCoordinate) + val y = BigInteger(1, yCoordinate) val ecPoint = parameterSpec.curve.createPoint(x, y) val ecPublicKeySpec = ECPublicKeySpec(ecPoint, parameterSpec) return JCEECPublicKey("EC", ecPublicKeySpec) @@ -293,7 +324,15 @@ fun CoseKey.getPublicKey(): PublicKey { fun JsonWebKey.Companion.fromJcaKey(publicKey: ECPublicKey, ecCurve: EcCurve) = fromCoordinates( - JwkType.EC, + EC, + ecCurve, + publicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), + publicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + ) + +fun CoseKey.Companion.fromJcaKey(publicKey: ECPublicKey, ecCurve: CoseEllipticCurve) = + fromCoordinates( + EC2, ecCurve, publicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), publicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) From 07777edcfadfe5bbb86fcc0ac8c1c829aeef28b0 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 4 Aug 2023 09:54:25 +0200 Subject: [PATCH 04/11] Add structure for an abstract public key --- .../at/asitplus/wallet/lib/CryptoPublicKey.kt | 125 ++++++++++++++++++ .../wallet/lib/agent/CryptoService.kt | 3 + .../wallet/lib/agent/DefaultCryptoService.kt | 7 + .../wallet/lib/agent/DefaultCryptoService.kt | 11 ++ 4 files changed, 146 insertions(+) create mode 100644 vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt new file mode 100644 index 000000000..ba5645363 --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt @@ -0,0 +1,125 @@ +package at.asitplus.wallet.lib + +import at.asitplus.KmmResult +import at.asitplus.wallet.lib.cbor.CoseAlgorithm +import at.asitplus.wallet.lib.cbor.CoseEllipticCurve +import at.asitplus.wallet.lib.cbor.CoseKey +import at.asitplus.wallet.lib.cbor.CoseKeyType +import at.asitplus.wallet.lib.jws.EcCurve +import at.asitplus.wallet.lib.jws.JsonWebKey +import at.asitplus.wallet.lib.jws.JwkType +import at.asitplus.wallet.lib.jws.MultibaseHelper + + +sealed class CryptoPublicKey { + + abstract fun toCoseKey(): CoseKey + abstract fun toJsonWebKey(): JsonWebKey + + data class Ec( + val curve: EcCurve, + val keyId: String, + val x: ByteArray, + val y: ByteArray, + ) : CryptoPublicKey() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Ec + + if (curve != other.curve) return false + if (keyId != other.keyId) return false + if (!x.contentEquals(other.x)) return false + if (!y.contentEquals(other.y)) return false + + return true + } + + override fun hashCode(): Int { + var result = curve.hashCode() + result = 31 * result + keyId.hashCode() + result = 31 * result + x.contentHashCode() + result = 31 * result + y.contentHashCode() + return result + } + + companion object { + + fun fromKeyId(it: String): CryptoPublicKey? { + val (xCoordinate, yCoordinate) = MultibaseHelper.calcPublicKey(it) + ?: return null + return CryptoPublicKey.Ec( + curve = EcCurve.SECP_256_R_1, + keyId = it, + x = xCoordinate, + y = yCoordinate + ) + } + + fun fromAnsiX963Bytes(type: JwkType, curve: EcCurve, it: ByteArray): CryptoPublicKey? { + if (type != JwkType.EC || curve != EcCurve.SECP_256_R_1) { + return null + } + if (it.size != 1 + 32 + 32 || it[0] != 0x04.toByte()) { + return null + } + val xCoordinate = it.sliceArray(1 until 33) + val yCoordinate = it.sliceArray(33 until 65) + val keyId = MultibaseHelper.calcKeyId(curve, xCoordinate, yCoordinate) + ?: return null + return CryptoPublicKey.Ec( + curve = curve, + keyId = keyId, + x = xCoordinate, + y = yCoordinate + ) + } + + fun fromCoordinates( + type: JwkType, + curve: EcCurve, + x: ByteArray, + y: ByteArray + ): CryptoPublicKey? { + if (type != JwkType.EC || curve != EcCurve.SECP_256_R_1) { + return null + } + val keyId = MultibaseHelper.calcKeyId(curve, x, y) + ?: return null + return CryptoPublicKey.Ec( + curve = curve, + keyId = keyId, + x = x, + y = y + ) + } + } + + fun toAnsiX963ByteArray(): KmmResult { + return KmmResult.success(byteArrayOf(0x04.toByte()) + x + y); + } + + override fun toCoseKey() = CoseKey( + type = CoseKeyType.EC2, + curve = curve.toCoseCurve(), + keyId = keyId.encodeToByteArray(), + algorithm = CoseAlgorithm.ES256, + x = x, + y = y + ) + + override fun toJsonWebKey() = JsonWebKey( + curve = curve, + type = JwkType.EC, + keyId = keyId, + x = x, + y = y + ) + } + +} + +private fun EcCurve.toCoseCurve(): CoseEllipticCurve = when (this) { + EcCurve.SECP_256_R_1 -> CoseEllipticCurve.P256 +} diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index 622ddc2c9..b03abaeac 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.jws.EcCurve @@ -56,6 +57,8 @@ interface CryptoService { fun toJsonWebKey(): JsonWebKey + fun toPublicKey(): CryptoPublicKey + fun toCoseKey(): CoseKey } diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index f505e5964..135792c43 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.cbor.CoseEllipticCurve import at.asitplus.wallet.lib.cbor.CoseKey @@ -13,6 +14,7 @@ import at.asitplus.wallet.lib.jws.JweEncryption import at.asitplus.wallet.lib.jws.JwkType import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsExtensions.convertToAsn1Signature +import at.asitplus.wallet.lib.jws.MultibaseHelper import io.ktor.http.content.ByteArrayContent import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer @@ -62,6 +64,7 @@ actual class DefaultCryptoService : CryptoService { private val publicKey: SecKeyRef private val jsonWebKey: JsonWebKey private val coseKey: CoseKey + private val cryptoPublicKey: CryptoPublicKey final override val certificate: ByteArray actual constructor() { @@ -75,6 +78,8 @@ actual class DefaultCryptoService : CryptoService { val data = CFBridgingRelease(publicKeyData) as NSData this.jsonWebKey = JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray())!! this.coseKey = CoseKey.fromAnsiX963Bytes(CoseKeyType.EC2, CoseEllipticCurve.P256, data.toByteArray())!! + val keyId = MultibaseHelper.calcKeyId(EcCurve.SECP_256_R_1, keyX, keyY)!! + this.cryptoPublicKey = CryptoPublicKey.Ec(curve = EcCurve.SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = byteArrayOf() // TODO How to create a self-signed certificate in Kotlin/iOS? } @@ -148,6 +153,8 @@ actual class DefaultCryptoService : CryptoService { override fun toJsonWebKey() = jsonWebKey override fun toCoseKey() = coseKey + + override fun toPublicKey() = cryptoPublicKey } @Suppress("UNCHECKED_CAST") diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index ffe3af3f8..596a2f713 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.cbor.CoseEllipticCurve import at.asitplus.wallet.lib.cbor.CoseEllipticCurve.P256 @@ -15,6 +16,7 @@ import at.asitplus.wallet.lib.jws.JwkType.EC import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsExtensions.convertToAsn1Signature import at.asitplus.wallet.lib.jws.JwsExtensions.ensureSize +import at.asitplus.wallet.lib.jws.MultibaseHelper import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.cert.X509v3CertificateBuilder @@ -51,6 +53,7 @@ actual open class DefaultCryptoService : CryptoService { private val ecCurve: EcCurve = SECP_256_R_1 private val keyPair: KeyPair + private val cryptoPublicKey: CryptoPublicKey private val jsonWebKey: JsonWebKey private val coseKey: CoseKey final override val certificate: ByteArray @@ -62,6 +65,8 @@ actual open class DefaultCryptoService : CryptoService { val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! + val keyId = MultibaseHelper.calcKeyId(SECP_256_R_1, keyX, keyY)!! + this.cryptoPublicKey = CryptoPublicKey.Ec(curve = SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = generateSelfSignedCertificate() } @@ -72,6 +77,8 @@ actual open class DefaultCryptoService : CryptoService { val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! + val keyId = MultibaseHelper.calcKeyId(SECP_256_R_1, keyX, keyY)!! + this.cryptoPublicKey = CryptoPublicKey.Ec(curve = SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = generateSelfSignedCertificate() } @@ -82,6 +89,8 @@ actual open class DefaultCryptoService : CryptoService { val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! + val keyId = MultibaseHelper.calcKeyId(SECP_256_R_1, keyX, keyY)!! + this.cryptoPublicKey = CryptoPublicKey.Ec(curve = SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = certificate.encoded } @@ -111,6 +120,8 @@ actual open class DefaultCryptoService : CryptoService { override fun toCoseKey() = coseKey + override fun toPublicKey() = cryptoPublicKey + override suspend fun sign(input: ByteArray): KmmResult = try { val signed = Signature.getInstance(jwsAlgorithm.jcaName).apply { From 6098e46d0924cfdab5cbb0c6fe0efcc6fb83dd8f Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 4 Aug 2023 09:58:31 +0200 Subject: [PATCH 05/11] Remove unnecessary interface methods --- .../lib/aries/IssueCredentialProtocolTest.kt | 16 ++++++++-------- .../wallet/lib/aries/PresentProofProtocolTest.kt | 16 ++++++++-------- .../asitplus/wallet/lib/oidc/OidcSiopVerifier.kt | 2 +- .../asitplus/wallet/lib/oidc/OidcSiopWallet.kt | 2 +- .../wallet/lib/oidc/OidcSiopIsoProtocolTest.kt | 2 +- .../at/asitplus/wallet/lib/CryptoPublicKey.kt | 13 ++++--------- .../asitplus/wallet/lib/agent/CryptoService.kt | 6 +----- .../at/asitplus/wallet/lib/jws/JwsService.kt | 12 ++++++------ .../asitplus/wallet/lib/cbor/CoseServiceTest.kt | 4 ++-- .../at/asitplus/wallet/lib/iso/IsoMdocTest.kt | 4 ++-- .../at/asitplus/wallet/lib/jws/JwsServiceTest.kt | 4 ++-- .../wallet/lib/agent/DefaultCryptoService.kt | 13 +------------ .../wallet/lib/agent/DefaultCryptoService.kt | 12 ------------ .../asitplus/wallet/lib/jws/JwsServiceJvmTest.kt | 6 +++--- 14 files changed, 40 insertions(+), 72 deletions(-) diff --git a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt index 720a12a84..76a44084d 100644 --- a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt +++ b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt @@ -50,15 +50,15 @@ class IssueCredentialProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = holderProtocol.parseMessage(invitationMessage, issuerCryptoService.toJsonWebKey()) + val parsedInvitation = holderProtocol.parseMessage(invitationMessage, issuerCryptoService.toPublicKey().toJsonWebKey()) parsedInvitation.shouldBeInstanceOf() val requestCredential = parsedInvitation.message - val parsedRequestCredential = issuerProtocol.parseMessage(requestCredential, holderCryptoService.toJsonWebKey()) + val parsedRequestCredential = issuerProtocol.parseMessage(requestCredential, holderCryptoService.toPublicKey().toJsonWebKey()) parsedRequestCredential.shouldBeInstanceOf() val issueCredential = parsedRequestCredential.message - val parsedIssueCredential = holderProtocol.parseMessage(issueCredential, issuerCryptoService.toJsonWebKey()) + val parsedIssueCredential = holderProtocol.parseMessage(issueCredential, issuerCryptoService.toPublicKey().toJsonWebKey()) parsedIssueCredential.shouldBeInstanceOf() val issuedCredential = parsedIssueCredential.lastMessage @@ -70,11 +70,11 @@ class IssueCredentialProtocolTest : FreeSpec({ requestCredential.shouldBeInstanceOf() val parsedRequestCredential = - issuerProtocol.parseMessage(requestCredential.message, holderCryptoService.toJsonWebKey()) + issuerProtocol.parseMessage(requestCredential.message, holderCryptoService.toPublicKey().toJsonWebKey()) parsedRequestCredential.shouldBeInstanceOf() val issueCredential = parsedRequestCredential.message - val parsedIssueCredential = holderProtocol.parseMessage(issueCredential, issuerCryptoService.toJsonWebKey()) + val parsedIssueCredential = holderProtocol.parseMessage(issueCredential, issuerCryptoService.toPublicKey().toJsonWebKey()) parsedIssueCredential.shouldBeInstanceOf() val issuedCredential = parsedIssueCredential.lastMessage @@ -88,7 +88,7 @@ class IssueCredentialProtocolTest : FreeSpec({ threadId = uuid4().toString(), attachment = JwmAttachment(id = uuid4().toString(), "mimeType", JwmAttachmentData()) ), - issuerCryptoService.toJsonWebKey() + issuerCryptoService.toPublicKey().toJsonWebKey() ) parsed.shouldBeInstanceOf() } @@ -98,7 +98,7 @@ class IssueCredentialProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = holderProtocol.parseMessage(invitationMessage, issuerCryptoService.toJsonWebKey()) + val parsedInvitation = holderProtocol.parseMessage(invitationMessage, issuerCryptoService.toPublicKey().toJsonWebKey()) parsedInvitation.shouldBeInstanceOf() val requestCredential = parsedInvitation.message @@ -116,7 +116,7 @@ class IssueCredentialProtocolTest : FreeSpec({ ) ) val parsedRequestCredential = - issuerProtocol.parseMessage(wrongRequestCredential, holderCryptoService.toJsonWebKey()) + issuerProtocol.parseMessage(wrongRequestCredential, holderCryptoService.toPublicKey().toJsonWebKey()) parsedRequestCredential.shouldBeInstanceOf() val problemReport = parsedRequestCredential.message diff --git a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt index 46c6d45ff..c3207e2bf 100644 --- a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt +++ b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt @@ -58,16 +58,16 @@ class PresentProofProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = verifierProtocol.parseMessage(invitationMessage, holderCryptoService.toJsonWebKey()) + val parsedInvitation = verifierProtocol.parseMessage(invitationMessage, holderCryptoService.toPublicKey().toJsonWebKey()) parsedInvitation.shouldBeInstanceOf() val requestPresentation = parsedInvitation.message val parsedRequestPresentation = - holderProtocol.parseMessage(requestPresentation, verifierCryptoService.toJsonWebKey()) + holderProtocol.parseMessage(requestPresentation, verifierCryptoService.toPublicKey().toJsonWebKey()) parsedRequestPresentation.shouldBeInstanceOf() val presentation = parsedRequestPresentation.message - val parsedPresentation = verifierProtocol.parseMessage(presentation, holderCryptoService.toJsonWebKey()) + val parsedPresentation = verifierProtocol.parseMessage(presentation, holderCryptoService.toPublicKey().toJsonWebKey()) parsedPresentation.shouldBeInstanceOf() val receivedPresentation = parsedPresentation.lastMessage @@ -89,11 +89,11 @@ class PresentProofProtocolTest : FreeSpec({ requestPresentation.shouldBeInstanceOf() val parsedRequestPresentation = - holderProtocol.parseMessage(requestPresentation.message, verifierCryptoService.toJsonWebKey()) + holderProtocol.parseMessage(requestPresentation.message, verifierCryptoService.toPublicKey().toJsonWebKey()) parsedRequestPresentation.shouldBeInstanceOf() val presentation = parsedRequestPresentation.message - val parsedPresentation = verifierProtocol.parseMessage(presentation, holderCryptoService.toJsonWebKey()) + val parsedPresentation = verifierProtocol.parseMessage(presentation, holderCryptoService.toPublicKey().toJsonWebKey()) parsedPresentation.shouldBeInstanceOf() val receivedPresentation = parsedPresentation.lastMessage @@ -107,7 +107,7 @@ class PresentProofProtocolTest : FreeSpec({ parentThreadId = uuid4().toString(), attachment = JwmAttachment(id = uuid4().toString(), "mimeType", JwmAttachmentData()) ), - holderCryptoService.toJsonWebKey() + holderCryptoService.toPublicKey().toJsonWebKey() ) parsed.shouldBeInstanceOf() } @@ -117,12 +117,12 @@ class PresentProofProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = verifierProtocol.parseMessage(invitationMessage, holderCryptoService.toJsonWebKey()) + val parsedInvitation = verifierProtocol.parseMessage(invitationMessage, holderCryptoService.toPublicKey().toJsonWebKey()) parsedInvitation.shouldBeInstanceOf() val requestPresentation = parsedInvitation.message val parsedRequestPresentation = - holderProtocol.parseMessage(requestPresentation, verifierCryptoService.toJsonWebKey()) + holderProtocol.parseMessage(requestPresentation, verifierCryptoService.toPublicKey().toJsonWebKey()) parsedRequestPresentation.shouldBeInstanceOf() val problemReport = parsedRequestPresentation.message diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index 8a91d24a8..03c45112e 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -80,7 +80,7 @@ class OidcSiopVerifier( ) = OidcSiopVerifier( verifier = verifier, relyingPartyUrl = relyingPartyUrl, - agentPublicKey = cryptoService.toJsonWebKey(), + agentPublicKey = cryptoService.toPublicKey().toJsonWebKey(), jwsService = jwsService, verifierJwsService = verifierJwsService, timeLeewaySeconds = timeLeewaySeconds, diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt index 728c51a3e..98e6fca02 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt @@ -65,7 +65,7 @@ class OidcSiopWallet( clientId: String = "https://wallet.a-sit.at/" ) = OidcSiopWallet( holder = holder, - agentPublicKey = cryptoService.toJsonWebKey(), + agentPublicKey = cryptoService.toPublicKey().toJsonWebKey(), jwsService = jwsService, verifierJwsService = verifierJwsService, clock = clock, diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt index 406cde943..1fccecde1 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt @@ -47,7 +47,7 @@ class OidcSiopIsoProtocolTest : FreeSpec({ dataProvider = DummyCredentialDataProvider(), ).issueCredentialWithTypes( holderAgent.identifier, - subjectPublicKey = holderCryptoService.toCoseKey(), + subjectPublicKey = holderCryptoService.toPublicKey().toCoseKey(), attributeTypes = listOf(ConstantIndex.MobileDrivingLicence2023.vcType) ).toStoreCredentialInput() ) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt index ba5645363..0616861e2 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt @@ -57,8 +57,8 @@ sealed class CryptoPublicKey { ) } - fun fromAnsiX963Bytes(type: JwkType, curve: EcCurve, it: ByteArray): CryptoPublicKey? { - if (type != JwkType.EC || curve != EcCurve.SECP_256_R_1) { + fun fromAnsiX963Bytes(curve: EcCurve, it: ByteArray): CryptoPublicKey? { + if (curve != EcCurve.SECP_256_R_1) { return null } if (it.size != 1 + 32 + 32 || it[0] != 0x04.toByte()) { @@ -76,13 +76,8 @@ sealed class CryptoPublicKey { ) } - fun fromCoordinates( - type: JwkType, - curve: EcCurve, - x: ByteArray, - y: ByteArray - ): CryptoPublicKey? { - if (type != JwkType.EC || curve != EcCurve.SECP_256_R_1) { + fun fromCoordinates(curve: EcCurve, x: ByteArray, y: ByteArray): CryptoPublicKey? { + if (curve != EcCurve.SECP_256_R_1) { return null } val keyId = MultibaseHelper.calcKeyId(curve, x, y) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index b03abaeac..d0f496e7f 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -44,7 +44,7 @@ interface CryptoService { fun messageDigest(input: ByteArray, digest: Digest): KmmResult val identifier: String - get() = toJsonWebKey().identifier + get() = toPublicKey().toJsonWebKey().identifier val jwsAlgorithm: JwsAlgorithm @@ -55,12 +55,8 @@ interface CryptoService { */ val certificate: ByteArray - fun toJsonWebKey(): JsonWebKey - fun toPublicKey(): CryptoPublicKey - fun toCoseKey(): CoseKey - } interface VerifierCryptoService { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt index 46dec733c..61aee2644 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt @@ -67,7 +67,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { ): String? { val jwsHeader = JwsHeader( algorithm = cryptoService.jwsAlgorithm, - keyId = cryptoService.toJsonWebKey().keyId, + keyId = cryptoService.toPublicKey().toJsonWebKey().keyId, type = type, contentType = contentType ) @@ -76,8 +76,8 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { override suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): String? { if (header.algorithm != cryptoService.jwsAlgorithm - || header.keyId?.let { it != cryptoService.toJsonWebKey().keyId } == true - || header.jsonWebKey?.let { it != cryptoService.toJsonWebKey() } == true + || header.keyId?.let { it != cryptoService.toPublicKey().toJsonWebKey().keyId } == true + || header.jsonWebKey?.let { it != cryptoService.toPublicKey().toJsonWebKey() } == true ) { return null.also { Napier.w("Algorithm or keyId not matching to cryptoService") } } @@ -100,9 +100,9 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { ): String? { var copy = header.copy(algorithm = cryptoService.jwsAlgorithm) if (addKeyId) - copy = copy.copy(keyId = cryptoService.toJsonWebKey().keyId) + copy = copy.copy(keyId = cryptoService.toPublicKey().toJsonWebKey().keyId) if (addJsonWebKey) - copy = copy.copy(jsonWebKey = cryptoService.toJsonWebKey()) + copy = copy.copy(jsonWebKey = cryptoService.toPublicKey().toJsonWebKey()) return createSignedJws(copy, payload) } @@ -159,7 +159,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { val jweHeader = JweHeader( algorithm = jweAlgorithm, encryption = jweEncryption, - jsonWebKey = cryptoService.toJsonWebKey(), + jsonWebKey = cryptoService.toPublicKey().toJsonWebKey(), type = type, contentType = contentType, ephemeralKeyPair = ephemeralKeyPair.toPublicJsonWebKey() diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt index 02e186f4d..3a27d822c 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseServiceTest.kt @@ -38,7 +38,7 @@ class CoseServiceTest : FreeSpec({ val parsed = CoseSigned.deserialize(signed.serialize()) parsed.shouldNotBeNull() - val result = verifierCoseService.verifyCose(parsed, cryptoService.toCoseKey()).getOrThrow() + val result = verifierCoseService.verifyCose(parsed, cryptoService.toPublicKey().toCoseKey()).getOrThrow() result shouldBe true } @@ -58,7 +58,7 @@ class CoseServiceTest : FreeSpec({ val parsed = CoseSigned.deserialize(signed.serialize()) parsed.shouldNotBeNull() - val result = verifierCoseService.verifyCose(parsed, cryptoService.toCoseKey()).getOrThrow() + val result = verifierCoseService.verifyCose(parsed, cryptoService.toPublicKey().toCoseKey()).getOrThrow() result shouldBe true } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/IsoMdocTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/IsoMdocTest.kt index 2f6382a8a..24c8c2c88 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/IsoMdocTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/IsoMdocTest.kt @@ -45,7 +45,7 @@ class IsoMdocTest : FreeSpec({ val verifierRequest = verifier.buildDeviceRequest() val walletResponse = wallet.buildDeviceResponse(verifierRequest) - verifier.verifyResponse(walletResponse, issuer.cryptoService.toCoseKey()) + verifier.verifyResponse(walletResponse, issuer.cryptoService.toPublicKey().toCoseKey()) } }) @@ -56,7 +56,7 @@ class Wallet { val coseService = DefaultCoseService(cryptoService) val deviceKeyInfo = DeviceKeyInfo( - deviceKey = cryptoService.toCoseKey(), + deviceKey = cryptoService.toPublicKey().toCoseKey(), ) var storedMdl: MobileDrivingLicence? = null diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt index f21dc2a70..c3a749c45 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt @@ -64,7 +64,7 @@ class JwsServiceTest : FreeSpec({ "signed object with jsonWebKey can be verified" { val payload = randomPayload.encodeToByteArray() - val header = JwsHeader(JwsAlgorithm.ES256, jsonWebKey = cryptoService.toJsonWebKey()) + val header = JwsHeader(JwsAlgorithm.ES256, jsonWebKey = cryptoService.toPublicKey().toJsonWebKey()) val signed = jwsService.createSignedJws(header, payload) signed.shouldNotBeNull() @@ -81,7 +81,7 @@ class JwsServiceTest : FreeSpec({ val encrypted = jwsService.encryptJweObject( JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, stringPayload.encodeToByteArray(), - cryptoService.toJsonWebKey(), + cryptoService.toPublicKey().toJsonWebKey(), JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, JweAlgorithm.ECDH_ES, JweEncryption.A256GCM, diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 135792c43..8fd741ca3 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -14,8 +14,6 @@ import at.asitplus.wallet.lib.jws.JweEncryption import at.asitplus.wallet.lib.jws.JwkType import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsExtensions.convertToAsn1Signature -import at.asitplus.wallet.lib.jws.MultibaseHelper -import io.ktor.http.content.ByteArrayContent import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.MemScope @@ -62,8 +60,6 @@ actual class DefaultCryptoService : CryptoService { override val coseAlgorithm = CoseAlgorithm.ES256 private val privateKey: SecKeyRef private val publicKey: SecKeyRef - private val jsonWebKey: JsonWebKey - private val coseKey: CoseKey private val cryptoPublicKey: CryptoPublicKey final override val certificate: ByteArray @@ -76,10 +72,7 @@ actual class DefaultCryptoService : CryptoService { publicKey = SecKeyCopyPublicKey(privateKey)!! val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) val data = CFBridgingRelease(publicKeyData) as NSData - this.jsonWebKey = JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray())!! - this.coseKey = CoseKey.fromAnsiX963Bytes(CoseKeyType.EC2, CoseEllipticCurve.P256, data.toByteArray())!! - val keyId = MultibaseHelper.calcKeyId(EcCurve.SECP_256_R_1, keyX, keyY)!! - this.cryptoPublicKey = CryptoPublicKey.Ec(curve = EcCurve.SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) + this.cryptoPublicKey = CryptoPublicKey.Ec.fromAnsiX963Bytes(EcCurve.SECP_256_R_1, data.toByteArray())!! this.certificate = byteArrayOf() // TODO How to create a self-signed certificate in Kotlin/iOS? } @@ -150,10 +143,6 @@ actual class DefaultCryptoService : CryptoService { return KmmResult.success(input) } - override fun toJsonWebKey() = jsonWebKey - - override fun toCoseKey() = coseKey - override fun toPublicKey() = cryptoPublicKey } diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 596a2f713..31cadba97 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -54,8 +54,6 @@ actual open class DefaultCryptoService : CryptoService { private val ecCurve: EcCurve = SECP_256_R_1 private val keyPair: KeyPair private val cryptoPublicKey: CryptoPublicKey - private val jsonWebKey: JsonWebKey - private val coseKey: CoseKey final override val certificate: ByteArray actual constructor() { @@ -63,8 +61,6 @@ actual open class DefaultCryptoService : CryptoService { val ecPublicKey = keyPair.public as ECPublicKey val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! - this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! val keyId = MultibaseHelper.calcKeyId(SECP_256_R_1, keyX, keyY)!! this.cryptoPublicKey = CryptoPublicKey.Ec(curve = SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = generateSelfSignedCertificate() @@ -75,8 +71,6 @@ actual open class DefaultCryptoService : CryptoService { val ecPublicKey = keyPair.public as ECPublicKey val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! - this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! val keyId = MultibaseHelper.calcKeyId(SECP_256_R_1, keyX, keyY)!! this.cryptoPublicKey = CryptoPublicKey.Ec(curve = SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = generateSelfSignedCertificate() @@ -87,8 +81,6 @@ actual open class DefaultCryptoService : CryptoService { val ecPublicKey = keyPair.public as ECPublicKey val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) - this.jsonWebKey = JsonWebKey.fromCoordinates(type = EC, curve = SECP_256_R_1, x = keyX, y = keyY)!! - this.coseKey = CoseKey.fromCoordinates(type = EC2, curve = P256, x = keyX, y = keyY)!! val keyId = MultibaseHelper.calcKeyId(SECP_256_R_1, keyX, keyY)!! this.cryptoPublicKey = CryptoPublicKey.Ec(curve = SECP_256_R_1, keyId = keyId, x = keyX, y = keyY) this.certificate = certificate.encoded @@ -116,10 +108,6 @@ actual open class DefaultCryptoService : CryptoService { override val coseAlgorithm = CoseAlgorithm.ES256 - override fun toJsonWebKey() = jsonWebKey - - override fun toCoseKey() = coseKey - override fun toPublicKey() = cryptoPublicKey override suspend fun sign(input: ByteArray): KmmResult = diff --git a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt index 4e92d1b5c..0ad273dcb 100644 --- a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt +++ b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt @@ -49,7 +49,7 @@ class JwsServiceJvmTest : FreeSpec({ val stringPayload = jsonSerializer.encodeToString(randomPayload) val libHeader = JWSHeader.Builder(JWSAlgorithm.ES256) .type(JOSEObjectType("JWT")) - .keyID(cryptoService.toJsonWebKey().keyId!!) + .keyID(cryptoService.toPublicKey().toJsonWebKey().keyId!!) .build() val libObject = JWSObject(libHeader, Payload(stringPayload)).also { it.sign(ECDSASigner(keyPair.private as ECPrivateKey)) @@ -81,7 +81,7 @@ class JwsServiceJvmTest : FreeSpec({ val libHeader = JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM) .type(JOSEObjectType(JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON)) .contentType(JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) - .keyID(cryptoService.toJsonWebKey().keyId!!) + .keyID(cryptoService.toPublicKey().toJsonWebKey().keyId!!) .build() val libObject = JWEObject(libHeader, Payload(stringPayload)).also { it.encrypt(ECDHEncrypter(keyPair.public as ECPublicKey)) @@ -100,7 +100,7 @@ class JwsServiceJvmTest : FreeSpec({ val encrypted = jwsService.encryptJweObject( JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, stringPayload.encodeToByteArray(), - cryptoService.toJsonWebKey(), + cryptoService.toPublicKey().toJsonWebKey(), JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, JweAlgorithm.ECDH_ES, JweEncryption.A256GCM, From 55ba7f8e7c212a448fc24e2ae51b3db99fb4713a Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 4 Aug 2023 10:05:47 +0200 Subject: [PATCH 06/11] Replace usages of specific JsonWebKey with generic CryptoPublicKey --- .../asitplus/wallet/lib/oidc/OidcSiopVerifier.kt | 8 ++++---- .../at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt | 12 ++++++------ .../at/asitplus/wallet/lib/agent/CryptoService.kt | 3 +-- .../at/asitplus/wallet/lib/agent/HolderAgent.kt | 2 +- .../at/asitplus/wallet/lib/agent/Validator.kt | 2 +- .../at/asitplus/wallet/lib/jws/JwsHeader.kt | 2 +- .../wallet/lib/agent/DefaultCryptoService.kt | 15 ++------------- .../wallet/lib/agent/DefaultCryptoService.kt | 12 +++--------- 8 files changed, 19 insertions(+), 37 deletions(-) diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index 03c45112e..680b6ba1c 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.oidc +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.DefaultVerifierCryptoService import at.asitplus.wallet.lib.agent.Verifier @@ -18,7 +19,6 @@ import at.asitplus.wallet.lib.data.dif.SchemaReference import at.asitplus.wallet.lib.iso.IsoDataModelConstants.NAMESPACE_MDL import at.asitplus.wallet.lib.jws.DefaultJwsService import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsHeader import at.asitplus.wallet.lib.jws.JwsService @@ -54,7 +54,7 @@ import kotlin.time.toDuration class OidcSiopVerifier( private val verifier: Verifier, private val relyingPartyUrl: String, - private val agentPublicKey: JsonWebKey, + private val agentPublicKey: CryptoPublicKey, private val jwsService: JwsService, private val verifierJwsService: VerifierJwsService, timeLeewaySeconds: Long = 300L, @@ -80,7 +80,7 @@ class OidcSiopVerifier( ) = OidcSiopVerifier( verifier = verifier, relyingPartyUrl = relyingPartyUrl, - agentPublicKey = cryptoService.toPublicKey().toJsonWebKey(), + agentPublicKey = cryptoService.toPublicKey(), jwsService = jwsService, verifierJwsService = verifierJwsService, timeLeewaySeconds = timeLeewaySeconds, @@ -175,7 +175,7 @@ class OidcSiopVerifier( ) val metadata = RelyingPartyMetadata( redirectUris = arrayOf(relyingPartyUrl), - jsonWebKeySet = JsonWebKeySet(arrayOf(agentPublicKey)), + jsonWebKeySet = JsonWebKeySet(arrayOf(agentPublicKey.toJsonWebKey())), subjectSyntaxTypesSupported = arrayOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY), vpFormats = vpFormats, ) diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt index 98e6fca02..b4f122f05 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.KmmResult +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.Holder import at.asitplus.wallet.lib.data.dif.ClaimFormatEnum @@ -8,7 +9,6 @@ import at.asitplus.wallet.lib.data.dif.PresentationSubmission import at.asitplus.wallet.lib.data.dif.PresentationSubmissionDescriptor import at.asitplus.wallet.lib.jws.DefaultJwsService import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JwsAlgorithm import at.asitplus.wallet.lib.jws.JwsHeader import at.asitplus.wallet.lib.jws.JwsService @@ -48,7 +48,7 @@ import kotlin.time.Duration.Companion.seconds */ class OidcSiopWallet( private val holder: Holder, - private val agentPublicKey: JsonWebKey, + private val agentPublicKey: CryptoPublicKey, private val jwsService: JwsService, private val verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(), private val clock: Clock = Clock.System, @@ -65,7 +65,7 @@ class OidcSiopWallet( clientId: String = "https://wallet.a-sit.at/" ) = OidcSiopWallet( holder = holder, - agentPublicKey = cryptoService.toPublicKey().toJsonWebKey(), + agentPublicKey = cryptoService.toPublicKey(), jwsService = jwsService, verifierJwsService = verifierJwsService, clock = clock, @@ -218,9 +218,9 @@ class OidcSiopWallet( val now = clock.now() // we'll assume jwk-thumbprint val idToken = IdToken( - issuer = agentPublicKey.jwkThumbprint, - subject = agentPublicKey.jwkThumbprint, - subjectJwk = agentPublicKey, + issuer = agentPublicKey.toJsonWebKey().jwkThumbprint, + subject = agentPublicKey.toJsonWebKey().jwkThumbprint, + subjectJwk = agentPublicKey.toJsonWebKey(), audience = params.redirectUrl, issuedAt = now, expiration = now + 60.seconds, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index d0f496e7f..66289df8f 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -78,8 +78,7 @@ interface VerifierCryptoService { } expect object CryptoUtils { - fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? - fun extractCoseKeyFromX509Cert(it: ByteArray): CoseKey? + fun extractPublicKeyFromX509Cert(it: ByteArray): CryptoPublicKey? } data class AuthenticatedCiphertext(val ciphertext: ByteArray, val authtag: ByteArray) { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt index 2f6455f9e..88b1996f9 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt @@ -99,7 +99,7 @@ class HolderAgent( } credentialList.filterIsInstance().forEach { cred -> val issuerKey = cred.issuerSigned.issuerAuth.unprotectedHeader?.certificateChain?.let { - CryptoUtils.extractCoseKeyFromX509Cert(it) + CryptoUtils.extractPublicKeyFromX509Cert(it)?.toCoseKey() } when (val result = validator.verifyIsoCred(cred.issuerSigned, issuerKey)) { is Verifier.VerifyCredentialResult.InvalidStructure -> rejected += result.input diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt index 723b2aadf..b21d183ef 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt @@ -196,7 +196,7 @@ class Validator( val issuerAuth = issuerSigned.issuerAuth val issuerKey = issuerAuth.unprotectedHeader?.certificateChain?.let { - CryptoUtils.extractCoseKeyFromX509Cert(it) + CryptoUtils.extractPublicKeyFromX509Cert(it)?.toCoseKey() } ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeBase16()) .also { Napier.w("Got no issuer key in $issuerAuth") } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt index d14cfbf73..a4f83733f 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt @@ -79,7 +79,7 @@ data class JwsHeader( val publicKey: JsonWebKey? by lazy { jsonWebKey ?: keyId?.let { JsonWebKey.fromKeyId(it) } - ?: certificateChain?.firstOrNull()?.let { CryptoUtils.extractPublicKeyFromX509Cert(it) } + ?: certificateChain?.firstOrNull()?.let { CryptoUtils.extractPublicKeyFromX509Cert(it)?.toJsonWebKey() } } companion object { diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 8fd741ca3..ffcb79041 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -216,25 +216,14 @@ actual class DefaultVerifierCryptoService : VerifierCryptoService { @Suppress("UNCHECKED_CAST") actual object CryptoUtils { - actual fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? { + actual fun extractPublicKeyFromX509Cert(it: ByteArray): CryptoPublicKey? { memScoped { val certData = CFBridgingRetain(toData(it)) as CFDataRef val certificate = SecCertificateCreateWithData(null, certData) val publicKey = SecCertificateCopyKey(certificate) val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) val data = CFBridgingRelease(publicKeyData) as NSData - return JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray()) - } - } - - actual fun extractCoseKeyFromX509Cert(it: ByteArray): CoseKey? { - memScoped { - val certData = CFBridgingRetain(toData(it)) as CFDataRef - val certificate = SecCertificateCreateWithData(null, certData) - val publicKey = SecCertificateCopyKey(certificate) - val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) - val data = CFBridgingRelease(publicKeyData) as NSData - return CoseKey.fromAnsiX963Bytes(CoseKeyType.EC2, CoseEllipticCurve.P256, data.toByteArray()) + return CryptoPublicKey.Ec.fromAnsiX963Bytes(EcCurve.SECP_256_R_1, data.toByteArray()) } } diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 31cadba97..67981f680 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -247,14 +247,9 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { actual object CryptoUtils { - actual fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? = kotlin.runCatching { + actual fun extractPublicKeyFromX509Cert(it: ByteArray): CryptoPublicKey? = kotlin.runCatching { val pubKey = CertificateFactory.getInstance("X.509").generateCertificate(it.inputStream()).publicKey - if (pubKey is ECPublicKey) JsonWebKey.fromJcaKey(pubKey, SECP_256_R_1) else null - }.getOrNull() - - actual fun extractCoseKeyFromX509Cert(it: ByteArray): CoseKey? = kotlin.runCatching { - val pubKey = CertificateFactory.getInstance("X.509").generateCertificate(it.inputStream()).publicKey - if (pubKey is ECPublicKey) CoseKey.fromJcaKey(pubKey, P256) else null + if (pubKey is ECPublicKey) CryptoPublicKey.Ec.Companion.fromJcaKey(pubKey, SECP_256_R_1) else null }.getOrNull() } @@ -329,9 +324,8 @@ fun JsonWebKey.Companion.fromJcaKey(publicKey: ECPublicKey, ecCurve: EcCurve) = publicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) ) -fun CoseKey.Companion.fromJcaKey(publicKey: ECPublicKey, ecCurve: CoseEllipticCurve) = +fun CryptoPublicKey.Ec.Companion.fromJcaKey(publicKey: ECPublicKey, ecCurve: EcCurve) = fromCoordinates( - EC2, ecCurve, publicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), publicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) From 801b04e032bcdfc57acf24865ebb8b4a88499491 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 4 Aug 2023 10:21:36 +0200 Subject: [PATCH 07/11] Simplify API for verification of cryptographic signatures --- .../lib/oidc/DummyCredentialDataProvider.kt | 1 - .../wallet/lib/oidvci/OidvciProcessTest.kt | 21 --------- .../at/asitplus/wallet/lib/CryptoPublicKey.kt | 2 + .../wallet/lib/agent/CryptoService.kt | 10 +---- .../asitplus/wallet/lib/cbor/CoseAlgorithm.kt | 8 ++++ .../wallet/lib/cbor/CoseEllipticCurve.kt | 7 +++ .../at/asitplus/wallet/lib/cbor/CoseKey.kt | 11 +++++ .../asitplus/wallet/lib/cbor/CoseService.kt | 10 ++++- .../at/asitplus/wallet/lib/jws/EcCurve.kt | 14 ++++-- .../at/asitplus/wallet/lib/jws/JsonWebKey.kt | 12 +++++- .../asitplus/wallet/lib/jws/JwsAlgorithm.kt | 10 ++++- .../at/asitplus/wallet/lib/jws/JwsService.kt | 9 ++-- .../wallet/lib/agent/DefaultCryptoService.kt | 39 ++--------------- .../wallet/lib/agent/DefaultCryptoService.kt | 43 ++++++------------- 14 files changed, 90 insertions(+), 107 deletions(-) diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt index 140715a15..492645090 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt @@ -8,7 +8,6 @@ import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.data.AtomicAttribute2023 import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.iso.DrivingPrivilege -import at.asitplus.wallet.lib.iso.DrivingPrivilegeCode import at.asitplus.wallet.lib.iso.ElementValue import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DataElements import at.asitplus.wallet.lib.iso.IssuerSignedItem diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidvci/OidvciProcessTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidvci/OidvciProcessTest.kt index 8c87f9915..98485988b 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidvci/OidvciProcessTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidvci/OidvciProcessTest.kt @@ -1,24 +1,8 @@ package at.asitplus.wallet.lib.oidvci -import at.asitplus.KmmResult -import at.asitplus.wallet.lib.agent.CredentialToBeIssued import at.asitplus.wallet.lib.agent.DefaultCryptoService import at.asitplus.wallet.lib.agent.IssuerAgent -import at.asitplus.wallet.lib.agent.IssuerCredentialDataProvider -import at.asitplus.wallet.lib.cbor.CoseKey -import at.asitplus.wallet.lib.data.AtomicAttribute2023 import at.asitplus.wallet.lib.data.ConstantIndex -import at.asitplus.wallet.lib.iso.DeviceKeyInfo -import at.asitplus.wallet.lib.iso.DrivingPrivilege -import at.asitplus.wallet.lib.iso.DrivingPrivilegeCode -import at.asitplus.wallet.lib.iso.ElementValue -import at.asitplus.wallet.lib.iso.IsoDataModelConstants -import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DataElements -import at.asitplus.wallet.lib.iso.IssuerSignedItem -import at.asitplus.wallet.lib.iso.MobileSecurityObject -import at.asitplus.wallet.lib.iso.ValidityInfo -import at.asitplus.wallet.lib.iso.ValueDigest -import at.asitplus.wallet.lib.iso.ValueDigestList import at.asitplus.wallet.lib.oidc.DummyCredentialDataProvider import at.asitplus.wallet.lib.oidc.OpenIdConstants import at.asitplus.wallet.lib.oidc.OpenIdConstants.GRANT_TYPE_CODE @@ -26,11 +10,6 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.ktor.http.Url -import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate -import kotlinx.datetime.plus -import kotlin.random.Random class OidvciProcessTest : FunSpec({ diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt index 0616861e2..29b9c43c5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/CryptoPublicKey.kt @@ -117,4 +117,6 @@ sealed class CryptoPublicKey { private fun EcCurve.toCoseCurve(): CoseEllipticCurve = when (this) { EcCurve.SECP_256_R_1 -> CoseEllipticCurve.P256 + EcCurve.SECP_384_R_1 -> CoseEllipticCurve.P384 + EcCurve.SECP_521_R_1 -> CoseEllipticCurve.P521 } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index 66289df8f..616b9b2a7 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -3,7 +3,6 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm -import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm @@ -65,14 +64,7 @@ interface VerifierCryptoService { input: ByteArray, signature: ByteArray, algorithm: JwsAlgorithm, - publicKey: JsonWebKey - ): KmmResult - - fun verify( - input: ByteArray, - signature: ByteArray, - algorithm: CoseAlgorithm, - publicKey: CoseKey + publicKey: CryptoPublicKey, ): KmmResult } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt index 186be3b86..88045d15d 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseAlgorithm.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.cbor +import at.asitplus.wallet.lib.jws.JwsAlgorithm import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -16,6 +17,13 @@ enum class CoseAlgorithm(val value: Int) { ES512(-36), HMAC256_256(5); + fun toJwsAlgorithm() = when(this) { + ES256 -> JwsAlgorithm.ES256 + ES384 -> JwsAlgorithm.ES384 + ES512 -> JwsAlgorithm.ES512 + HMAC256_256 -> JwsAlgorithm.HMAC256 + } + val signatureValueLength get() = when (this) { ES256 -> 256 / 8 diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt index b495e0130..ea0d8ce34 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseEllipticCurve.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.cbor +import at.asitplus.wallet.lib.jws.EcCurve import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -14,6 +15,12 @@ enum class CoseEllipticCurve(val value: Int) { P256(1), P384(2), P521(3); + + fun toJwkCurve() = when (this) { + P256 -> EcCurve.SECP_256_R_1 + P384 -> EcCurve.SECP_384_R_1 + P521 -> EcCurve.SECP_521_R_1 + } //X25519(4), //X448(5), //Ed25519(6), diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt index 0c90a4255..1e3c9dcb1 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseKey.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.cbor import at.asitplus.KmmResult +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.iso.cborSerializer import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey @@ -172,5 +173,15 @@ data class CoseKey( return result } + fun toCryptoPublicKey(): CryptoPublicKey? { + if (this.type != CoseKeyType.EC2 || this.curve == null || this.keyId == null || this.x == null || this.y == null) return null + return CryptoPublicKey.Ec( + curve = curve.toJwkCurve(), + keyId = keyId.decodeToString(), + x = x, + y = y, + ) + } + } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt index 4a08f0808..b83069a05 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseService.kt @@ -95,7 +95,15 @@ class DefaultVerifierCoseService( val algorithm = coseSigned.protectedHeader.value.algorithm ?: return KmmResult.failure(IllegalArgumentException("Algorithm not specified")) - val verified = cryptoService.verify(signatureInput, coseSigned.signature, algorithm, signer) + val publicKey = signer.toCryptoPublicKey() + ?: return KmmResult.failure(IllegalArgumentException("Signer not convertibale")) + .also { Napier.w("Could not convert signer to public key: $signer") } + val verified = cryptoService.verify( + input = signatureInput, + signature = coseSigned.signature, + algorithm = algorithm.toJwsAlgorithm(), + publicKey = publicKey + ) val result = verified.getOrElse { Napier.w("No verification from native code", it) return KmmResult.failure(it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/EcCurve.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/EcCurve.kt index d86fc3701..7443e57b9 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/EcCurve.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/EcCurve.kt @@ -5,21 +5,29 @@ import kotlinx.serialization.Serializable @Serializable(with = EcCurveSerializer::class) enum class EcCurve(val jwkName: String) { - SECP_256_R_1("P-256"); + SECP_256_R_1("P-256"), + SECP_384_R_1("P-384"), + SECP_521_R_1("P-521"); val keyLengthBits get() = when (this) { SECP_256_R_1 -> 256 + SECP_384_R_1 -> 384 + SECP_521_R_1 -> 521 } val coordinateLengthBytes get() = when (this) { - SECP_256_R_1 -> 32 + SECP_256_R_1 -> 256 / 8 + SECP_384_R_1 -> 384 / 8 + SECP_521_R_1 -> 521 / 8 } val signatureLengthBytes get() = when (this) { - SECP_256_R_1 -> 32 + SECP_256_R_1 -> 256 / 8 + SECP_384_R_1 -> 384 / 8 + SECP_521_R_1 -> 521 / 8 } } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt index 0c6130c32..aaaddbf80 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt @@ -1,12 +1,12 @@ package at.asitplus.wallet.lib.jws import at.asitplus.KmmResult +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.data.jsonSerializer import io.github.aakira.napier.Napier import io.matthewnelson.component.base64.encodeBase64 import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okio.ByteString.Companion.toByteString @@ -138,5 +138,15 @@ data class JsonWebKey( return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId, x=${x?.encodeBase64()}, y=${y?.encodeBase64()})" } + fun toCryptoPublicKey(): CryptoPublicKey? { + if (this.type != JwkType.EC || this.curve == null || this.x == null || this.y == null) return null + return CryptoPublicKey.Ec( + curve = this.curve, + keyId = identifier, + x = x, + y = y, + ) + } + } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsAlgorithm.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsAlgorithm.kt index 422163bb3..9d795c0db 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsAlgorithm.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsAlgorithm.kt @@ -5,10 +5,16 @@ import kotlinx.serialization.Serializable @Serializable(with = JwsAlgorithmSerializer::class) enum class JwsAlgorithm(val text: String) { - ES256("ES256"); + ES256("ES256"), + ES384("ES384"), + ES512("ES512"), + HMAC256("HS256"); val signatureValueLength get() = when (this) { - ES256 -> 32 + ES256 -> 256 / 8 + ES384 -> 384 / 8 + ES512 -> 521 / 8 + HMAC256 -> 256 / 8 } } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt index 61aee2644..df8bca3f1 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt @@ -213,14 +213,14 @@ class DefaultVerifierJwsService( */ override fun verifyJwsObject(jwsObject: JwsSigned, serialized: String?): Boolean { val header = jwsObject.header - val publicKey = header.publicKey + val publicKey = header.publicKey?.toCryptoPublicKey() ?: return false .also { Napier.w("Could not extract PublicKey from header: $header") } val verified = cryptoService.verify( jwsObject.plainSignatureInput.encodeToByteArray(), jwsObject.signature, header.algorithm, - publicKey + publicKey, ) return verified.getOrElse { Napier.w("No verification from native code") @@ -232,11 +232,14 @@ class DefaultVerifierJwsService( * Verifiers the signature of [jwsObject] by using [signer]. */ override fun verifyJws(jwsObject: JwsSigned, signer: JsonWebKey): Boolean { + val publicKey = signer.toCryptoPublicKey() + ?: return false + .also { Napier.w("Could not convert signer to public key: $signer") } val verified = cryptoService.verify( jwsObject.plainSignatureInput.encodeToByteArray(), jwsObject.signature, jwsObject.header.algorithm, - signer + publicKey, ) return verified.getOrElse { Napier.w("No verification from native code") diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index ffcb79041..dd4ac310c 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -3,9 +3,6 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm -import at.asitplus.wallet.lib.cbor.CoseEllipticCurve -import at.asitplus.wallet.lib.cbor.CoseKey -import at.asitplus.wallet.lib.cbor.CoseKeyType import io.matthewnelson.component.base64.encodeBase64 import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey @@ -153,40 +150,11 @@ actual class DefaultVerifierCryptoService : VerifierCryptoService { input: ByteArray, signature: ByteArray, algorithm: JwsAlgorithm, - publicKey: JsonWebKey + publicKey: CryptoPublicKey ): KmmResult { - memScoped { - val ansix962 = publicKey.toAnsiX963ByteArray().getOrElse { - return KmmResult.failure(it) - } - val keyData = CFBridgingRetain(toData(ansix962)) as CFDataRef - val attributes = CFDictionaryCreateMutable(null, 3, null, null).apply { - CFDictionaryAddValue1(this, kSecAttrKeyClass, kSecAttrKeyClassPublic) - CFDictionaryAddValue1(this, kSecAttrKeyType, kSecAttrKeyTypeEC) - CFDictionaryAddValue1(this, kSecAttrKeySizeInBits, CFBridgingRetain(NSNumber(256))) - } - val secKey = SecKeyCreateWithData(keyData, attributes, null) - ?: return KmmResult.failure(IllegalArgumentException()) - val inputData = CFBridgingRetain(toData(input)) as CFDataRef - val signatureData = CFBridgingRetain(toData(signature.convertToAsn1Signature(32))) as CFDataRef - val verified = SecKeyVerifySignature( - secKey, - kSecKeyAlgorithmECDSASignatureMessageX962SHA256, - inputData, - signatureData, - null - ) - return KmmResult.success(verified) + if (publicKey !is CryptoPublicKey.Ec) { + return KmmResult.failure(IllegalArgumentException("Public key is not an EC key")) } - } - - - override fun verify( - input: ByteArray, - signature: ByteArray, - algorithm: CoseAlgorithm, - publicKey: CoseKey - ): KmmResult { memScoped { val ansix962 = publicKey.toAnsiX963ByteArray().getOrElse { return KmmResult.failure(it) @@ -211,6 +179,7 @@ actual class DefaultVerifierCryptoService : VerifierCryptoService { return KmmResult.success(verified) } } + } @Suppress("UNCHECKED_CAST") diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 67981f680..8f6117ebf 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -5,8 +5,6 @@ import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.cbor.CoseEllipticCurve import at.asitplus.wallet.lib.cbor.CoseEllipticCurve.P256 -import at.asitplus.wallet.lib.cbor.CoseKey -import at.asitplus.wallet.lib.cbor.CoseKeyType.EC2 import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.EcCurve.SECP_256_R_1 import at.asitplus.wallet.lib.jws.JsonWebKey @@ -212,28 +210,13 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { input: ByteArray, signature: ByteArray, algorithm: JwsAlgorithm, - publicKey: JsonWebKey + publicKey: CryptoPublicKey, ): KmmResult { - return try { - val asn1Signature = signature.convertToAsn1Signature(publicKey.curve?.signatureLengthBytes ?: 32) - val result = Signature.getInstance(algorithm.jcaName).apply { - initVerify(publicKey.getPublicKey()) - update(input) - }.verify(asn1Signature) - KmmResult.success(result) - } catch (e: Throwable) { - KmmResult.failure(e) + if (publicKey !is CryptoPublicKey.Ec) { + return KmmResult.failure(IllegalArgumentException("Public key is not an EC key")) } - } - - override fun verify( - input: ByteArray, - signature: ByteArray, - algorithm: CoseAlgorithm, - publicKey: CoseKey - ): KmmResult { return try { - val asn1Signature = signature.convertToAsn1Signature(publicKey.curve?.signatureLengthBytes ?: 32) + val asn1Signature = signature.convertToAsn1Signature(publicKey.curve.signatureLengthBytes) val result = Signature.getInstance(algorithm.jcaName).apply { initVerify(publicKey.getPublicKey()) update(input) @@ -243,6 +226,7 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { KmmResult.failure(e) } } + } actual object CryptoUtils { @@ -257,14 +241,9 @@ actual object CryptoUtils { val JwsAlgorithm.jcaName get() = when (this) { JwsAlgorithm.ES256 -> "SHA256withECDSA" - } - -val CoseAlgorithm.jcaName - get() = when (this) { - CoseAlgorithm.ES256 -> "SHA256withECDSA" - CoseAlgorithm.ES384 -> "SHA384withECDSA" - CoseAlgorithm.ES512 -> "SHA512withECDSA" - CoseAlgorithm.HMAC256_256 -> "HmacSHA256" + JwsAlgorithm.ES384 -> "SHA384withECDSA" + JwsAlgorithm.ES512 -> "SHA512withECDSA" + JwsAlgorithm.HMAC256 -> "HmacSHA256" } val Digest.jcaName @@ -290,6 +269,8 @@ val JweAlgorithm.jcaName val EcCurve.jcaName get() = when (this) { SECP_256_R_1 -> "secp256r1" + EcCurve.SECP_384_R_1 -> "secp384r1" + EcCurve.SECP_521_R_1 -> "secp521r1" } val CoseEllipticCurve.jcaName @@ -303,8 +284,8 @@ fun JsonWebKey.getPublicKey(): PublicKey { return toPublicKey(curve?.jcaName, x, y) } -fun CoseKey.getPublicKey(): PublicKey { - return toPublicKey(curve?.jcaName, x, y) +fun CryptoPublicKey.Ec.getPublicKey(): PublicKey { + return toPublicKey(curve.jcaName, x, y) } private fun toPublicKey(curveName: String?, xCoordinate: ByteArray?, yCoordinate: ByteArray?): JCEECPublicKey { From 52e9833eda5031250b39eb43739c0ac87290192d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 14 Aug 2023 10:54:27 +0200 Subject: [PATCH 08/11] add dry run publishing action --- .github/workflows/publish-dry-run.yml | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/publish-dry-run.yml diff --git a/.github/workflows/publish-dry-run.yml b/.github/workflows/publish-dry-run.yml new file mode 100644 index 000000000..f242804c3 --- /dev/null +++ b/.github/workflows/publish-dry-run.yml @@ -0,0 +1,36 @@ +name: Publish Dry Run +on: workflow_dispatch +permissions: + contents: read + pages: write + id-token: write +jobs: + build: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + - name: Publish to Maven Local + run: ./gradlew clean publishToMavenLocal + env: + ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PUBLISH_SIGNING_KEYID }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PUBLISH_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PUBLISH_SIGNING_PASSWORD }} + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.PUBLISH_SONATYPE_USER }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.PUBLISH_SONATYPE_PASSWORD }} + deploy-docs: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build Dokka HTML + run: ./gradlew dokkaHtmlMultiModule \ No newline at end of file From 25a806ba9c334f5574dfefe241de7e6de52c8419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Tue, 22 Aug 2023 12:57:06 +0200 Subject: [PATCH 09/11] Draft release 2.1.0 --- CHANGELOG.md | 5 ++++- README.md | 3 ++- gradle.properties | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dca88a91a..888375f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Changelog Release 2.1.0: - - tbd + - Creating, issung, managing and verifying ISO/IEC 18013-5:2021 credentials + - Kotlin 1.9 + - Generic structure for public keys + - kotlinx.serialization fork with CBOR enhancements for COSE support Release 2.0.2: - `vclib-openid`: Add response modes for query and fragment, i.e. Wallet may return the authentication response in query params or as fragment params on a SIOPv2 call diff --git a/README.md b/README.md index 649793630..b7429e678 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # KMM VC Library [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-brightgreen.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Kotlin](https://img.shields.io/badge/kotlin-multiplatform--mobile-orange.svg?logo=kotlin)](http://kotlinlang.org) -[![Kotlin](https://img.shields.io/badge/kotlin-1.8.10-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Kotlin](https://img.shields.io/badge/kotlin-1.9.0-blue.svg?logo=kotlin)](http://kotlinlang.org) ![Java](https://img.shields.io/badge/java-11-blue.svg?logo=OPENJDK) [![Maven Central](https://img.shields.io/maven-central/v/at.asitplus.wallet/vclib)](https://mvnrepository.com/artifact/at.asitplus.wallet/vclib/) This [Kotlin Mulitplatform](https://kotlinlang.org/docs/multiplatform.html) library implements the [W3C VC Data Model](https://w3c.github.io/vc-data-model/) to support several use cases of verifiable credentials, verifiable presentations, and validation thereof. This library may be shared between Wallet Apps, Verifier Apps and a Backend Service issuing credentials. + ## Architecture This library was built with [Kotlin Mulitplatform](https://kotlinlang.org/docs/multiplatform.html) and [Mulitplatform Mobile](https://kotlinlang.org/lp/mobile/) in mind. Its primary targets are JVM, Android and iOS. In order to achieve smooth usage especially under iOS, there have been some notable design decisions: diff --git a/gradle.properties b/gradle.properties index c1bb9d53c..7bc215c86 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,4 @@ kotlin.experimental.tryK2=false # workaround dokka bug (need to wait for next snapshot build) org.jetbrains.dokka.classpath.excludePlatformDependencyFiles=true -artifactVersion = 2.1.0-SNAPSHOT +artifactVersion = 2.1.0 From 2bec79ac7157a957d27db6dbed8c6b84bb81ad20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Wed, 23 Aug 2023 22:02:13 +0200 Subject: [PATCH 10/11] update conventions --- conventions-vclib/gradle-conventions-plugin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conventions-vclib/gradle-conventions-plugin b/conventions-vclib/gradle-conventions-plugin index 3c4c91754..f43262be2 160000 --- a/conventions-vclib/gradle-conventions-plugin +++ b/conventions-vclib/gradle-conventions-plugin @@ -1 +1 @@ -Subproject commit 3c4c91754fb5c92d06ef1f82191a9866825b9f48 +Subproject commit f43262be20f3ca7335ef7ffe9d6334d64fefe936 From 5cc7d831f172bf6fc188a2fbd2f2cd83ddfbf830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 24 Aug 2023 10:30:58 +0200 Subject: [PATCH 11/11] update conventons --- conventions-vclib/gradle-conventions-plugin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conventions-vclib/gradle-conventions-plugin b/conventions-vclib/gradle-conventions-plugin index f43262be2..025f17e7b 160000 --- a/conventions-vclib/gradle-conventions-plugin +++ b/conventions-vclib/gradle-conventions-plugin @@ -1 +1 @@ -Subproject commit f43262be20f3ca7335ef7ffe9d6334d64fefe936 +Subproject commit 025f17e7bd9ab6f4c2a878d384f412cb5081c2c3