From 8c57d91a7f42322e27cc1c7efd5b0248a5ef2839 Mon Sep 17 00:00:00 2001 From: Simon Mueller Date: Thu, 7 Dec 2023 16:30:21 +0100 Subject: [PATCH] Fix Encoding-Decoding --- kmp-crypto | 2 +- .../wallet/lib/aries/MessageWrapper.kt | 4 +- .../wallet/lib/oidc/OidcSiopVerifier.kt | 4 +- .../wallet/lib/oidc/OidcSiopWallet.kt | 6 +- .../wallet/lib/oidvci/WalletService.kt | 2 +- .../asitplus/wallet/lib/agent/HolderAgent.kt | 2 +- .../asitplus/wallet/lib/agent/IssuerAgent.kt | 2 +- .../at/asitplus/wallet/lib/agent/Validator.kt | 6 +- .../at/asitplus/wallet/lib/jws/JwsService.kt | 35 ++-- .../wallet/lib/agent/ValidatorVcTest.kt | 65 +++---- .../wallet/lib/agent/ValidatorVpTest.kt | 8 +- .../asitplus/wallet/lib/jws/JwsServiceTest.kt | 132 ++++++------- .../wallet/lib/agent/DefaultCryptoService.kt | 51 +++-- .../wallet/lib/jws/JwsServiceJvmTest.kt | 176 ++++++++++-------- 14 files changed, 246 insertions(+), 249 deletions(-) diff --git a/kmp-crypto b/kmp-crypto index 0559c018d..897271b57 160000 --- a/kmp-crypto +++ b/kmp-crypto @@ -1 +1 @@ -Subproject commit 0559c018d8b22b94da86e7a3799b4c99891915bb +Subproject commit 897271b57d7638fd2bac40d1b582b728fab375b9 diff --git a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt index f8edce042..b6fa119c5 100644 --- a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt +++ b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt @@ -54,7 +54,7 @@ class MessageWrapper( private fun parseJwsMessage(joseObject: JwsSigned, serialized: String): ReceivedMessage { Napier.d("Parsing JWS ${joseObject.serialize()}") - if (!verifierJwsService.verifyJwsObject(joseObject, serialized)) + if (!verifierJwsService.verifyJwsObject(joseObject)) return ReceivedMessage.Error .also { Napier.w("Signature invalid") } if (joseObject.header.contentType == JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) { @@ -99,7 +99,7 @@ class MessageWrapper( JwsContentTypeConstants.DIDCOMM_SIGNED_JSON, jwm.serialize().encodeToByteArray(), JwsContentTypeConstants.DIDCOMM_PLAIN_JSON - ) + )?.serialize() } } \ No newline at end of file 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 6232c6fa9..719610b9a 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 @@ -143,7 +143,7 @@ class OidcSiopVerifier( JwsHeader(algorithm = JwsAlgorithm.ES256), requestObjectSerialized.encodeToByteArray(), true - ) + )?.serialize() return AuthenticationRequestParameters(clientId = relyingPartyUrl, request = signedJws) } @@ -293,7 +293,7 @@ class OidcSiopVerifier( val jwsSigned = JwsSigned.parse(idTokenJws) ?: return AuthnResponseResult.ValidationError("idToken", params.state) .also { Napier.w("Could not parse JWS from idToken: $idTokenJws") } - if (!verifierJwsService.verifyJwsObject(jwsSigned, idTokenJws)) + if (!verifierJwsService.verifyJwsObject(jwsSigned)) return AuthnResponseResult.ValidationError("idToken", params.state) .also { Napier.w { "JWS of idToken not verified: $idTokenJws" } } val idToken = IdToken.deserialize(jwsSigned.payload.decodeToString()) 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 bd6faae86..f8943aedf 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 @@ -121,7 +121,7 @@ class OidcSiopWallet( private fun extractRequestObject(params: AuthenticationRequestParameters): AuthenticationRequestParameters? { params.request?.let { requestObject -> JwsSigned.parse(requestObject)?.let { jws -> - if (verifierJwsService.verifyJwsObject(jws, requestObject)) { + if (verifierJwsService.verifyJwsObject(jws)) { return kotlin.runCatching { jsonSerializer.decodeFromString(jws.payload.decodeToString()) }.getOrNull() @@ -224,8 +224,8 @@ class OidcSiopWallet( nonce = params.nonce, ) val jwsPayload = idToken.serialize().encodeToByteArray() - val jwsHeader = JwsHeader(JwsAlgorithm.ES256) - val signedIdToken = jwsService.createSignedJwsAddingParams(jwsHeader, jwsPayload) + val jwsHeader = JwsHeader(algorithm = JwsAlgorithm.ES256) + val signedIdToken = jwsService.createSignedJwsAddingParams(jwsHeader, jwsPayload)?.serialize() ?: return KmmResult.failure(OAuth2Exception(Errors.USER_CANCELLED)) .also { Napier.w("Could not sign id_token") } diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt index 9b7ae6e69..150446697 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt @@ -102,7 +102,7 @@ class WalletService( ).serialize().encodeToByteArray(), addKeyId = true, addJsonWebKey = true - )!! + )!!.serialize() ) return when (credentialScheme.credentialFormat) { ConstantIndex.CredentialFormat.ISO_18013 -> CredentialRequestParameters( 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 80539a313..103b2c5ab 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 @@ -242,7 +242,7 @@ class HolderAgent( val jws = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) ?: return null .also { Napier.w("Could not create JWS for presentation") } - return Holder.CreatePresentationResult.Signed(jws) + return Holder.CreatePresentationResult.Signed(jws.serialize()) } 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 70057c247..887f24a62 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 @@ -242,7 +242,7 @@ class IssuerAgent( private suspend fun wrapVcInJws(vc: VerifiableCredential): String? { val jwsPayload = vc.toJws().serialize().encodeToByteArray() - return jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + return jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)?.serialize() } private fun getRevocationListUrlFor(timePeriod: Int) = 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 a2904c1c8..d89825512 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 @@ -65,7 +65,7 @@ class Validator( val jws = JwsSigned.parse(it) ?: return false .also { Napier.w("Revocation List: Could not parse JWS") } - if (!verifierJwsService.verifyJwsObject(jws, it)) + if (!verifierJwsService.verifyJwsObject(jws)) return false .also { Napier.w("Revocation List: Signature invalid") } val payload = jws.payload.decodeToString() @@ -140,7 +140,7 @@ class Validator( val jws = JwsSigned.parse(it) ?: return Verifier.VerifyPresentationResult.InvalidStructure(it) .also { Napier.w("VP: Could not parse JWS") } - if (!verifierJwsService.verifyJwsObject(jws, it)) + if (!verifierJwsService.verifyJwsObject(jws)) return Verifier.VerifyPresentationResult.InvalidStructure(it) .also { Napier.w("VP: Signature invalid") } val payload = jws.payload.decodeToString() @@ -290,7 +290,7 @@ class Validator( val jws = JwsSigned.parse(it) ?: return Verifier.VerifyCredentialResult.InvalidStructure(it) .also { Napier.w("VC: Could not parse JWS") } - if (!verifierJwsService.verifyJwsObject(jws, it)) + if (!verifierJwsService.verifyJwsObject(jws)) return Verifier.VerifyCredentialResult.InvalidStructure(it) .also { Napier.w("VC: Signature invalid") } val payload = jws.payload.decodeToString() 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 97cf478c1..05d571c26 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 @@ -9,6 +9,7 @@ import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.DefaultVerifierCryptoService import at.asitplus.wallet.lib.agent.VerifierCryptoService import io.github.aakira.napier.Napier +import io.ktor.util.* import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString @@ -23,9 +24,9 @@ interface JwsService { type: String, payload: ByteArray, contentType: String? = null - ): String? + ): JwsSigned? - suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): String? + suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): JwsSigned? /** * Appends correct values for [JweHeader.keyId], [JwsHeader.algorithm] and [JwsHeader.jsonWebKey], @@ -36,7 +37,7 @@ interface JwsService { payload: ByteArray, addKeyId: Boolean = true, addJsonWebKey: Boolean = true - ): String? + ): JwsSigned? fun encryptJweObject( type: String, @@ -53,7 +54,7 @@ interface JwsService { interface VerifierJwsService { - fun verifyJwsObject(jwsObject: JwsSigned, serialized: String? = null): Boolean + fun verifyJwsObject(jwsObject: JwsSigned): Boolean fun verifyJws(jwsObject: JwsSigned, signer: JsonWebKey): Boolean @@ -65,7 +66,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { type: String, payload: ByteArray, contentType: String? - ): String? { + ): JwsSigned? { val jwsHeader = JwsHeader( algorithm = cryptoService.algorithm, keyId = cryptoService.publicKey.keyId, @@ -75,21 +76,20 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { return createSignedJws(jwsHeader, payload) } - override suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): String? { + override suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): JwsSigned? { if (header.algorithm != cryptoService.algorithm || header.keyId?.let { it != cryptoService.publicKey.keyId } == true || header.jsonWebKey?.let { it != cryptoService.jsonWebKey } == true ) { return null.also { Napier.w("Algorithm or keyId not matching to cryptoService") } } - val signatureInput = header.serialize().encodeToByteArray().encodeToString(Base64UrlStrict) + - "." + payload.encodeToString(Base64UrlStrict) - val signatureInputBytes = signatureInput.encodeToByteArray() + + val signatureInputBytes = prepareJwsSignatureInput(header, payload).encodeToByteArray() val signature = cryptoService.sign(signatureInputBytes).getOrElse { Napier.w("No signature from native code", it) return null } - return JwsSigned(header, payload, signature, signatureInput).serialize() + return JwsSigned(header, payload, signature) } override suspend fun createSignedJwsAddingParams( @@ -97,7 +97,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { payload: ByteArray, addKeyId: Boolean, addJsonWebKey: Boolean - ): String? { + ): JwsSigned? { var copy = header.copy(algorithm = cryptoService.algorithm) if (addKeyId) copy = copy.copy(keyId = cryptoService.publicKey.keyId) @@ -211,7 +211,7 @@ class DefaultVerifierJwsService( * Verifies the signature of [jwsObject], by extracting the public key from [JwsHeader.keyId] (`kid`), * or from [JwsHeader.jsonWebKey] (`jwk`), or from [JwsHeader.certificateChain] (`x5c`). */ - override fun verifyJwsObject(jwsObject: JwsSigned, serialized: String?): Boolean { + override fun verifyJwsObject(jwsObject: JwsSigned): Boolean { val header = jwsObject.header val publicKey = header.publicKey ?: return false @@ -222,11 +222,12 @@ class DefaultVerifierJwsService( algorithm = header.algorithm, publicKey = publicKey ) - val falseVar = false //workaround kotlin bug for linking xcframework - return verified.getOrElse { - Napier.w("No verification from native code") - falseVar - } +// val falseVar = false //workaround kotlin bug for linking xcframework +// return verified.getOrElse { +// Napier.w("No verification from native code", it) +// falseVar +// } + return verified.getOrThrow() } /** diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt index c852dc43a..3b339390a 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt @@ -1,10 +1,10 @@ package at.asitplus.wallet.lib.agent import at.asitplus.crypto.datatypes.JwsAlgorithm -import at.asitplus.crypto.datatypes.io.Base64UrlStrict import at.asitplus.crypto.datatypes.jws.JwsContentTypeConstants import at.asitplus.crypto.datatypes.jws.JwsHeader import at.asitplus.crypto.datatypes.jws.JwsSigned +import at.asitplus.crypto.datatypes.jws.prepareJwsSignatureInput import at.asitplus.wallet.lib.data.* import at.asitplus.wallet.lib.jws.DefaultJwsService import at.asitplus.wallet.lib.jws.JwsService @@ -14,7 +14,6 @@ import io.kotest.datatest.withData import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf -import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.hours @@ -243,10 +242,8 @@ class ValidatorVcTest : FreeSpec() { verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) ).getOrThrow() - ) { - issueCredential(it, expirationDate = null) - .let { wrapVcInJws(it) } - .let { signJws(it) } + ) { it -> + signJws(wrapVcInJws(issueCredential(it, expirationDate = null))) ?.let { verifier.verifyVcJws(it).shouldBeInstanceOf() } @@ -260,10 +257,13 @@ class ValidatorVcTest : FreeSpec() { verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) ).getOrThrow() - ) { - issueCredential(it, expirationDate = Clock.System.now() + 1.hours) - .let { wrapVcInJws(it, expirationDate = Clock.System.now() - 1.hours) } - .let { signJws(it) } + ) { it -> + signJws( + wrapVcInJws( + issueCredential(it, expirationDate = Clock.System.now() + 1.hours), + expirationDate = Clock.System.now() - 1.hours + ) + ) ?.let { verifier.verifyVcJws(it) .shouldBeInstanceOf() @@ -278,18 +278,16 @@ class ValidatorVcTest : FreeSpec() { verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) ).getOrThrow() - ) { - it.let { - issueCredential(it, expirationDate = Clock.System.now() + 1.hours) + ) { it -> + signJws( + wrapVcInJws( + issueCredential(it, expirationDate = Clock.System.now() + 1.hours), + expirationDate = Clock.System.now() + 2.hours + ) + )?.let { + verifier.verifyVcJws(it) + .shouldBeInstanceOf() } - .let { - wrapVcInJws(it, expirationDate = Clock.System.now() + 2.hours) - } - .let { signJws(it) } - ?.let { - verifier.verifyVcJws(it) - .shouldBeInstanceOf() - } } } @@ -300,12 +298,8 @@ class ValidatorVcTest : FreeSpec() { verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) ).getOrThrow() - ) { - it.let { issueCredential(it) } - .let { - wrapVcInJws(it, issuanceDate = Clock.System.now() + 2.hours) - } - .let { signJws(it) } + ) { it -> + signJws(wrapVcInJws(issueCredential(it), issuanceDate = Clock.System.now() + 2.hours)) ?.let { verifier.verifyVcJws(it) .shouldBeInstanceOf() @@ -338,10 +332,8 @@ class ValidatorVcTest : FreeSpec() { verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) ).getOrThrow() - ) { - issueCredential(it, issuanceDate = Clock.System.now() - 1.hours) - .let { wrapVcInJws(it, issuanceDate = Clock.System.now()) } - .let { signJws(it) } + ) { it -> + signJws(wrapVcInJws(issueCredential(it, issuanceDate = Clock.System.now() - 1.hours), issuanceDate = Clock.System.now())) ?.let { verifier.verifyVcJws(it) .shouldBeInstanceOf() @@ -405,7 +397,7 @@ class ValidatorVcTest : FreeSpec() { private suspend fun signJws(vcJws: VerifiableCredentialJws): String? { val vcSerialized = vcJws.serialize() val jwsPayload = vcSerialized.encodeToByteArray() - return issuerJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + return issuerJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)?.serialize() } private suspend fun wrapVcInJwsWrongKey(vcJws: VerifiableCredentialJws): String? { @@ -415,13 +407,10 @@ class ValidatorVcTest : FreeSpec() { type = JwsContentTypeConstants.JWT ) val jwsPayload = vcJws.serialize().encodeToByteArray() - val signatureInput = - jwsHeader.serialize().encodeToByteArray().encodeToString(Base64UrlStrict) + - "." + jwsPayload.encodeToString(Base64UrlStrict) - val signatureInputBytes = signatureInput.encodeToByteArray() - val signature = issuerCryptoService.sign(signatureInputBytes) + val signatureInput = prepareJwsSignatureInput(jwsHeader, jwsPayload).encodeToByteArray() + val signature = issuerCryptoService.sign(signatureInput) .getOrElse { return null } - return JwsSigned(jwsHeader, jwsPayload, signature, signatureInput).serialize() + return JwsSigned(jwsHeader, jwsPayload, signature).serialize() } } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt index 0727c1b9f..b89e16756 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt @@ -135,7 +135,7 @@ class ValidatorVpTest : FreeSpec({ audienceId = verifier.identifier, ).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)?.serialize() vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) @@ -156,7 +156,7 @@ class ValidatorVpTest : FreeSpec({ jwtId = vp.id, ).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)?.serialize() vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) @@ -176,7 +176,7 @@ class ValidatorVpTest : FreeSpec({ jwtId = "wrong_jwtId", ).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)?.serialize() vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) @@ -198,7 +198,7 @@ class ValidatorVpTest : FreeSpec({ audienceId = verifier.identifier, ).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)?.serialize() vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) 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 91f295bf2..81e5cc0f0 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 @@ -18,83 +18,75 @@ class JwsServiceTest : FreeSpec({ lateinit var verifierJwsService: VerifierJwsService lateinit var randomPayload: String - for (i in 1..10) { - beforeEach { - cryptoService = DefaultCryptoService() - jwsService = DefaultJwsService(cryptoService) - verifierJwsService = DefaultVerifierJwsService() - randomPayload = uuid4().toString() - } - - "signed object with bytes can be verified" { - val payload = randomPayload.encodeToByteArray() - val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, payload) - signed.shouldNotBeNull() - - val parsed = JwsSigned.parse(signed) - parsed.shouldNotBeNull() - parsed.payload shouldBe payload - - val result = verifierJwsService.verifyJwsObject(parsed, signed) - result shouldBe true - } - - "signed object can be verified" { - val stringPayload = jsonSerializer.encodeToString(randomPayload) - val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, stringPayload.encodeToByteArray()) - signed.shouldNotBeNull() - - val parsed = JwsSigned.parse(signed) - parsed.shouldNotBeNull() - parsed.payload.decodeToString() shouldBe stringPayload - - val result = verifierJwsService.verifyJwsObject(parsed, signed) - result shouldBe true - } + beforeEach { + cryptoService = DefaultCryptoService() + jwsService = DefaultJwsService(cryptoService) + verifierJwsService = DefaultVerifierJwsService() + randomPayload = uuid4().toString() + } - "signed object with automatically added params can be verified" { - val payload = randomPayload.encodeToByteArray() - val signed = jwsService.createSignedJwsAddingParams(JwsHeader(JwsAlgorithm.ES256), payload) - signed.shouldNotBeNull() + "signed object with bytes can be verified" { + val payload = randomPayload.encodeToByteArray() + val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, payload) + signed.shouldNotBeNull() + val result = verifierJwsService.verifyJwsObject(signed) + result shouldBe true + } - val parsed = JwsSigned.parse(signed) - parsed.shouldNotBeNull() - parsed.payload shouldBe payload + "Object can be reconstructed" { + val payload = randomPayload.encodeToByteArray() + val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, payload)?.serialize() + signed.shouldNotBeNull() - val result = verifierJwsService.verifyJwsObject(parsed, signed) - result shouldBe true - } + val parsed = JwsSigned.parse(signed) + parsed.shouldNotBeNull() + parsed.serialize() shouldBe signed + parsed.payload shouldBe payload - "signed object with jsonWebKey can be verified" { - val payload = randomPayload.encodeToByteArray() - val header = JwsHeader(JwsAlgorithm.ES256, jsonWebKey = cryptoService.jsonWebKey) - val signed = jwsService.createSignedJws(header, payload) - signed.shouldNotBeNull() + val result = verifierJwsService.verifyJwsObject(parsed) + result shouldBe true + } - val parsed = JwsSigned.parse(signed) - parsed.shouldNotBeNull() - parsed.payload shouldBe payload + "signed object can be verified" { + val stringPayload = jsonSerializer.encodeToString(randomPayload) + val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, stringPayload.encodeToByteArray()) + signed.shouldNotBeNull() + val result = verifierJwsService.verifyJwsObject(signed) + result shouldBe true + } - val result = verifierJwsService.verifyJwsObject(parsed, signed) - result shouldBe true - } + "signed object with automatically added params can be verified" { + val payload = randomPayload.encodeToByteArray() + val signed = jwsService.createSignedJwsAddingParams(JwsHeader(algorithm = JwsAlgorithm.ES256), payload) + signed.shouldNotBeNull() + val result = verifierJwsService.verifyJwsObject(signed) + result shouldBe true + } - "encrypted object can be decrypted" { - val stringPayload = jsonSerializer.encodeToString(randomPayload) - val encrypted = jwsService.encryptJweObject( - JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, - stringPayload.encodeToByteArray(), - cryptoService.jsonWebKey, - JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, - JweAlgorithm.ECDH_ES, - JweEncryption.A256GCM, - ) - encrypted.shouldNotBeNull() - val parsed = JweEncrypted.parse(encrypted) - parsed.shouldNotBeNull() + "signed object with jsonWebKey can be verified" { + val payload = randomPayload.encodeToByteArray() + val header = JwsHeader(algorithm = JwsAlgorithm.ES256, jsonWebKey = cryptoService.jsonWebKey) + val signed = jwsService.createSignedJws(header, payload) + signed.shouldNotBeNull() + val result = verifierJwsService.verifyJwsObject(signed) + result shouldBe true + } - val result = jwsService.decryptJweObject(parsed, encrypted) - result?.payload?.decodeToString() shouldBe stringPayload - } + "encrypted object can be decrypted" { + val stringPayload = jsonSerializer.encodeToString(randomPayload) + val encrypted = jwsService.encryptJweObject( + JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, + stringPayload.encodeToByteArray(), + cryptoService.jsonWebKey, + JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, + JweAlgorithm.ECDH_ES, + JweEncryption.A256GCM, + ) + encrypted.shouldNotBeNull() + val parsed = JweEncrypted.parse(encrypted) + parsed.shouldNotBeNull() + + val result = jwsService.decryptJweObject(parsed, encrypted) + result?.payload?.decodeToString() shouldBe stringPayload } }) 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 9d2353ffb..a27e256f9 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,16 +3,20 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult import at.asitplus.KmmResult.Companion.wrap import at.asitplus.crypto.datatypes.* +import at.asitplus.crypto.datatypes.Digest import at.asitplus.crypto.datatypes.EcCurve.SECP_256_R_1 import at.asitplus.crypto.datatypes.asn1.Asn1String import at.asitplus.crypto.datatypes.asn1.Asn1Time import at.asitplus.crypto.datatypes.cose.CoseKey import at.asitplus.crypto.datatypes.cose.toCoseAlgorithm import at.asitplus.crypto.datatypes.cose.toCoseKey +import at.asitplus.crypto.datatypes.io.Base64UrlStrict import at.asitplus.crypto.datatypes.jws.* import at.asitplus.crypto.datatypes.pki.DistinguishedName import at.asitplus.crypto.datatypes.pki.TbsCertificate import at.asitplus.crypto.datatypes.pki.X509Certificate +import io.ktor.util.* +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.coroutines.runBlocking import kotlinx.datetime.toKotlinInstant import org.bouncycastle.jce.ECNamedCurveTable @@ -64,7 +68,7 @@ actual open class DefaultCryptoService : CryptoService { * Constructor which allows all public keys implemented in `KMP-Crypto` * Because RSA needs the algorithm parameter to be useful (as it cannot be inferred from the key) * it's mandatory - * Also used for non-self-signed certificates + * Also used for custom certificates */ constructor(keyPair: KeyPair, algorithm: JwsAlgorithm, certificate: Certificate? = null) { this.privateKey = keyPair.private @@ -76,24 +80,6 @@ actual open class DefaultCryptoService : CryptoService { certificate?.let { X509Certificate.decodeFromDer(it.encoded) } ?: generateSelfSignedCertificate() } -// 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(algorithm.jcaName).build(keyPair.private) -// val certificateHolder = builder.build(contentSigner) -// return certificateHolder.encoded -// } - private fun generateSelfSignedCertificate(): X509Certificate { val serialNumber: BigInteger = BigInteger.valueOf(Random.nextLong().absoluteValue) val commonName = "DefaultCryptoService" @@ -119,18 +105,29 @@ actual open class DefaultCryptoService : CryptoService { return X509Certificate(tbsCertificate, algorithm, signature) } + // override suspend fun sign(input: ByteArray): KmmResult = +// runCatching { +// Signature.getInstance(algorithm.jcaName).apply { +// initSign(privateKey) +// update(input) +// }.sign() +// }.wrap().mapCatching { +//// when (algorithm) { +//// JwsAlgorithm.ES256, JwsAlgorithm.ES384, JwsAlgorithm.ES512 -> CryptoSignature.EC(it) +//// else -> CryptoSignature.RSAorHMAC(it) +//// } +// CryptoSignature.decodeFromDer(it) +// } override suspend fun sign(input: ByteArray): KmmResult = runCatching { - Signature.getInstance(algorithm.jcaName).apply { + val sig = Signature.getInstance(algorithm.jcaName).apply { initSign(privateKey) update(input) }.sign() - }.wrap().mapCatching { - when (algorithm) { - JwsAlgorithm.ES256, JwsAlgorithm.ES384, JwsAlgorithm.ES512 -> CryptoSignature.EC(it) - else -> CryptoSignature.RSAorHMAC(it) - } - } + val test = sig.encodeToString(Base64UrlStrict) + println(test) + CryptoSignature.decodeFromDer(sig) + }.wrap().also { it.getOrThrow() } override fun encrypt( key: ByteArray, iv: ByteArray, aad: ByteArray, input: ByteArray, algorithm: JweEncryption @@ -217,7 +214,7 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { Signature.getInstance(algorithm.jcaName).apply { initVerify(publicKey.getJcaPublicKey().getOrThrow()) update(input) - }.verify(signature.rawByteArray) + }.verify(signature.encodeToDer()) }.wrap() } 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 f51e443cb..c7ae9c586 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 @@ -12,9 +12,11 @@ import com.nimbusds.jose.crypto.ECDHEncrypter import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jose.crypto.ECDSAVerifier import io.kotest.core.spec.style.FreeSpec +import io.kotest.datatest.withData import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe +import io.kotest.mpp.timeInMillis import kotlinx.serialization.encodeToString import java.security.KeyPair import java.security.KeyPairGenerator @@ -23,89 +25,105 @@ import java.security.interfaces.ECPublicKey class JwsServiceJvmTest : FreeSpec({ - lateinit var keyPair: KeyPair lateinit var cryptoService: CryptoService lateinit var jwsService: JwsService lateinit var verifierJwsService: VerifierJwsService lateinit var randomPayload: String - - beforeTest { - keyPair = KeyPairGenerator.getInstance("EC").also { - it.initialize(256) - }.genKeyPair() - cryptoService = DefaultCryptoService(keyPair, JwsAlgorithm.ES256) - jwsService = DefaultJwsService(cryptoService) - verifierJwsService = DefaultVerifierJwsService() - randomPayload = uuid4().toString() - } - - "signed object from ext. library can be verified" { - val stringPayload = jsonSerializer.encodeToString(randomPayload) - val libHeader = JWSHeader.Builder(JWSAlgorithm.ES256) - .type(JOSEObjectType("JWT")) - .keyID(cryptoService.jsonWebKey.keyId) - .build() - val libObject = JWSObject(libHeader, Payload(stringPayload)).also { - it.sign(ECDSASigner(keyPair.private as ECPrivateKey)) + lateinit var publicKeyDesc: List> + + "EC" - { + withData(256, 384, 521) { bits -> + publicKeyDesc = List>(5) { + val keyPair = KeyPairGenerator.getInstance("EC").apply { + initialize(bits) + }.genKeyPair() + val algo = when (bits) { + 256 -> JwsAlgorithm.ES256 + 384 -> JwsAlgorithm.ES384 + 521 -> JwsAlgorithm.ES512 + else -> JwsAlgorithm.NON_JWS_SHA1_WITH_RSA.also { throw IllegalArgumentException("Unknown EC Curve size") } // necessary(compiler), but redundant else-branch + } + Pair(keyPair, algo) + } + withData(publicKeyDesc) { (keyPair, algorithm) -> + cryptoService = DefaultCryptoService(keyPair, algorithm) + jwsService = DefaultJwsService(cryptoService) + verifierJwsService = DefaultVerifierJwsService() + randomPayload = uuid4().toString() + + withData("signed object from ext. library can be verified: $algorithm", Pair(keyPair, algorithm)) { + val stringPayload = jsonSerializer.encodeToString(randomPayload) +// if (algorithm == JwsAlgorithm.ES512){ +// print(stringPayload) +// } + val libHeader = JWSHeader.Builder(JWSAlgorithm(algorithm.name)).type(JOSEObjectType("JWT")) + .keyID(cryptoService.jsonWebKey.keyId).build() + val libObject = JWSObject(libHeader, Payload(stringPayload)).also { + it.sign(ECDSASigner(keyPair.private as ECPrivateKey)) + } + libObject.verify(ECDSAVerifier(keyPair.public as ECPublicKey)) shouldBe true + + // Parsing to our structure verifying payload + val signedLibObject = libObject.serialize() + val parsedJwsSigned = JwsSigned.parse(signedLibObject) + parsedJwsSigned.shouldNotBeNull() + parsedJwsSigned.payload.decodeToString() shouldBe stringPayload + val parsedSig = parsedJwsSigned.signature.serialize() + // verifying external JWT with our service + val result = verifierJwsService.verifyJwsObject(parsedJwsSigned) + result shouldBe true + } + + withData("signed object can be verified with ext. library: $algorithm", Pair(keyPair, algorithm)) { + val stringPayload = jsonSerializer.encodeToString(randomPayload) + val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, stringPayload.encodeToByteArray()) + + verifierJwsService.verifyJwsObject(signed!!) shouldBe true + val parsed = JWSObject.parse(signed.serialize()) + parsed.shouldNotBeNull() + parsed.payload.toString() shouldBe stringPayload + val result = parsed.verify(ECDSAVerifier(keyPair.public as ECPublicKey)) + result shouldBe true + } + + withData("encrypted object from ext. library can be decrypted: $algorithm", Pair(keyPair, algorithm)) { + val stringPayload = jsonSerializer.encodeToString(randomPayload) + val libJweHeader = JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM) + .type(JOSEObjectType(JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON)) + .contentType(JwsContentTypeConstants.DIDCOMM_PLAIN_JSON).keyID(cryptoService.jsonWebKey.keyId) + .build() + val libJweObject = JWEObject(libJweHeader, Payload(stringPayload)).also { + it.encrypt(ECDHEncrypter(keyPair.public as ECPublicKey)) + } + val encryptedJwe = libJweObject.serialize() + + val parsedJwe = JweEncrypted.parse(encryptedJwe) + parsedJwe.shouldNotBeNull() + + jwsService.decryptJweObject( + parsedJwe, encryptedJwe + )?.payload?.decodeToString() shouldBe stringPayload + } + + withData("encrypted object can be decrypted with ext. library: $algorithm", Pair(keyPair, algorithm)) { + val stringPayload = jsonSerializer.encodeToString(randomPayload) + val encrypted = jwsService.encryptJweObject( + JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, + stringPayload.encodeToByteArray(), + cryptoService.jsonWebKey, + JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, + JweAlgorithm.ECDH_ES, + JweEncryption.A256GCM, + ) + + val parsed = JWEObject.parse(encrypted) + parsed.shouldNotBeNull() + parsed.payload.shouldBeNull() + + parsed.decrypt(ECDHDecrypter(keyPair.private as ECPrivateKey)) + parsed.payload.toString() shouldBe stringPayload + } + } } - val signed = libObject.serialize() - - val parsed = JwsSigned.parse(signed) - parsed.shouldNotBeNull() - parsed.payload.decodeToString() shouldBe stringPayload - - val result = verifierJwsService.verifyJwsObject(parsed, signed) - result shouldBe true } - - "signed object can be verified with ext. library" { - val stringPayload = jsonSerializer.encodeToString(randomPayload) - val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, stringPayload.encodeToByteArray()) - - val parsed = JWSObject.parse(signed) - parsed.shouldNotBeNull() - parsed.payload.toString() shouldBe stringPayload - - val result = parsed.verify(ECDSAVerifier(keyPair.public as ECPublicKey)) - result shouldBe true - } - - "encrypted object from ext. library can be decrypted" { - val stringPayload = jsonSerializer.encodeToString(randomPayload) - val libHeader = JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM) - .type(JOSEObjectType(JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON)) - .contentType(JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) - .keyID(cryptoService.jsonWebKey.keyId) - .build() - val libObject = JWEObject(libHeader, Payload(stringPayload)).also { - it.encrypt(ECDHEncrypter(keyPair.public as ECPublicKey)) - } - val encrypted = libObject.serialize() - - val parsed = JweEncrypted.parse(encrypted) - parsed.shouldNotBeNull() - - val result = jwsService.decryptJweObject(parsed, encrypted) - result?.payload?.decodeToString() shouldBe stringPayload - } - - "encrypted object can be decrypted with ext. library" { - val stringPayload = jsonSerializer.encodeToString(randomPayload) - val encrypted = jwsService.encryptJweObject( - JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, - stringPayload.encodeToByteArray(), - cryptoService.jsonWebKey, - JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, - JweAlgorithm.ECDH_ES, - JweEncryption.A256GCM, - ) - - val parsed = JWEObject.parse(encrypted) - parsed.shouldNotBeNull() - parsed.payload.shouldBeNull() - - parsed.decrypt(ECDHDecrypter(keyPair.private as ECPrivateKey)) - parsed.payload.toString() shouldBe stringPayload - } - })