From f46b6b10ed930b7d817bde243b19a7904abe0032 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 1 Sep 2023 10:37:23 +0200 Subject: [PATCH 01/14] CI: Run and report tests on push --- .github/workflows/build-ios.yml | 2 +- .github/workflows/build-jvm.yml | 2 +- .github/workflows/test-ios.yml | 25 +++++++++++++++++++++++++ .github/workflows/test-jvm.yml | 23 +++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test-ios.yml create mode 100644 .github/workflows/test-jvm.yml diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 81fcc8f17..a2e7d602f 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -1,5 +1,5 @@ name: Build iOS Framework -on: [push] +on: workflow_dispatch jobs: build: runs-on: macos-latest diff --git a/.github/workflows/build-jvm.yml b/.github/workflows/build-jvm.yml index 038b24382..301980fa9 100644 --- a/.github/workflows/build-jvm.yml +++ b/.github/workflows/build-jvm.yml @@ -1,5 +1,5 @@ name: Build JVM artifacts -on: [push] +on: workflow_dispatch jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml new file mode 100644 index 000000000..afab3a0b1 --- /dev/null +++ b/.github/workflows/test-ios.yml @@ -0,0 +1,25 @@ +name: Test iOS implementation +on: [push] +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: Build klibs + run: ./gradlew iosArm64MainKlibrary iosX64MainKlibrary + - name: Run tests + run: ./gradlew iosX64Test + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() + with: + name: vclib Tests + path: vclib/build/test-results/**/TEST*.xml,vclib-aries/build/test-results/**/TEST*.xml,vclib-openid/build/test-results/**/TEST*.xml + reporter: java-junit diff --git a/.github/workflows/test-jvm.yml b/.github/workflows/test-jvm.yml new file mode 100644 index 000000000..15ea93fc0 --- /dev/null +++ b/.github/workflows/test-jvm.yml @@ -0,0 +1,23 @@ +name: Test JVM implementation +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + - name: Run tests + run: ./gradlew jvmTest + - name: Test Report + uses: dorny/test-reporter@v1 + if: success() || failure() + with: + name: vclib Tests + path: vclib/build/test-results/**/TEST*.xml,vclib-aries/build/test-results/**/TEST*.xml,vclib-openid/build/test-results/**/TEST*.xml + reporter: java-junit From b9ac57b9772992d67934955254a0d80316e952ef Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 1 Sep 2023 10:55:44 +0200 Subject: [PATCH 02/14] Fix test cases --- .../wallet/lib/oidc/OidcSiopProtocolTest.kt | 23 +------ .../wallet/lib/oidc/TestCredentialScheme.kt | 64 ------------------- 2 files changed, 3 insertions(+), 84 deletions(-) delete mode 100644 vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/TestCredentialScheme.kt diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt index fa51bcd96..c0073e1f6 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt @@ -2,6 +2,7 @@ package at.asitplus.wallet.lib.oidc import at.asitplus.wallet.lib.LibraryInitializer import at.asitplus.wallet.lib.agent.* +import at.asitplus.wallet.lib.data.AtomicAttribute2023 import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.CredentialSubject import at.asitplus.wallet.lib.oidvci.decodeFromPostBody @@ -34,15 +35,6 @@ class OidcSiopProtocolTest : FreeSpec({ lateinit var holderSiop: OidcSiopWallet lateinit var verifierSiop: OidcSiopVerifier - LibraryInitializer.registerExtensionLibrary(LibraryInitializer.ExtensionLibraryInfo( - credentialScheme = TestCredentialScheme, - serializersModule = kotlinx.serialization.modules.SerializersModule { - polymorphic(CredentialSubject::class) { - subclass(TestCredential::class) - } - } - )) - beforeEach { holderCryptoService = DefaultCryptoService() verifierCryptoService = DefaultCryptoService() @@ -165,16 +157,7 @@ class OidcSiopProtocolTest : FreeSpec({ verifier = verifierAgent, cryptoService = verifierCryptoService, relyingPartyUrl = relyingPartyUrl, - credentialScheme = TestCredentialScheme - ) - holderAgent.storeCredentials( - IssuerAgent.newDefaultInstance( - DefaultCryptoService(), - dataProvider = TestCredentialDataProvider(), - ).issueCredentialWithTypes( - holderAgent.identifier, - attributeTypes = listOf(TestCredentialScheme.vcType) - ).toStoreCredentialInput() + credentialScheme = ConstantIndex.AtomicAttribute2023 ) val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl) @@ -188,7 +171,7 @@ class OidcSiopProtocolTest : FreeSpec({ result.shouldBeInstanceOf() result.vp.verifiableCredentials.shouldNotBeEmpty() result.vp.verifiableCredentials.forEach { - it.vc.credentialSubject.shouldBeInstanceOf() + it.vc.credentialSubject.shouldBeInstanceOf() } } }) \ No newline at end of file diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/TestCredentialScheme.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/TestCredentialScheme.kt deleted file mode 100644 index fa945ce52..000000000 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/TestCredentialScheme.kt +++ /dev/null @@ -1,64 +0,0 @@ -package at.asitplus.wallet.lib.oidc - -import at.asitplus.KmmResult -import at.asitplus.wallet.lib.agent.CredentialToBeIssued -import at.asitplus.wallet.lib.agent.IssuerCredentialDataProvider -import at.asitplus.wallet.lib.cbor.CoseKey -import at.asitplus.wallet.lib.data.ConstantIndex -import at.asitplus.wallet.lib.data.CredentialSubject -import io.matthewnelson.component.encoding.base16.encodeBase16 -import kotlinx.datetime.Clock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlin.random.Random -import kotlin.time.Duration.Companion.minutes - -object TestCredentialScheme : ConstantIndex.CredentialScheme { - override val credentialDefinitionName: String = "test-credential" - override val schemaUri: String = "https://example.com/schema/testcredential/1.0.0" - override val vcType: String = "TestCredential" - override val credentialFormat: ConstantIndex.CredentialFormat = ConstantIndex.CredentialFormat.W3C_VC -} - -@Serializable -@SerialName("TestCredential") -data class TestCredential ( - override val id: String, - - @SerialName("name") - val name: String, - - @SerialName("value") - val value: String -) : CredentialSubject() - -class TestCredentialDataProvider( - private val clock: Clock = Clock.System, -) : IssuerCredentialDataProvider { - - private val defaultLifetime = 1.minutes - - override fun getCredentialWithType( - subjectId: String, - subjectPublicKey: CoseKey?, - attributeTypes: Collection - ): KmmResult> { - val attributeType = TestCredentialScheme.vcType - if (!attributeTypes.contains(attributeType)) { - return KmmResult.failure(UnsupportedOperationException("no data")) - } - val expiration = clock.now() + defaultLifetime - return KmmResult.success( - listOf( - CredentialToBeIssued.Vc( - TestCredential(subjectId, randomValue(), randomValue()), - expiration, - attributeType, - ), - ) - ) - } - - private fun randomValue() = Random.nextBytes(32).encodeBase16() - -} From e24420e2be7f192045e2aa932635a9621689d571 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 1 Sep 2023 11:25:43 +0200 Subject: [PATCH 03/14] Use strict base16 encoding without linebreaks --- .../asitplus/wallet/lib/msg/JwmAttachment.kt | 3 -- .../lib/aries/DummyCredentialDataProvider.kt | 5 +- .../wallet/lib/oidc/OidcSiopWallet.kt | 3 +- .../lib/oidc/DummyCredentialDataProvider.kt | 5 +- .../agent/InMemoryIssuerCredentialStore.kt | 3 +- .../asitplus/wallet/lib/agent/IssuerAgent.kt | 3 -- .../at/asitplus/wallet/lib/agent/Validator.kt | 34 +++++++------- .../wallet/lib/agent/VerifierAgent.kt | 3 +- .../at/asitplus/wallet/lib/cbor/CoseHeader.kt | 10 ++-- .../at/asitplus/wallet/lib/cbor/CoseKey.kt | 15 ++---- .../at/asitplus/wallet/lib/cbor/CoseSigned.kt | 8 ++-- .../asitplus/wallet/lib/iso/DeviceRequest.kt | 5 +- .../wallet/lib/iso/MobileDrivingLicence.kt | 5 +- .../wallet/lib/iso/MobileSecurityObject.kt | 3 +- .../lib/jws/ByteArrayBase64Serializer.kt | 2 - .../lib/jws/ByteArrayBase64UrlSerializer.kt | 1 - .../at/asitplus/wallet/lib/jws/JwsSigned.kt | 1 - .../wallet/lib/jws/MultibaseHelper.kt | 2 - .../lib/agent/DummyCredentialDataProvider.kt | 5 +- .../wallet/lib/cbor/CoseSerializationTest.kt | 11 +++-- .../wallet/lib/cbor/CoseServiceTest.kt | 7 +-- .../wallet/lib/iso/CborSerializationTest.kt | 47 ++++++++++--------- .../at/asitplus/wallet/lib/iso/IsoMdocTest.kt | 7 +-- 23 files changed, 84 insertions(+), 104 deletions(-) diff --git a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt index d524ae231..951fbeec8 100644 --- a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt +++ b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt @@ -3,14 +3,11 @@ package at.asitplus.wallet.lib.msg import at.asitplus.wallet.lib.aries.jsonSerializer import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier -import io.matthewnelson.component.base64.decodeBase64ToArray -import io.matthewnelson.component.base64.encodeBase64 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString /** diff --git a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt index e588a6cbd..c020cc5eb 100644 --- a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt +++ b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt @@ -7,7 +7,8 @@ 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 io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlin.random.Random import kotlin.time.Duration.Companion.minutes @@ -60,7 +61,7 @@ class DummyCredentialDataProvider( ) } - private fun randomValue() = Random.nextBytes(32).encodeBase16() + private fun randomValue() = Random.nextBytes(32).encodeToString(Base16(strict = true)) } 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 040b304e1..56bc4a564 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 @@ -34,7 +34,6 @@ import io.github.aakira.napier.Napier import io.ktor.http.URLBuilder import io.ktor.http.Url import io.ktor.util.flattenEntries -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock @@ -292,7 +291,7 @@ class OidcSiopWallet( AuthenticationResponseParameters( idToken = signedIdToken, state = params.state, - vpToken = vp.document.serialize().encodeToString(Base16()), + vpToken = vp.document.serialize().encodeToString(Base16(strict = true)), presentationSubmission = presentationSubmission, ) ) 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 492645090..99dde4d73 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 @@ -11,7 +11,8 @@ import at.asitplus.wallet.lib.iso.DrivingPrivilege import at.asitplus.wallet.lib.iso.ElementValue import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DataElements import at.asitplus.wallet.lib.iso.IssuerSignedItem -import io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlin.random.Random @@ -91,7 +92,7 @@ class DummyCredentialDataProvider( return KmmResult.success(listOfAttributes) } - private fun randomValue() = Random.nextBytes(32).encodeBase16() + private fun randomValue() = Random.nextBytes(32).encodeToString(Base16(strict = true)) fun buildIssuerSignedItem(elementIdentifier: String, elementValue: String, digestId: UInt) = IssuerSignedItem( digestId = digestId, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt index 5dd33d2ca..cb7bfc08e 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt @@ -3,7 +3,6 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.CredentialSubject import at.asitplus.wallet.lib.iso.IssuerSignedItem import at.asitplus.wallet.lib.iso.sha256 -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Instant @@ -47,7 +46,7 @@ class InMemoryIssuerCredentialStore : IssuerCredentialStore { val list = map.getOrPut(timePeriod) { mutableListOf() } val newIndex = (list.maxOfOrNull { it.statusListIndex } ?: 0) + 1 list += Credential( - vcId = issuerSignedItemList.toString().encodeToByteArray().sha256().encodeToString(Base16()), + vcId = issuerSignedItemList.toString().encodeToByteArray().sha256().encodeToString(Base16(strict = true)), statusListIndex = newIndex, revoked = false, expirationDate = expirationDate 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 26d026c4f..993abe3cb 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 @@ -28,12 +28,9 @@ import at.asitplus.wallet.lib.jws.JwsContentTypeConstants import at.asitplus.wallet.lib.jws.JwsService import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier -import io.matthewnelson.component.base64.encodeBase64 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.plus import kotlin.time.Duration import kotlin.time.Duration.Companion.hours 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 04a0742ee..419d65e40 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 @@ -24,8 +24,6 @@ import at.asitplus.wallet.lib.jws.JwsSigned import at.asitplus.wallet.lib.jws.VerifierJwsService import at.asitplus.wallet.lib.toBitSet import io.github.aakira.napier.Napier -import io.matthewnelson.component.base64.decodeBase64ToArray -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull @@ -190,10 +188,10 @@ class Validator( */ fun verifyDocument(doc: Document, challenge: String): Verifier.VerifyPresentationResult { if (doc.docType != DOC_TYPE_MDL) - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("Invalid docType: ${doc.docType}") } if (doc.errors != null) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("Document has errors: ${doc.errors}") } } val issuerSigned = doc.issuerSigned @@ -201,45 +199,45 @@ class Validator( val issuerKey = issuerAuth.unprotectedHeader?.certificateChain?.let { CryptoUtils.extractPublicKeyFromX509Cert(it)?.toCoseKey() - } ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + } ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("Got no issuer key in $issuerAuth") } if (verifierCoseService.verifyCose(issuerAuth, issuerKey).getOrNull() != true) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("IssuerAuth not verified: $issuerAuth") } } val mso = issuerSigned.getIssuerAuthPayloadAsMso() - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) - .also { Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16())}") } + ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + .also { Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16(strict = true))}") } if (mso.docType != DOC_TYPE_MDL) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("Invalid docType in MSO: ${mso.docType}") } } val mdlItems = mso.valueDigests[NAMESPACE_MDL] - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("mdlItems are null in MSO: ${mso.valueDigests}") } val walletKey = mso.deviceKeyInfo.deviceKey val deviceSignature = doc.deviceSigned.deviceAuth.deviceSignature - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("DeviceSignature is null: ${doc.deviceSigned.deviceAuth}") } if (verifierCoseService.verifyCose(deviceSignature, walletKey).getOrNull() != true) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("DeviceSignature not verified") } } val deviceSignaturePayload = deviceSignature.payload - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("DeviceSignature does not contain challenge") } if (!deviceSignaturePayload.contentEquals(challenge.encodeToByteArray())) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("DeviceSignature does not contain correct challenge") } } val issuerSignedItems = issuerSigned.namespaces?.get(NAMESPACE_MDL) - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16())) + ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) .also { Napier.w("No issuer signed items in ${issuerSigned.namespaces}") } val validatedItems = issuerSignedItems.entries.associateWith { it.verify(mdlItems) } @@ -255,7 +253,7 @@ class Validator( val issuerHash = mdlItems.entries.first { it.key == value.digestId } // TODO analyze usages of tag wrapping val verifierHash = serialized.wrapInCborTag(24).sha256() - if (!verifierHash.encodeToString(Base16()).contentEquals(issuerHash.value.encodeToString(Base16()))) { + if (!verifierHash.encodeToString(Base16(strict = true)).contentEquals(issuerHash.value.encodeToString(Base16(strict = true)))) { Napier.w("Could not verify hash of value for ${value.elementIdentifier}") return false } @@ -307,12 +305,12 @@ class Validator( Napier.d("Verifying ISO Cred $it") if (issuerKey == null) { Napier.w("ISO: No issuer key") - return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16())) + return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16(strict = true))) } val result = verifierCoseService.verifyCose(it.issuerAuth, issuerKey) if (result.getOrNull() != true) { Napier.w("ISO: Could not verify credential", result.exceptionOrNull()) - return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16())) + return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16(strict = true))) } return Verifier.VerifyCredentialResult.SuccessIso(it) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt index ea5ed0f46..19aa04218 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt @@ -5,7 +5,6 @@ import at.asitplus.wallet.lib.data.VerifiablePresentationParsed import at.asitplus.wallet.lib.iso.Document import at.asitplus.wallet.lib.jws.JwsSigned import io.github.aakira.napier.Napier -import io.matthewnelson.component.encoding.base16.decodeBase16ToArray import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull @@ -58,7 +57,7 @@ class VerifierAgent private constructor( return validator.verifyVpJws(it, challenge, identifier) } val document = - runCatching { it.decodeToByteArrayOrNull(Base16())?.let { bytes -> Document.deserialize(bytes) } }.getOrNull() + runCatching { it.decodeToByteArrayOrNull(Base16(strict = true))?.let { bytes -> Document.deserialize(bytes) } }.getOrNull() if (document != null) { return validator.verifyDocument(document, challenge) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseHeader.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseHeader.kt index a05528322..850744677 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseHeader.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseHeader.kt @@ -2,8 +2,6 @@ package at.asitplus.wallet.lib.cbor import at.asitplus.wallet.lib.iso.cborSerializer import io.github.aakira.napier.Napier -import io.ktor.http.content.ByteArrayContent -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.ExperimentalSerializationApi @@ -95,10 +93,10 @@ data class CoseHeader( return "CoseHeader(algorithm=$algorithm," + " criticalHeaders=$criticalHeaders," + " contentType=$contentType," + - " kid=${kid?.encodeToString(Base16())}," + - " iv=${iv?.encodeToString(Base16())}," + - " partialIv=${partialIv?.encodeToString(Base16())}," + - " certificateChain=${certificateChain?.encodeToString(Base16())})" + " kid=${kid?.encodeToString(Base16(strict = true))}," + + " iv=${iv?.encodeToString(Base16(strict = true))}," + + " partialIv=${partialIv?.encodeToString(Base16(strict = true))}," + + " certificateChain=${certificateChain?.encodeToString(Base16(strict = true))})" } companion object { 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 72063ba3e..3e448c219 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 @@ -3,13 +3,8 @@ 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 -import at.asitplus.wallet.lib.jws.JwkType import at.asitplus.wallet.lib.jws.MultibaseHelper import io.github.aakira.napier.Napier -import io.matthewnelson.component.base64.encodeBase64 -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.ExperimentalSerializationApi @@ -115,14 +110,14 @@ data class CoseKey( override fun toString(): String { return "CoseKey(type=$type," + - " keyId=${keyId?.encodeToString(Base16())}," + + " keyId=${keyId?.encodeToString(Base16(strict = true))}," + " algorithm=$algorithm," + " operations=${operations?.contentToString()}," + - " baseIv=${baseIv?.encodeToString(Base16())}," + + " baseIv=${baseIv?.encodeToString(Base16(strict = true))}," + " curve=$curve," + - " x=${x?.encodeToString(Base16())}," + - " y=${y?.encodeToString(Base16())}," + - " d=${d?.encodeToString(Base16())})" + " x=${x?.encodeToString(Base16(strict = true))}," + + " y=${y?.encodeToString(Base16(strict = true))}," + + " d=${d?.encodeToString(Base16(strict = true))})" } override fun equals(other: Any?): Boolean { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt index 45992ad78..847c9067d 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/cbor/CoseSigned.kt @@ -62,8 +62,8 @@ data class CoseSigned( override fun toString(): String { return "CoseSigned(protectedHeader=${protectedHeader.value}," + " unprotectedHeader=$unprotectedHeader," + - " payload=${payload?.encodeToString(Base16())}," + - " signature=${signature.encodeToString(Base16())})" + " payload=${payload?.encodeToString(Base16(strict = true))}," + + " signature=${signature.encodeToString(Base16(strict = true))})" } companion object { @@ -119,8 +119,8 @@ data class CoseSignatureInput( override fun toString(): String { return "CoseSignatureInput(contextString='$contextString'," + " protectedHeader=${protectedHeader.value}," + - " externalAad=${externalAad.encodeToString(Base16())}," + - " payload=${payload?.encodeToString(Base16())})" + " externalAad=${externalAad.encodeToString(Base16(strict = true))}," + + " payload=${payload?.encodeToString(Base16(strict = true))})" } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceRequest.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceRequest.kt index 7c3f9e34b..233f85d34 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceRequest.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/DeviceRequest.kt @@ -5,7 +5,6 @@ package at.asitplus.wallet.lib.iso import at.asitplus.wallet.lib.cbor.CoseSigned import at.asitplus.wallet.lib.iso.IsoDataModelConstants.NAMESPACE_MDL import io.github.aakira.napier.Napier -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.LocalDate @@ -409,7 +408,7 @@ data class IssuerSignedItem( override fun toString(): String { return "IssuerSignedItem(digestId=$digestId," + - " random=${random.encodeToString(Base16())}," + + " random=${random.encodeToString(Base16(strict = true))}," + " elementIdentifier='$elementIdentifier'," + " elementValue=$elementValue)" } @@ -439,7 +438,7 @@ data class ElementValue( fun serialize() = cborSerializer.encodeToByteArray(this) override fun toString(): String { - return "ElementValue(bytes=${bytes?.encodeToString(Base16())}," + + return "ElementValue(bytes=${bytes?.encodeToString(Base16(strict = true))}," + " date=${date}," + " string=$string," + " drivingPrivilege=$drivingPrivilege)" diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileDrivingLicence.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileDrivingLicence.kt index 59bc1111f..191b59c90 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileDrivingLicence.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileDrivingLicence.kt @@ -36,7 +36,6 @@ import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DataElements.UN_DISTINGU import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DataElements.WEIGHT import at.asitplus.wallet.lib.jws.ByteArrayBase64UrlSerializer import io.github.aakira.napier.Napier -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.LocalDate @@ -213,7 +212,7 @@ data class MobileDrivingLicence( " issuingCountry='$issuingCountry'," + " issuingAuthority='$issuingAuthority'," + " licenceNumber='$licenceNumber'," + - " portrait=${portrait.encodeToString(Base16())}," + + " portrait=${portrait.encodeToString(Base16(strict = true))}," + " drivingPrivileges=${drivingPrivileges}," + " unDistinguishingSign='$unDistinguishingSign'," + " administrativeNumber=$administrativeNumber," + @@ -236,7 +235,7 @@ data class MobileDrivingLicence( " residentCountry=$residentCountry," + " familyNameNationalCharacters=$familyNameNationalCharacters," + " givenNameNationalCharacters=$givenNameNationalCharacters," + - " signatureOrUsualMark=${signatureOrUsualMark?.encodeToString(Base16())})" + " signatureOrUsualMark=${signatureOrUsualMark?.encodeToString(Base16(strict = true))})" } companion object { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileSecurityObject.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileSecurityObject.kt index a68dcdee6..0ec4fa09a 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileSecurityObject.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/iso/MobileSecurityObject.kt @@ -4,7 +4,6 @@ package at.asitplus.wallet.lib.iso import at.asitplus.wallet.lib.cbor.CoseKey import io.github.aakira.napier.Napier -import io.matthewnelson.component.encoding.base16.encodeBase16 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Instant @@ -106,7 +105,7 @@ data class ValueDigest( } override fun toString(): String { - return "MobileSecurityObjectNamespaceEntry(key=$key, value=${value.encodeToString(Base16())})" + return "MobileSecurityObjectNamespaceEntry(key=$key, value=${value.encodeToString(Base16(strict = true))})" } companion object { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt index d247bd32e..0f0f9a1d5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt @@ -1,7 +1,5 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.component.base64.decodeBase64ToArray -import io.matthewnelson.component.base64.encodeBase64 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt index 7510394ac..211b166bd 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt @@ -1,6 +1,5 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.component.base64.decodeBase64ToArray import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt index 8d89787a1..1e2517b67 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt @@ -1,7 +1,6 @@ package at.asitplus.wallet.lib.jws import io.github.aakira.napier.Napier -import io.matthewnelson.component.base64.decodeBase64ToArray import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt index 5793f52c8..9cd3e466c 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt @@ -1,8 +1,6 @@ package at.asitplus.wallet.lib.jws import at.asitplus.wallet.lib.cbor.CoseEllipticCurve -import io.matthewnelson.component.base64.decodeBase64ToArray -import io.matthewnelson.component.base64.encodeBase64 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt index 1adb9dbca..7a8301b23 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt @@ -4,7 +4,8 @@ import at.asitplus.KmmResult import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.data.AtomicAttribute2023 import at.asitplus.wallet.lib.data.ConstantIndex -import io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlin.random.Random import kotlin.time.Duration.Companion.minutes @@ -57,6 +58,6 @@ class DummyCredentialDataProvider( ) } - private fun randomValue() = Random.nextBytes(32).encodeBase16() + private fun randomValue() = Random.nextBytes(32).encodeToString(Base16(strict = true)) } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseSerializationTest.kt index e131138e5..9cde4832f 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/cbor/CoseSerializationTest.kt @@ -6,8 +6,9 @@ import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain -import io.matthewnelson.component.encoding.base16.decodeBase16ToArray -import io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.cbor.ByteStringWrapper class CoseSerializationTest : FreeSpec({ @@ -21,7 +22,7 @@ class CoseSerializationTest : FreeSpec({ payload = "This is the content.".encodeToByteArray(), signature = "bar".encodeToByteArray() ) - val serialized = cose.serialize().encodeBase16().uppercase() + val serialized = cose.serialize().encodeToString(Base16(strict = true)).uppercase() serialized shouldContain "546869732069732074686520636F6E74656E742E" // "This is the content." serialized shouldContain "43A10126" @@ -29,7 +30,7 @@ class CoseSerializationTest : FreeSpec({ "Serialize header" { val header = CoseHeader(algorithm = CoseAlgorithm.ES256, kid = "11".encodeToByteArray()) - val serialized = header.serialize().encodeBase16().uppercase() + val serialized = header.serialize().encodeToString(Base16(strict = true)).uppercase() println(serialized) val deserialized = CoseHeader.deserialize(header.serialize()) @@ -43,7 +44,7 @@ class CoseSerializationTest : FreeSpec({ "742e58408eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a" + "91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b09" + "16e5a4c345cacb36" - val cose = CoseSigned.deserialize(input.uppercase().decodeBase16ToArray()!!) + val cose = CoseSigned.deserialize(input.uppercase().decodeToByteArray(Base16(strict = true))) println(cose) cose.shouldNotBeNull() 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 3a27d822c..1c82f164a 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 @@ -5,7 +5,8 @@ import at.asitplus.wallet.lib.agent.DefaultCryptoService import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.random.Random class CoseServiceTest : FreeSpec({ @@ -30,7 +31,7 @@ class CoseServiceTest : FreeSpec({ addKeyId = true ).getOrThrow() signed.shouldNotBeNull() - println(signed.serialize().encodeBase16()) + println(signed.serialize().encodeToString(Base16(strict = true))) signed.payload shouldBe randomPayload signed.signature.shouldNotBeNull() @@ -50,7 +51,7 @@ class CoseServiceTest : FreeSpec({ addKeyId = true ).getOrThrow() signed.shouldNotBeNull() - println(signed.serialize().encodeBase16()) + println(signed.serialize().encodeToString(Base16(strict = true))) signed.payload shouldBe null signed.signature.shouldNotBeNull() diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/CborSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/CborSerializationTest.kt index e286a848a..88e4659e0 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/CborSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/iso/CborSerializationTest.kt @@ -18,8 +18,9 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.ktor.utils.io.core.toByteArray -import io.matthewnelson.component.encoding.base16.decodeBase16ToArray -import io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.serialization.encodeToString @@ -51,7 +52,7 @@ class CborSerializationTest : FreeSpec({ unDistinguishingSign = "AT" ) - val serialized = mdl.serialize().encodeBase16().uppercase() + val serialized = mdl.serialize().encodeToString(Base16(strict = true)).uppercase() println(serialized) serialized shouldContain "76656869636C655F63617465676F72795F636F6465" // "vehicle_category_code" @@ -138,7 +139,7 @@ class CborSerializationTest : FreeSpec({ 9bb7f80bf """.trimIndent().replace("\n", "").uppercase() - val deviceRequest = DeviceRequest.deserialize(input.decodeBase16ToArray()!!) + val deviceRequest = DeviceRequest.deserialize(input.decodeToByteArray(Base16(strict = true))) deviceRequest.shouldNotBeNull() println(deviceRequest) @@ -158,7 +159,7 @@ class CborSerializationTest : FreeSpec({ docRequest.readerAuth.shouldNotBeNull() docRequest.readerAuth?.unprotectedHeader?.certificateChain?.shouldNotBeNull() - deviceRequest.serialize().encodeBase16().uppercase() shouldBe input + deviceRequest.serialize().encodeToString(Base16(strict = true)).uppercase() shouldBe input } // From ISO/IEC 18013-5:2021(E), D4.1.2, page 116 @@ -325,7 +326,7 @@ class CborSerializationTest : FreeSpec({ 806a07f8b5388a332d92c189a7bf293ee1f543405ae6824d6673746174757300 """.trimIndent().replace("\n", "").uppercase() - val deviceResponse = DeviceResponse.deserialize(input.decodeBase16ToArray()!!) + val deviceResponse = DeviceResponse.deserialize(input.decodeToByteArray(Base16(strict = true))) deviceResponse.shouldNotBeNull() println(deviceResponse) @@ -370,20 +371,20 @@ class CborSerializationTest : FreeSpec({ val valueDigestList = mso.valueDigests[NAMESPACE_MDL] valueDigestList.shouldNotBeNull() valueDigestList.findItem(0U) shouldBe "75167333B47B6C2BFB86ECCC1F438CF57AF055371AC55E1E359E20F254ADCEBF" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) valueDigestList.findItem(1U) shouldBe "67E539D6139EBD131AEF441B445645DD831B2B375B390CA5EF6279B205ED4571" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) val valueDigestListUs = mso.valueDigests["$NAMESPACE_MDL.US"] valueDigestListUs.shouldNotBeNull() valueDigestListUs.findItem(0U) shouldBe "D80B83D25173C484C5640610FF1A31C949C1D934BF4CF7F18D5223B15DD4F21C" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) valueDigestListUs.findItem(1U) shouldBe "4D80E1E2E4FB246D97895427CE7000BB59BB24C8CD003ECF94BF35BBD2917E34" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) document.deviceSigned.deviceAuth.deviceMac.shouldNotBeNull() // TODO "elementValue" in IssuerSignedItem needs a tag 1004u (0xD903EC) iff the value is a date - deviceResponse.serialize().encodeBase16() shouldBe input + deviceResponse.serialize().encodeToString(Base16(strict = true)) shouldBe input } "Driving Privilege" { @@ -393,7 +394,7 @@ class CborSerializationTest : FreeSpec({ expiryDate = LocalDate.parse("2024-10-20") ) - val serialized = drivingPrivilege.serialize().encodeBase16().uppercase() + val serialized = drivingPrivilege.serialize().encodeToString(Base16(strict = true)).uppercase() println(serialized) serialized shouldContain "76656869636C655F63617465676F72795F636F6465" // "vehicle_category_code" @@ -408,7 +409,7 @@ class CborSerializationTest : FreeSpec({ val input = "a37576656869636c655f63617465676f72795f636f646561416a69737375655f64617465d903ec6a323031382d30382d" + "30396b6578706972795f64617465d903ec6a323032342d31302d3230" - val deserialized = DrivingPrivilege.deserialize(input.uppercase().decodeBase16ToArray()!!) + val deserialized = DrivingPrivilege.deserialize(input.uppercase().decodeToByteArray(Base16(strict = true))) deserialized.shouldNotBeNull() deserialized.vehicleCategoryCode shouldBe "A" @@ -423,12 +424,12 @@ class CborSerializationTest : FreeSpec({ 6c656d656e744964656e7469666965726a69737375655f646174656c656c656d656e7456616c7565d903ec6a323031392d31302d3230 """.trimIndent().replace("\n", "").uppercase() - val deserialized = IssuerSignedItem.deserialize(input.decodeBase16ToArray()!!) + val deserialized = IssuerSignedItem.deserialize(input.decodeToByteArray(Base16(strict = true))) deserialized.shouldNotBeNull() val serialized = deserialized.serialize() // TODO "elementValue" in IssuerSignedItem needs a tag 1004u (0xD903EC) iff the value is a date - serialized.encodeBase16().uppercase() shouldBe input + serialized.encodeToString(Base16(strict = true)).uppercase() shouldBe input } "Driving Privilege in IssuerSignedItem from ISO example" { @@ -487,11 +488,11 @@ class CborSerializationTest : FreeSpec({ 03EC6A323031372D30322D32336B6578706972795F64617465D903EC6A323032342D31302D3230 """.trimIndent().replace("\n", "") - val deserialized = IssuerSignedItem.deserialize(input.decodeBase16ToArray()!!) + val deserialized = IssuerSignedItem.deserialize(input.decodeToByteArray(Base16(strict = true))) deserialized.shouldNotBeNull() val serialized = deserialized.serialize() - serialized.encodeBase16().uppercase() shouldBe input + serialized.encodeToString(Base16(strict = true)).uppercase() shouldBe input } // From ISO/IEC 18013-5:2021(E), page 130 @@ -604,7 +605,7 @@ class CborSerializationTest : FreeSpec({ 044b890ad85aa53f129134775d733754d7cb7a413766aeff13cb2e """.trimIndent().replace("\n", "").uppercase() - val coseSigned = CoseSigned.deserialize(input.decodeBase16ToArray()!!) + val coseSigned = CoseSigned.deserialize(input.decodeToByteArray(Base16(strict = true))) coseSigned.shouldNotBeNull() println(coseSigned) @@ -622,17 +623,17 @@ class CborSerializationTest : FreeSpec({ val valueDigestList = mso.valueDigests[NAMESPACE_MDL] valueDigestList.shouldNotBeNull() valueDigestList.findItem(0U) shouldBe "75167333B47B6C2BFB86ECCC1F438CF57AF055371AC55E1E359E20F254ADCEBF" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) valueDigestList.findItem(1U) shouldBe "67E539D6139EBD131AEF441B445645DD831B2B375B390CA5EF6279B205ED4571" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) val valueDigestListUs = mso.valueDigests["$NAMESPACE_MDL.US"] valueDigestListUs.shouldNotBeNull() valueDigestListUs.findItem(0U) shouldBe "D80B83D25173C484C5640610FF1A31C949C1D934BF4CF7F18D5223B15DD4F21C" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) valueDigestListUs.findItem(1U) shouldBe "4D80E1E2E4FB246D97895427CE7000BB59BB24C8CD003ECF94BF35BBD2917E34" - .decodeBase16ToArray() + .decodeToByteArray(Base16(strict = true)) - coseSigned.serialize().encodeBase16() shouldBe input + coseSigned.serialize().encodeToString(Base16(strict = true)) shouldBe input } }) 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 24c8c2c88..43d624a3d 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 @@ -22,7 +22,8 @@ import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.matthewnelson.component.encoding.base16.encodeBase16 +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.serialization.cbor.ByteStringWrapper @@ -277,8 +278,8 @@ class Verifier { val issuerHash = mdlItems.entries.first { it.key == issuerSignedItem.value.digestId } issuerHash.shouldNotBeNull() val verifierHash = issuerSignedItem.serialized.sha256() - verifierHash.encodeBase16() shouldBe issuerHash.value.encodeBase16() - println("Verifier got $key with value $elementValue and correct hash ${verifierHash.encodeBase16()}") + verifierHash.encodeToString(Base16(strict = true)) shouldBe issuerHash.value.encodeToString(Base16(strict = true)) + println("Verifier got $key with value $elementValue and correct hash ${verifierHash.encodeToString(Base16(strict = true))}") } } From 4607d6ea3c8d1544c2ccff0c1d5f6d575367b049 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 1 Sep 2023 11:32:23 +0200 Subject: [PATCH 04/14] Use static instances for Base64 url-safe encoding --- .../wallet/lib/oidvci/IssuerService.kt | 5 ++--- .../lib/jws/ByteArrayBase64UrlSerializer.kt | 4 ++-- .../at/asitplus/wallet/lib/jws/JweEncrypted.kt | 10 +++++----- .../at/asitplus/wallet/lib/jws/JwsService.kt | 9 ++++----- .../at/asitplus/wallet/lib/jws/JwsSigned.kt | 6 +----- .../wallet/lib/agent/ValidatorVcTest.kt | 9 +++++---- .../lib/jws/JwsHeaderSerializationTest.kt | 18 ++++++++++++------ .../wallet/lib/agent/DefaultCryptoService.kt | 7 ++++--- 8 files changed, 35 insertions(+), 33 deletions(-) diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt index 157c88e0a..9a8d966d7 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt @@ -22,7 +22,7 @@ import at.asitplus.wallet.lib.oidc.OpenIdConstants.TOKEN_PREFIX_BEARER import at.asitplus.wallet.lib.oidc.OpenIdConstants.TOKEN_TYPE_BEARER import at.asitplus.wallet.lib.oidc.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification -import io.ktor.http.* +import io.ktor.http.URLBuilder import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.coroutines.cancellation.CancellationException @@ -167,8 +167,7 @@ class IssuerService( return when (val issuedCredential = issuedCredentialResult.successful.first()) { is Issuer.IssuedCredential.Iso -> CredentialResponseParameters( format = CredentialFormatEnum.MSO_MDOC, - credential = issuedCredential.issuerSigned.serialize() - .encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false }) + credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64.UrlSafe) ) is Issuer.IssuedCredential.Vc -> CredentialResponseParameters( diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt index 211b166bd..a6a6e6de3 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt @@ -16,11 +16,11 @@ object ByteArrayBase64UrlSerializer : KSerializer { PrimitiveSerialDescriptor("ByteArrayBase64UrlSerializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - encoder.encodeString(value.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false })) + encoder.encodeString(value.encodeToString(Base64.UrlSafe)) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64()) ?: byteArrayOf() + return decoder.decodeString().decodeToByteArrayOrNull(Base64.UrlSafe) ?: byteArrayOf() } } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt index ea9ec61ad..e2db49d33 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt @@ -22,11 +22,11 @@ data class JweEncrypted( get() = JweHeader.deserialize(headerAsParsed.decodeToString()) fun serialize(): String { - return headerAsParsed.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false }) + - ".${encryptedKey?.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false }) ?: ""}" + - ".${iv.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false })}" + - ".${ciphertext.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false })}" + - ".${authTag.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false })}" + return headerAsParsed.encodeToString(Base64.UrlSafe) + + ".${encryptedKey?.encodeToString(Base64.UrlSafe) ?: ""}" + + ".${iv.encodeToString(Base64.UrlSafe)}" + + ".${ciphertext.encodeToString(Base64.UrlSafe)}" + + ".${authTag.encodeToString(Base64.UrlSafe)}" } override fun equals(other: Any?): Boolean { 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 54b006761..980a0b986 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 @@ -81,9 +81,8 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { ) { return null.also { Napier.w("Algorithm or keyId not matching to cryptoService") } } - val signatureInput = header.serialize().encodeToByteArray() - .encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false }) + - "." + payload.encodeToString(Base64 { encodeToUrlSafe = true; padEncoded = false }) + val signatureInput = header.serialize().encodeToByteArray().encodeToString(Base64.UrlSafe) + + "." + payload.encodeToString(Base64.UrlSafe) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = cryptoService.sign(signatureInputBytes).getOrElse { Napier.w("No signature from native code", it) @@ -131,7 +130,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { return null } val iv = jweObject.iv - val aad = jweObject.headerAsParsed.encodeToByteArray(Base64 { encodeToUrlSafe = true; padEncoded = false }) + val aad = jweObject.headerAsParsed.encodeToByteArray(Base64.UrlSafe) val ciphertext = jweObject.ciphertext val authTag = jweObject.authTag val plaintext = @@ -178,7 +177,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { val iv = Random.Default.nextBytes(jweEncryption.ivLengthBits / 8) val headerSerialized = jweHeader.serialize() val aad = headerSerialized.encodeToByteArray() - val aadForCipher = aad.encodeToByteArray(Base64 { encodeToUrlSafe = true; padEncoded = false }) + val aadForCipher = aad.encodeToByteArray(Base64.UrlSafe) val ciphertext = cryptoService.encrypt(key, iv, aadForCipher, payload, jweEncryption).getOrElse { Napier.w("No ciphertext from native code", it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt index 1e2517b67..bbf0879e0 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt @@ -15,11 +15,7 @@ data class JwsSigned( val plainSignatureInput: String, ) { fun serialize(): String { - return "${plainSignatureInput}.${ - signature.encodeToString(Base64 { - encodeToUrlSafe = true; padEncoded = false - }) - }" + return "${plainSignatureInput}.${signature.encodeToString(Base64.UrlSafe)}" } override fun equals(other: Any?): Boolean { 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 83b84b87a..7e0d7cf24 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 @@ -17,8 +17,8 @@ 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.component.base64.Base64 -import io.matthewnelson.component.base64.encodeBase64 +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.hours @@ -400,8 +400,9 @@ class ValidatorVcTest : FreeSpec() { type = JwsContentTypeConstants.JWT ) val jwsPayload = vcJws.serialize().encodeToByteArray() - val signatureInput = jwsHeader.serialize().encodeToByteArray().encodeBase64(Base64.UrlSafe()) + - "." + jwsPayload.encodeBase64(Base64.UrlSafe()) + val signatureInput = + jwsHeader.serialize().encodeToByteArray().encodeToString(Base64.UrlSafe) + + "." + jwsPayload.encodeToString(Base64.UrlSafe) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = issuerCryptoService.sign(signatureInputBytes) .getOrElse { return null } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt index 52b4e9a0b..4e0f691b2 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt @@ -1,13 +1,14 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.component.base64.Base64 -import io.matthewnelson.component.base64.encodeBase64 + import com.benasher44.uuid.uuid4 import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.random.Random class JwsHeaderSerializationTest : FreeSpec({ @@ -27,8 +28,8 @@ class JwsHeaderSerializationTest : FreeSpec({ val serialized = header.serialize() - serialized shouldContain """"${first.encodeBase64(Base64.Default)}"""" - serialized shouldContain """"${second.encodeBase64(Base64.Default)}"""" + serialized shouldContain """"${first.encodeToString(Base64())}"""" + serialized shouldContain """"${second.encodeToString(Base64())}"""" serialized shouldContain """"$kid"""" } @@ -39,8 +40,13 @@ class JwsHeaderSerializationTest : FreeSpec({ val kid = uuid4().toString() val type = JwsContentTypeConstants.JWT - val serialized = - """{"alg": "${algorithm.text}", "kid": "$kid", "typ": "$type", "x5c":["${first.encodeBase64()}","${second.encodeBase64()}"]}""" + val serialized = """{ + | "alg": "${algorithm.text}", + | "kid": "$kid", + | "typ": "$type", + | "x5c":["${first.encodeToString(Base64())}", + | "${second.encodeToString(Base64())}"]} + | """.trimMargin() val parsed = JwsHeader.deserialize(serialized) 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 84cdcc89f..f7bb41c8a 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 @@ -5,7 +5,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 io.matthewnelson.component.base64.encodeBase64 import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm @@ -13,6 +12,8 @@ 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.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer import kotlinx.cinterop.MemScope @@ -95,7 +96,7 @@ actual class DefaultCryptoService : CryptoService { return KmmResult.success( AuthenticatedCiphertext( input.reversedArray(), - "authtag-${key.encodeBase64()}".encodeToByteArray() + "authtag-${key.encodeToString(Base64())}".encodeToByteArray() ) ) } @@ -108,7 +109,7 @@ actual class DefaultCryptoService : CryptoService { authTag: ByteArray, algorithm: JweEncryption ): KmmResult { - return if (authTag.contentEquals("authtag-${key.encodeBase64()}".encodeToByteArray())) + return if (authTag.contentEquals("authtag-${key.encodeToString(Base64())}".encodeToByteArray())) KmmResult.success(input.reversedArray()) else KmmResult.failure(IllegalArgumentException()) From cca3af239c1361326eee8f36e62a479ca6f4e814 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 1 Sep 2023 11:49:49 +0200 Subject: [PATCH 05/14] Use correct Base64 encodings --- .../asitplus/wallet/lib/msg/JwmAttachment.kt | 10 ++++----- .../wallet/lib/oidvci/IssuerService.kt | 3 ++- .../asitplus/wallet/lib/agent/IssuerAgent.kt | 2 +- .../at/asitplus/wallet/lib/agent/Validator.kt | 2 +- .../at/asitplus/wallet/lib/data/Json.kt | 9 ++++++++ .../lib/jws/ByteArrayBase64Serializer.kt | 4 ++-- .../lib/jws/ByteArrayBase64UrlSerializer.kt | 6 +++--- .../at/asitplus/wallet/lib/jws/JsonWebKey.kt | 4 ++-- .../asitplus/wallet/lib/jws/JweEncrypted.kt | 21 ++++++++++--------- .../at/asitplus/wallet/lib/jws/JwsService.kt | 9 ++++---- .../at/asitplus/wallet/lib/jws/JwsSigned.kt | 9 ++++---- .../wallet/lib/jws/MultibaseHelper.kt | 4 ++-- .../wallet/lib/agent/ValidatorVcTest.kt | 5 +++-- .../lib/jws/JwsHeaderSerializationTest.kt | 8 +++---- .../wallet/lib/agent/DefaultCryptoService.kt | 4 ++-- 15 files changed, 57 insertions(+), 43 deletions(-) diff --git a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt index 951fbeec8..bb979190e 100644 --- a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt +++ b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt @@ -30,7 +30,7 @@ data class JwmAttachment( fun decodeString(): String? { if (data.base64 != null) - return data.base64.decodeToByteArrayOrNull(Base64())?.decodeToString() + return data.base64.decodeToByteArrayOrNull(Base64(strict = true))?.decodeToString() if (data.jws != null) return data.jws return null @@ -39,7 +39,7 @@ data class JwmAttachment( fun decodeBinary(): ByteArray? { if (data.base64 != null) - return data.base64.decodeToByteArrayOrNull(Base64()) + return data.base64.decodeToByteArrayOrNull(Base64(strict = true)) return null .also { Napier.w("Could not binary decode JWM attachment") } } @@ -57,7 +57,7 @@ data class JwmAttachment( id = uuid4().toString(), mediaType = "application/base64", data = JwmAttachmentData( - base64 = data.encodeToByteArray().encodeToString(Base64()) + base64 = data.encodeToByteArray().encodeToString(Base64(strict = true)) ) ) @@ -65,7 +65,7 @@ data class JwmAttachment( id = uuid4().toString(), mediaType = "application/base64", data = JwmAttachmentData( - base64 = data.encodeToString(Base64()) + base64 = data.encodeToString(Base64(strict = true)) ) ) @@ -75,7 +75,7 @@ data class JwmAttachment( filename = filename, parent = parent, data = JwmAttachmentData( - base64 = data.encodeToString(Base64()) + base64 = data.encodeToString(Base64(strict = true)) ) ) diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt index 9a8d966d7..481f02ed9 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt @@ -4,6 +4,7 @@ import at.asitplus.wallet.lib.agent.Issuer 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.data.Base64Url import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.VcDataModelConstants.VERIFIABLE_CREDENTIAL import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DOC_TYPE_MDL @@ -167,7 +168,7 @@ class IssuerService( return when (val issuedCredential = issuedCredentialResult.successful.first()) { is Issuer.IssuedCredential.Iso -> CredentialResponseParameters( format = CredentialFormatEnum.MSO_MDOC, - credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64.UrlSafe) + credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64Url) ) is Issuer.IssuedCredential.Vc -> CredentialResponseParameters( 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 993abe3cb..a29661ad9 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 @@ -218,7 +218,7 @@ class IssuerAgent( issuerCredentialStore.getRevokedStatusListIndexList(timePeriod) .forEach { bitset[it] = true } val input = bitset.toByteArray() - return zlibService.compress(input)?.encodeToString(Base64()) + return zlibService.compress(input)?.encodeToString(Base64(strict = true)) } /** 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 419d65e40..a28b329a4 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 @@ -88,7 +88,7 @@ class Validator( return false .also { Napier.d("credentialSubject invalid") } val encodedList = parsedVc.jws.vc.credentialSubject.encodedList - this.revocationList = encodedList.decodeToByteArrayOrNull(Base64())?.let { + this.revocationList = encodedList.decodeToByteArrayOrNull(Base64(strict = true))?.let { zlibService.decompress(it)?.toBitSet() ?: return false.also { Napier.d("Invalid ZLIB") } } ?: return false.also { Napier.d("Invalid Base64") } Napier.d("Revocation list is valid") diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt index d8b0fefbf..60f4e6e59 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt @@ -1,5 +1,7 @@ package at.asitplus.wallet.lib.data +import io.matthewnelson.encoding.base64.Base64 +import io.matthewnelson.encoding.base64.Base64ConfigBuilder import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic @@ -28,3 +30,10 @@ val jsonSerializer by lazy { } } } + +val Base64Url = Base64(config = Base64ConfigBuilder().apply { + lineBreakInterval = 0 + encodeToUrlSafe = true + isLenient = true + padEncoded = false +}.build()) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt index 0f0f9a1d5..5b0a8f17a 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt @@ -16,11 +16,11 @@ object ByteArrayBase64Serializer : KSerializer { PrimitiveSerialDescriptor("ByteArrayBase64Serializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - encoder.encodeString(value.encodeToString(Base64())) + encoder.encodeString(value.encodeToString(Base64(strict = true))) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64()) ?: byteArrayOf() + return decoder.decodeString().decodeToByteArrayOrNull(Base64(strict = true)) ?: byteArrayOf() } } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt index a6a6e6de3..3152ff2ab 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt @@ -1,6 +1,6 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.encoding.base64.Base64 +import at.asitplus.wallet.lib.data.Base64Url import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.KSerializer @@ -16,11 +16,11 @@ object ByteArrayBase64UrlSerializer : KSerializer { PrimitiveSerialDescriptor("ByteArrayBase64UrlSerializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - encoder.encodeString(value.encodeToString(Base64.UrlSafe)) + encoder.encodeString(value.encodeToString(Base64Url)) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64.UrlSafe) ?: byteArrayOf() + return decoder.decodeString().decodeToByteArrayOrNull(Base64Url) ?: byteArrayOf() } } \ 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 bdfb19b38..e91439b54 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 @@ -136,8 +136,8 @@ data class JsonWebKey( } override fun toString(): String { - return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId, x=${x?.encodeToString(Base64())}, y=${ - y?.encodeToString(Base64()) + return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId, x=${x?.encodeToString(Base64(strict = true))}, y=${ + y?.encodeToString(Base64(strict = true)) })" } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt index e2db49d33..2aa762a83 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.jws +import at.asitplus.wallet.lib.data.Base64Url import io.github.aakira.napier.Napier import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull @@ -22,11 +23,11 @@ data class JweEncrypted( get() = JweHeader.deserialize(headerAsParsed.decodeToString()) fun serialize(): String { - return headerAsParsed.encodeToString(Base64.UrlSafe) + - ".${encryptedKey?.encodeToString(Base64.UrlSafe) ?: ""}" + - ".${iv.encodeToString(Base64.UrlSafe)}" + - ".${ciphertext.encodeToString(Base64.UrlSafe)}" + - ".${authTag.encodeToString(Base64.UrlSafe)}" + return headerAsParsed.encodeToString(Base64Url) + + ".${encryptedKey?.encodeToString(Base64Url) ?: ""}" + + ".${iv.encodeToString(Base64Url)}" + + ".${ciphertext.encodeToString(Base64Url)}" + + ".${authTag.encodeToString(Base64Url)}" } override fun equals(other: Any?): Boolean { @@ -61,14 +62,14 @@ data class JweEncrypted( fun parse(it: String): JweEncrypted? { val stringList = it.replace("[^A-Za-z0-9-_.]".toRegex(), "").split(".") if (stringList.size != 5) return null.also { Napier.w("Could not parse JWE: $it") } - val headerAsParsed = stringList[0].decodeToByteArrayOrNull(Base64()) + val headerAsParsed = stringList[0].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val encryptedKey = stringList[1].decodeToByteArrayOrNull(Base64()) - val iv = stringList[2].decodeToByteArrayOrNull(Base64()) + val encryptedKey = stringList[1].decodeToByteArrayOrNull(Base64(strict = true)) + val iv = stringList[2].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64()) + val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val authTag = stringList[4].decodeToByteArrayOrNull(Base64()) + val authTag = stringList[4].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWE: $it") } return JweEncrypted(headerAsParsed, encryptedKey, iv, ciphertext, authTag) } 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 980a0b986..28e1d18a8 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 @@ -4,6 +4,7 @@ import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.DefaultVerifierCryptoService import at.asitplus.wallet.lib.agent.Digest import at.asitplus.wallet.lib.agent.VerifierCryptoService +import at.asitplus.wallet.lib.data.Base64Url import at.asitplus.wallet.lib.jws.JwsExtensions.encodeToByteArray import at.asitplus.wallet.lib.jws.JwsExtensions.encodeWithLength import at.asitplus.wallet.lib.jws.JwsExtensions.extractSignatureValues @@ -81,8 +82,8 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { ) { return null.also { Napier.w("Algorithm or keyId not matching to cryptoService") } } - val signatureInput = header.serialize().encodeToByteArray().encodeToString(Base64.UrlSafe) + - "." + payload.encodeToString(Base64.UrlSafe) + val signatureInput = header.serialize().encodeToByteArray().encodeToString(Base64Url) + + "." + payload.encodeToString(Base64Url) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = cryptoService.sign(signatureInputBytes).getOrElse { Napier.w("No signature from native code", it) @@ -130,7 +131,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { return null } val iv = jweObject.iv - val aad = jweObject.headerAsParsed.encodeToByteArray(Base64.UrlSafe) + val aad = jweObject.headerAsParsed.encodeToByteArray(Base64Url) val ciphertext = jweObject.ciphertext val authTag = jweObject.authTag val plaintext = @@ -177,7 +178,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { val iv = Random.Default.nextBytes(jweEncryption.ivLengthBits / 8) val headerSerialized = jweHeader.serialize() val aad = headerSerialized.encodeToByteArray() - val aadForCipher = aad.encodeToByteArray(Base64.UrlSafe) + val aadForCipher = aad.encodeToByteArray(Base64Url) val ciphertext = cryptoService.encrypt(key, iv, aadForCipher, payload, jweEncryption).getOrElse { Napier.w("No ciphertext from native code", it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt index bbf0879e0..1ad52a62b 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.jws +import at.asitplus.wallet.lib.data.Base64Url import io.github.aakira.napier.Napier import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull @@ -15,7 +16,7 @@ data class JwsSigned( val plainSignatureInput: String, ) { fun serialize(): String { - return "${plainSignatureInput}.${signature.encodeToString(Base64.UrlSafe)}" + return "${plainSignatureInput}.${signature.encodeToString(Base64Url)}" } override fun equals(other: Any?): Boolean { @@ -42,13 +43,13 @@ data class JwsSigned( fun parse(it: String): JwsSigned? { val stringList = it.replace("[^A-Za-z0-9-_.]".toRegex(), "").split(".") if (stringList.size != 3) return null.also { Napier.w("Could not parse JWS: $it") } - val headerInput = stringList[0].decodeToByteArrayOrNull(Base64()) + val headerInput = stringList[0].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWS: $it") } val header = JwsHeader.deserialize(headerInput.decodeToString()) ?: return null.also { Napier.w("Could not parse JWS: $it") } - val payload = stringList[1].decodeToByteArrayOrNull(Base64()) + val payload = stringList[1].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWS: $it") } - val signature = stringList[2].decodeToByteArrayOrNull(Base64()) + val signature = stringList[2].decodeToByteArrayOrNull(Base64(strict = true)) ?: return null.also { Napier.w("Could not parse JWS: $it") } return JwsSigned(header, payload, signature, "${stringList[0]}.${stringList[1]}") } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt index 9cd3e466c..3ae11f04b 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt @@ -42,11 +42,11 @@ object MultibaseHelper { return decodeP256Key(multicodecDecode(multibaseDecode(stripped))) } - private fun multibaseWrapBase64(it: ByteArray) = "m${it.encodeToString(Base64())}" + private fun multibaseWrapBase64(it: ByteArray) = "m${it.encodeToString(Base64(strict = true))}" private fun multibaseDecode(it: String?) = if (it != null && it.startsWith("m")) { - it.removePrefix("m").decodeToByteArrayOrNull(Base64()) + it.removePrefix("m").decodeToByteArrayOrNull(Base64(strict = true)) } else null // 0x1200 would be with compression, so we'll use 0x1290 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 7e0d7cf24..e1b50a006 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,6 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.AtomicAttribute2023 +import at.asitplus.wallet.lib.data.Base64Url import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.CredentialStatus import at.asitplus.wallet.lib.data.VerifiableCredential @@ -401,8 +402,8 @@ class ValidatorVcTest : FreeSpec() { ) val jwsPayload = vcJws.serialize().encodeToByteArray() val signatureInput = - jwsHeader.serialize().encodeToByteArray().encodeToString(Base64.UrlSafe) + - "." + jwsPayload.encodeToString(Base64.UrlSafe) + jwsHeader.serialize().encodeToByteArray().encodeToString(Base64Url) + + "." + jwsPayload.encodeToString(Base64Url) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = issuerCryptoService.sign(signatureInputBytes) .getOrElse { return null } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt index 4e0f691b2..9a586041a 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt @@ -28,8 +28,8 @@ class JwsHeaderSerializationTest : FreeSpec({ val serialized = header.serialize() - serialized shouldContain """"${first.encodeToString(Base64())}"""" - serialized shouldContain """"${second.encodeToString(Base64())}"""" + serialized shouldContain """"${first.encodeToString(Base64(strict = true))}"""" + serialized shouldContain """"${second.encodeToString(Base64(strict = true))}"""" serialized shouldContain """"$kid"""" } @@ -44,8 +44,8 @@ class JwsHeaderSerializationTest : FreeSpec({ | "alg": "${algorithm.text}", | "kid": "$kid", | "typ": "$type", - | "x5c":["${first.encodeToString(Base64())}", - | "${second.encodeToString(Base64())}"]} + | "x5c":["${first.encodeToString(Base64(strict = true))}", + | "${second.encodeToString(Base64(strict = true))}"]} | """.trimMargin() val parsed = JwsHeader.deserialize(serialized) 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 f7bb41c8a..177d0e801 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 @@ -96,7 +96,7 @@ actual class DefaultCryptoService : CryptoService { return KmmResult.success( AuthenticatedCiphertext( input.reversedArray(), - "authtag-${key.encodeToString(Base64())}".encodeToByteArray() + "authtag-${key.encodeToString(Base64(strict = true))}".encodeToByteArray() ) ) } @@ -109,7 +109,7 @@ actual class DefaultCryptoService : CryptoService { authTag: ByteArray, algorithm: JweEncryption ): KmmResult { - return if (authTag.contentEquals("authtag-${key.encodeToString(Base64())}".encodeToByteArray())) + return if (authTag.contentEquals("authtag-${key.encodeToString(Base64(strict = true))}".encodeToByteArray())) KmmResult.success(input.reversedArray()) else KmmResult.failure(IllegalArgumentException()) From eaed2cf8cc000dca10af32aa7bdb8d9eee195126 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Fri, 1 Sep 2023 11:53:52 +0200 Subject: [PATCH 06/14] Use static instances for Base64 encoding --- .../asitplus/wallet/lib/msg/JwmAttachment.kt | 12 ++-- .../wallet/lib/oidvci/IssuerService.kt | 5 +- .../asitplus/wallet/lib/agent/IssuerAgent.kt | 3 +- .../at/asitplus/wallet/lib/agent/Validator.kt | 64 ++++++++++++++----- .../at/asitplus/wallet/lib/data/Json.kt | 10 ++- .../lib/jws/ByteArrayBase64Serializer.kt | 6 +- .../lib/jws/ByteArrayBase64UrlSerializer.kt | 6 +- .../at/asitplus/wallet/lib/jws/JsonWebKey.kt | 8 +-- .../asitplus/wallet/lib/jws/JweEncrypted.kt | 24 +++---- .../at/asitplus/wallet/lib/jws/JwsService.kt | 11 ++-- .../at/asitplus/wallet/lib/jws/JwsSigned.kt | 12 ++-- .../wallet/lib/jws/MultibaseHelper.kt | 5 +- .../wallet/lib/agent/ValidatorVcTest.kt | 7 +- .../lib/jws/JwsHeaderSerializationTest.kt | 9 ++- .../wallet/lib/agent/DefaultCryptoService.kt | 8 +-- 15 files changed, 113 insertions(+), 77 deletions(-) diff --git a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt index bb979190e..ca275adfe 100644 --- a/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt +++ b/vclib-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt @@ -1,9 +1,9 @@ package at.asitplus.wallet.lib.msg import at.asitplus.wallet.lib.aries.jsonSerializer +import at.asitplus.wallet.lib.data.Base64Strict import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.SerialName @@ -30,7 +30,7 @@ data class JwmAttachment( fun decodeString(): String? { if (data.base64 != null) - return data.base64.decodeToByteArrayOrNull(Base64(strict = true))?.decodeToString() + return data.base64.decodeToByteArrayOrNull(Base64Strict)?.decodeToString() if (data.jws != null) return data.jws return null @@ -39,7 +39,7 @@ data class JwmAttachment( fun decodeBinary(): ByteArray? { if (data.base64 != null) - return data.base64.decodeToByteArrayOrNull(Base64(strict = true)) + return data.base64.decodeToByteArrayOrNull(Base64Strict) return null .also { Napier.w("Could not binary decode JWM attachment") } } @@ -57,7 +57,7 @@ data class JwmAttachment( id = uuid4().toString(), mediaType = "application/base64", data = JwmAttachmentData( - base64 = data.encodeToByteArray().encodeToString(Base64(strict = true)) + base64 = data.encodeToByteArray().encodeToString(Base64Strict) ) ) @@ -65,7 +65,7 @@ data class JwmAttachment( id = uuid4().toString(), mediaType = "application/base64", data = JwmAttachmentData( - base64 = data.encodeToString(Base64(strict = true)) + base64 = data.encodeToString(Base64Strict) ) ) @@ -75,7 +75,7 @@ data class JwmAttachment( filename = filename, parent = parent, data = JwmAttachmentData( - base64 = data.encodeToString(Base64(strict = true)) + base64 = data.encodeToString(Base64Strict) ) ) diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt index 481f02ed9..e4d07a15b 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt @@ -4,7 +4,7 @@ import at.asitplus.wallet.lib.agent.Issuer 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.data.Base64Url +import at.asitplus.wallet.lib.data.Base64UrlStrict import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.VcDataModelConstants.VERIFIABLE_CREDENTIAL import at.asitplus.wallet.lib.iso.IsoDataModelConstants.DOC_TYPE_MDL @@ -24,7 +24,6 @@ import at.asitplus.wallet.lib.oidc.OpenIdConstants.TOKEN_TYPE_BEARER import at.asitplus.wallet.lib.oidc.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification import io.ktor.http.URLBuilder -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.coroutines.cancellation.CancellationException @@ -168,7 +167,7 @@ class IssuerService( return when (val issuedCredential = issuedCredentialResult.successful.first()) { is Issuer.IssuedCredential.Iso -> CredentialResponseParameters( format = CredentialFormatEnum.MSO_MDOC, - credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64Url) + credential = issuedCredential.issuerSigned.serialize().encodeToString(Base64UrlStrict) ) is Issuer.IssuedCredential.Vc -> CredentialResponseParameters( 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 a29661ad9..19ebc0b96 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 @@ -9,6 +9,7 @@ import at.asitplus.wallet.lib.cbor.CoseHeader import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.cbor.CoseService import at.asitplus.wallet.lib.cbor.DefaultCoseService +import at.asitplus.wallet.lib.data.Base64Strict import at.asitplus.wallet.lib.data.CredentialStatus import at.asitplus.wallet.lib.data.RevocationListSubject import at.asitplus.wallet.lib.data.VcDataModelConstants.REVOCATION_LIST_MIN_SIZE @@ -218,7 +219,7 @@ class IssuerAgent( issuerCredentialStore.getRevokedStatusListIndexList(timePeriod) .forEach { bitset[it] = true } val input = bitset.toByteArray() - return zlibService.compress(input)?.encodeToString(Base64(strict = true)) + return zlibService.compress(input)?.encodeToString(Base64Strict) } /** 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 a28b329a4..32908dab4 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 @@ -6,6 +6,7 @@ import at.asitplus.wallet.lib.ZlibService import at.asitplus.wallet.lib.cbor.CoseKey import at.asitplus.wallet.lib.cbor.DefaultVerifierCoseService import at.asitplus.wallet.lib.cbor.VerifierCoseService +import at.asitplus.wallet.lib.data.Base64Strict import at.asitplus.wallet.lib.data.IsoDocumentParsed import at.asitplus.wallet.lib.data.RevocationListSubject import at.asitplus.wallet.lib.data.VerifiableCredentialJws @@ -25,7 +26,6 @@ import at.asitplus.wallet.lib.jws.VerifierJwsService import at.asitplus.wallet.lib.toBitSet import io.github.aakira.napier.Napier import io.matthewnelson.encoding.base16.Base16 -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.cbor.ByteStringWrapper @@ -88,7 +88,7 @@ class Validator( return false .also { Napier.d("credentialSubject invalid") } val encodedList = parsedVc.jws.vc.credentialSubject.encodedList - this.revocationList = encodedList.decodeToByteArrayOrNull(Base64(strict = true))?.let { + this.revocationList = encodedList.decodeToByteArrayOrNull(Base64Strict)?.let { zlibService.decompress(it)?.toBitSet() ?: return false.also { Napier.d("Invalid ZLIB") } } ?: return false.also { Napier.d("Invalid Base64") } Napier.d("Revocation list is valid") @@ -188,10 +188,14 @@ class Validator( */ fun verifyDocument(doc: Document, challenge: String): Verifier.VerifyPresentationResult { if (doc.docType != DOC_TYPE_MDL) - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("Invalid docType: ${doc.docType}") } if (doc.errors != null) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("Document has errors: ${doc.errors}") } } val issuerSigned = doc.issuerSigned @@ -199,45 +203,65 @@ class Validator( val issuerKey = issuerAuth.unprotectedHeader?.certificateChain?.let { CryptoUtils.extractPublicKeyFromX509Cert(it)?.toCoseKey() - } ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + } ?: return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("Got no issuer key in $issuerAuth") } if (verifierCoseService.verifyCose(issuerAuth, issuerKey).getOrNull() != true) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("IssuerAuth not verified: $issuerAuth") } } val mso = issuerSigned.getIssuerAuthPayloadAsMso() - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + ?: return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16(strict = true))}") } if (mso.docType != DOC_TYPE_MDL) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("Invalid docType in MSO: ${mso.docType}") } } val mdlItems = mso.valueDigests[NAMESPACE_MDL] - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + ?: return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("mdlItems are null in MSO: ${mso.valueDigests}") } val walletKey = mso.deviceKeyInfo.deviceKey val deviceSignature = doc.deviceSigned.deviceAuth.deviceSignature - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + ?: return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("DeviceSignature is null: ${doc.deviceSigned.deviceAuth}") } if (verifierCoseService.verifyCose(deviceSignature, walletKey).getOrNull() != true) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("DeviceSignature not verified") } } val deviceSignaturePayload = deviceSignature.payload - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + ?: return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("DeviceSignature does not contain challenge") } if (!deviceSignaturePayload.contentEquals(challenge.encodeToByteArray())) { - return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("DeviceSignature does not contain correct challenge") } } val issuerSignedItems = issuerSigned.namespaces?.get(NAMESPACE_MDL) - ?: return Verifier.VerifyPresentationResult.InvalidStructure(doc.serialize().encodeToString(Base16(strict = true))) + ?: return Verifier.VerifyPresentationResult.InvalidStructure( + doc.serialize().encodeToString(Base16(strict = true)) + ) .also { Napier.w("No issuer signed items in ${issuerSigned.namespaces}") } val validatedItems = issuerSignedItems.entries.associateWith { it.verify(mdlItems) } @@ -253,7 +277,9 @@ class Validator( val issuerHash = mdlItems.entries.first { it.key == value.digestId } // TODO analyze usages of tag wrapping val verifierHash = serialized.wrapInCborTag(24).sha256() - if (!verifierHash.encodeToString(Base16(strict = true)).contentEquals(issuerHash.value.encodeToString(Base16(strict = true)))) { + if (!verifierHash.encodeToString(Base16(strict = true)) + .contentEquals(issuerHash.value.encodeToString(Base16(strict = true))) + ) { Napier.w("Could not verify hash of value for ${value.elementIdentifier}") return false } @@ -305,12 +331,16 @@ class Validator( Napier.d("Verifying ISO Cred $it") if (issuerKey == null) { Napier.w("ISO: No issuer key") - return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyCredentialResult.InvalidStructure( + it.serialize().encodeToString(Base16(strict = true)) + ) } val result = verifierCoseService.verifyCose(it.issuerAuth, issuerKey) if (result.getOrNull() != true) { Napier.w("ISO: Could not verify credential", result.exceptionOrNull()) - return Verifier.VerifyCredentialResult.InvalidStructure(it.serialize().encodeToString(Base16(strict = true))) + return Verifier.VerifyCredentialResult.InvalidStructure( + it.serialize().encodeToString(Base16(strict = true)) + ) } return Verifier.VerifyCredentialResult.SuccessIso(it) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt index 60f4e6e59..70c476f33 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt @@ -31,9 +31,17 @@ val jsonSerializer by lazy { } } -val Base64Url = Base64(config = Base64ConfigBuilder().apply { +val Base64UrlStrict = Base64(config = Base64ConfigBuilder().apply { lineBreakInterval = 0 encodeToUrlSafe = true isLenient = true padEncoded = false +}.build()) + + +val Base64Strict = Base64(config = Base64ConfigBuilder().apply { + lineBreakInterval = 0 + encodeToUrlSafe = false + isLenient = true + padEncoded = true }.build()) \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt index 5b0a8f17a..9e14605d6 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64Serializer.kt @@ -1,6 +1,6 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.encoding.base64.Base64 +import at.asitplus.wallet.lib.data.Base64Strict import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.KSerializer @@ -16,11 +16,11 @@ object ByteArrayBase64Serializer : KSerializer { PrimitiveSerialDescriptor("ByteArrayBase64Serializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - encoder.encodeString(value.encodeToString(Base64(strict = true))) + encoder.encodeString(value.encodeToString(Base64Strict)) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64(strict = true)) ?: byteArrayOf() + return decoder.decodeString().decodeToByteArrayOrNull(Base64Strict) ?: byteArrayOf() } } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt index 3152ff2ab..973385987 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/ByteArrayBase64UrlSerializer.kt @@ -1,6 +1,6 @@ package at.asitplus.wallet.lib.jws -import at.asitplus.wallet.lib.data.Base64Url +import at.asitplus.wallet.lib.data.Base64UrlStrict import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.KSerializer @@ -16,11 +16,11 @@ object ByteArrayBase64UrlSerializer : KSerializer { PrimitiveSerialDescriptor("ByteArrayBase64UrlSerializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - encoder.encodeString(value.encodeToString(Base64Url)) + encoder.encodeString(value.encodeToString(Base64UrlStrict)) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64Url) ?: byteArrayOf() + return decoder.decodeString().decodeToByteArrayOrNull(Base64UrlStrict) ?: byteArrayOf() } } \ 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 e91439b54..378f11fcf 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 @@ -2,9 +2,9 @@ package at.asitplus.wallet.lib.jws import at.asitplus.KmmResult import at.asitplus.wallet.lib.CryptoPublicKey +import at.asitplus.wallet.lib.data.Base64Strict import at.asitplus.wallet.lib.data.jsonSerializer import io.github.aakira.napier.Napier -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -136,9 +136,9 @@ data class JsonWebKey( } override fun toString(): String { - return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId, x=${x?.encodeToString(Base64(strict = true))}, y=${ - y?.encodeToString(Base64(strict = true)) - })" + return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId," + + " x=${x?.encodeToString(Base64Strict)}," + + " y=${y?.encodeToString(Base64Strict)})" } fun toCryptoPublicKey(): CryptoPublicKey? { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt index 2aa762a83..9d2bdc1c8 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweEncrypted.kt @@ -1,8 +1,8 @@ package at.asitplus.wallet.lib.jws -import at.asitplus.wallet.lib.data.Base64Url +import at.asitplus.wallet.lib.data.Base64Strict +import at.asitplus.wallet.lib.data.Base64UrlStrict import io.github.aakira.napier.Napier -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString @@ -23,11 +23,11 @@ data class JweEncrypted( get() = JweHeader.deserialize(headerAsParsed.decodeToString()) fun serialize(): String { - return headerAsParsed.encodeToString(Base64Url) + - ".${encryptedKey?.encodeToString(Base64Url) ?: ""}" + - ".${iv.encodeToString(Base64Url)}" + - ".${ciphertext.encodeToString(Base64Url)}" + - ".${authTag.encodeToString(Base64Url)}" + return headerAsParsed.encodeToString(Base64UrlStrict) + + ".${encryptedKey?.encodeToString(Base64UrlStrict) ?: ""}" + + ".${iv.encodeToString(Base64UrlStrict)}" + + ".${ciphertext.encodeToString(Base64UrlStrict)}" + + ".${authTag.encodeToString(Base64UrlStrict)}" } override fun equals(other: Any?): Boolean { @@ -62,14 +62,14 @@ data class JweEncrypted( fun parse(it: String): JweEncrypted? { val stringList = it.replace("[^A-Za-z0-9-_.]".toRegex(), "").split(".") if (stringList.size != 5) return null.also { Napier.w("Could not parse JWE: $it") } - val headerAsParsed = stringList[0].decodeToByteArrayOrNull(Base64(strict = true)) + val headerAsParsed = stringList[0].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val encryptedKey = stringList[1].decodeToByteArrayOrNull(Base64(strict = true)) - val iv = stringList[2].decodeToByteArrayOrNull(Base64(strict = true)) + val encryptedKey = stringList[1].decodeToByteArrayOrNull(Base64Strict) + val iv = stringList[2].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64(strict = true)) + val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val authTag = stringList[4].decodeToByteArrayOrNull(Base64(strict = true)) + val authTag = stringList[4].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWE: $it") } return JweEncrypted(headerAsParsed, encryptedKey, iv, ciphertext, authTag) } 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 28e1d18a8..1d7c7fd1b 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 @@ -4,12 +4,11 @@ import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.DefaultVerifierCryptoService import at.asitplus.wallet.lib.agent.Digest import at.asitplus.wallet.lib.agent.VerifierCryptoService -import at.asitplus.wallet.lib.data.Base64Url +import at.asitplus.wallet.lib.data.Base64UrlStrict import at.asitplus.wallet.lib.jws.JwsExtensions.encodeToByteArray import at.asitplus.wallet.lib.jws.JwsExtensions.encodeWithLength import at.asitplus.wallet.lib.jws.JwsExtensions.extractSignatureValues import io.github.aakira.napier.Napier -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.random.Random @@ -82,8 +81,8 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { ) { return null.also { Napier.w("Algorithm or keyId not matching to cryptoService") } } - val signatureInput = header.serialize().encodeToByteArray().encodeToString(Base64Url) + - "." + payload.encodeToString(Base64Url) + val signatureInput = header.serialize().encodeToByteArray().encodeToString(Base64UrlStrict) + + "." + payload.encodeToString(Base64UrlStrict) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = cryptoService.sign(signatureInputBytes).getOrElse { Napier.w("No signature from native code", it) @@ -131,7 +130,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { return null } val iv = jweObject.iv - val aad = jweObject.headerAsParsed.encodeToByteArray(Base64Url) + val aad = jweObject.headerAsParsed.encodeToByteArray(Base64UrlStrict) val ciphertext = jweObject.ciphertext val authTag = jweObject.authTag val plaintext = @@ -178,7 +177,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { val iv = Random.Default.nextBytes(jweEncryption.ivLengthBits / 8) val headerSerialized = jweHeader.serialize() val aad = headerSerialized.encodeToByteArray() - val aadForCipher = aad.encodeToByteArray(Base64Url) + val aadForCipher = aad.encodeToByteArray(Base64UrlStrict) val ciphertext = cryptoService.encrypt(key, iv, aadForCipher, payload, jweEncryption).getOrElse { Napier.w("No ciphertext from native code", it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt index 1ad52a62b..426e9c3c9 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsSigned.kt @@ -1,8 +1,8 @@ package at.asitplus.wallet.lib.jws -import at.asitplus.wallet.lib.data.Base64Url +import at.asitplus.wallet.lib.data.Base64Strict +import at.asitplus.wallet.lib.data.Base64UrlStrict import io.github.aakira.napier.Napier -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString @@ -16,7 +16,7 @@ data class JwsSigned( val plainSignatureInput: String, ) { fun serialize(): String { - return "${plainSignatureInput}.${signature.encodeToString(Base64Url)}" + return "${plainSignatureInput}.${signature.encodeToString(Base64UrlStrict)}" } override fun equals(other: Any?): Boolean { @@ -43,13 +43,13 @@ data class JwsSigned( fun parse(it: String): JwsSigned? { val stringList = it.replace("[^A-Za-z0-9-_.]".toRegex(), "").split(".") if (stringList.size != 3) return null.also { Napier.w("Could not parse JWS: $it") } - val headerInput = stringList[0].decodeToByteArrayOrNull(Base64(strict = true)) + val headerInput = stringList[0].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWS: $it") } val header = JwsHeader.deserialize(headerInput.decodeToString()) ?: return null.also { Napier.w("Could not parse JWS: $it") } - val payload = stringList[1].decodeToByteArrayOrNull(Base64(strict = true)) + val payload = stringList[1].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWS: $it") } - val signature = stringList[2].decodeToByteArrayOrNull(Base64(strict = true)) + val signature = stringList[2].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWS: $it") } return JwsSigned(header, payload, signature, "${stringList[0]}.${stringList[1]}") } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt index 3ae11f04b..8b5701bd2 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/MultibaseHelper.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.jws import at.asitplus.wallet.lib.cbor.CoseEllipticCurve +import at.asitplus.wallet.lib.data.Base64Strict import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString @@ -42,11 +43,11 @@ object MultibaseHelper { return decodeP256Key(multicodecDecode(multibaseDecode(stripped))) } - private fun multibaseWrapBase64(it: ByteArray) = "m${it.encodeToString(Base64(strict = true))}" + private fun multibaseWrapBase64(it: ByteArray) = "m${it.encodeToString(Base64Strict)}" private fun multibaseDecode(it: String?) = if (it != null && it.startsWith("m")) { - it.removePrefix("m").decodeToByteArrayOrNull(Base64(strict = true)) + it.removePrefix("m").decodeToByteArrayOrNull(Base64Strict) } else null // 0x1200 would be with compression, so we'll use 0x1290 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 e1b50a006..f0644d6d2 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,7 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.AtomicAttribute2023 -import at.asitplus.wallet.lib.data.Base64Url +import at.asitplus.wallet.lib.data.Base64UrlStrict import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.CredentialStatus import at.asitplus.wallet.lib.data.VerifiableCredential @@ -18,7 +18,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.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -402,8 +401,8 @@ class ValidatorVcTest : FreeSpec() { ) val jwsPayload = vcJws.serialize().encodeToByteArray() val signatureInput = - jwsHeader.serialize().encodeToByteArray().encodeToString(Base64Url) + - "." + jwsPayload.encodeToString(Base64Url) + jwsHeader.serialize().encodeToByteArray().encodeToString(Base64UrlStrict) + + "." + jwsPayload.encodeToString(Base64UrlStrict) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = issuerCryptoService.sign(signatureInputBytes) .getOrElse { return null } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt index 9a586041a..825223598 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt @@ -1,13 +1,13 @@ package at.asitplus.wallet.lib.jws +import at.asitplus.wallet.lib.data.Base64Strict import com.benasher44.uuid.uuid4 import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain -import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.random.Random @@ -28,8 +28,8 @@ class JwsHeaderSerializationTest : FreeSpec({ val serialized = header.serialize() - serialized shouldContain """"${first.encodeToString(Base64(strict = true))}"""" - serialized shouldContain """"${second.encodeToString(Base64(strict = true))}"""" + serialized shouldContain """"${first.encodeToString(Base64Strict)}"""" + serialized shouldContain """"${second.encodeToString(Base64Strict)}"""" serialized shouldContain """"$kid"""" } @@ -44,8 +44,7 @@ class JwsHeaderSerializationTest : FreeSpec({ | "alg": "${algorithm.text}", | "kid": "$kid", | "typ": "$type", - | "x5c":["${first.encodeToString(Base64(strict = true))}", - | "${second.encodeToString(Base64(strict = true))}"]} + | "x5c":["${first.encodeToString(Base64Strict)}","${second.encodeToString(Base64Strict)}"]} | """.trimMargin() val parsed = JwsHeader.deserialize(serialized) 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 177d0e801..658f8da50 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 @@ -5,6 +5,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.data.Base64Strict import at.asitplus.wallet.lib.jws.EcCurve import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm @@ -12,7 +13,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 io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer @@ -28,8 +28,8 @@ import platform.Foundation.CFBridgingRetain import platform.Foundation.NSData import platform.Foundation.NSNumber import platform.Foundation.create -import platform.Security.SecCertificateCreateWithData import platform.Security.SecCertificateCopyKey +import platform.Security.SecCertificateCreateWithData import platform.Security.SecKeyCopyExternalRepresentation import platform.Security.SecKeyCopyPublicKey import platform.Security.SecKeyCreateRandomKey @@ -96,7 +96,7 @@ actual class DefaultCryptoService : CryptoService { return KmmResult.success( AuthenticatedCiphertext( input.reversedArray(), - "authtag-${key.encodeToString(Base64(strict = true))}".encodeToByteArray() + "authtag-${key.encodeToString(Base64Strict)}".encodeToByteArray() ) ) } @@ -109,7 +109,7 @@ actual class DefaultCryptoService : CryptoService { authTag: ByteArray, algorithm: JweEncryption ): KmmResult { - return if (authTag.contentEquals("authtag-${key.encodeToString(Base64(strict = true))}".encodeToByteArray())) + return if (authTag.contentEquals("authtag-${key.encodeToString(Base64Strict)}".encodeToByteArray())) KmmResult.success(input.reversedArray()) else KmmResult.failure(IllegalArgumentException()) From 9e6975d44998321e093a4df081baa48b0b4ca762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Mon, 4 Sep 2023 19:47:00 +0200 Subject: [PATCH 07/14] Custom nameFn for data-driven tests --- .../wallet/lib/aries/ProblemReporterTest.kt | 6 +++--- .../wallet/lib/agent/ValidatorVcTest.kt | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/ProblemReporterTest.kt b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/ProblemReporterTest.kt index d7fcd7a6a..076dbd126 100644 --- a/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/ProblemReporterTest.kt +++ b/vclib-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/ProblemReporterTest.kt @@ -15,7 +15,7 @@ class ProblemReporterTest : FreeSpec({ val problemReporter = ProblemReporter() "sorter" - { - withData(ProblemReportSorter.values().asList()) { + withData(ProblemReportSorter.entries) { val report = ProblemReport( body = ProblemReportBody( sorter = it, @@ -36,7 +36,7 @@ class ProblemReporterTest : FreeSpec({ } "scope" - { - withData(ProblemReportScope.values().asList()) { + withData(ProblemReportScope.entries) { val report = ProblemReport( body = ProblemReportBody( sorter = ProblemReportSorter.WARNING, @@ -57,7 +57,7 @@ class ProblemReporterTest : FreeSpec({ } "descriptor" - { - withData(ProblemReportDescriptor.values().asList()) { + withData(ProblemReportDescriptor.entries) { val report = ProblemReport( body = ProblemReportBody( sorter = ProblemReportSorter.WARNING, 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 f0644d6d2..c76f6da67 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 @@ -108,6 +108,7 @@ class ValidatorVcTest : FreeSpec() { "Manually created and valid credential is valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -124,6 +125,7 @@ class ValidatorVcTest : FreeSpec() { "Wrong key ends in wrong signature is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -141,6 +143,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid sub in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -158,6 +161,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid issuer in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -174,6 +178,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid jwtId in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -191,6 +196,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid type in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -209,6 +215,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid expiration in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -235,6 +242,7 @@ class ValidatorVcTest : FreeSpec() { "No expiration date is valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -251,6 +259,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid jws-expiration in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -268,6 +277,7 @@ class ValidatorVcTest : FreeSpec() { "Expiration not matching in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -289,6 +299,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid NotBefore in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -308,6 +319,7 @@ class ValidatorVcTest : FreeSpec() { "Invalid issuance date in credential is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -325,6 +337,7 @@ class ValidatorVcTest : FreeSpec() { "Issuance date and not before not matching is not valid" - { withData( + nameFn = ::credentialNameFn, dataProvider.getCredentialWithType( verifier.identifier, attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType) @@ -341,6 +354,12 @@ class ValidatorVcTest : FreeSpec() { } } + private fun credentialNameFn(it: CredentialToBeIssued): String = + when (it) { + is CredentialToBeIssued.Iso -> it.attributeType;is CredentialToBeIssued.Vc -> it.attributeType + + } + private fun issueCredential( credential: CredentialToBeIssued, issuanceDate: Instant = Clock.System.now(), From d3384cc65278e63c4838285a4273583be74c7fe7 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Mon, 11 Sep 2023 10:54:44 +0200 Subject: [PATCH 08/14] Update gradle-conventions-plugin --- 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 4aec06670..547265c8d 160000 --- a/conventions-vclib/gradle-conventions-plugin +++ b/conventions-vclib/gradle-conventions-plugin @@ -1 +1 @@ -Subproject commit 4aec06670f8b4b13551281fa466e81458c2c7bbd +Subproject commit 547265c8d424c42109677da6de274e656c3ebb54 From cd8178627f7f5cae9a33d8326948eb6baabca56d Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Wed, 20 Sep 2023 14:24:45 +0200 Subject: [PATCH 09/14] Prevent segfaults on iOS --- .../at/asitplus/wallet/lib/agent/DefaultCryptoService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 658f8da50..bfe8c98c7 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 @@ -61,7 +61,7 @@ actual class DefaultCryptoService : CryptoService { private val privateKey: SecKeyRef private val publicKey: SecKeyRef private val cryptoPublicKey: CryptoPublicKey - final override val certificate: ByteArray + override val certificate: ByteArray actual constructor() { val query = CFDictionaryCreateMutable(null, 2, null, null).apply { @@ -189,6 +189,7 @@ actual class DefaultVerifierCryptoService : VerifierCryptoService { actual object CryptoUtils { actual fun extractPublicKeyFromX509Cert(it: ByteArray): CryptoPublicKey? { + if (it.isEmpty()) return null memScoped { val certData = CFBridgingRetain(toData(it)) as CFDataRef val certificate = SecCertificateCreateWithData(null, certData) From a0e93a0e8d23a5266a73a8856fbd6d53bc221ef5 Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Wed, 20 Sep 2023 15:31:41 +0200 Subject: [PATCH 10/14] Create self-signed certificate under iOS This implementas a rudimentary DER encoder for X.509 certificates --- .../lib/oidc/OidcSiopIsoProtocolTest.kt | 5 +- .../asitplus/wallet/lib/jws/JwsExtensions.kt | 9 ++ .../wallet/lib/jws/X509Certificate.kt | 133 ++++++++++++++++++ .../wallet/lib/agent/DefaultCryptoService.kt | 24 ++-- .../wallet/lib/jws/X509CertificateJvmTest.kt | 95 +++++++++++++ 5 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt create mode 100644 vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt 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 1fccecde1..1700d027d 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 @@ -10,11 +10,12 @@ import at.asitplus.wallet.lib.agent.VerifierAgent import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.iso.IsoDataModelConstants import com.benasher44.uuid.uuid4 +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldBeSingleton import io.kotest.matchers.collections.shouldHaveSingleElement -import io.kotest.matchers.collections.shouldMatchEach import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.types.shouldBeInstanceOf import kotlinx.coroutines.runBlocking @@ -34,6 +35,8 @@ class OidcSiopIsoProtocolTest : FreeSpec({ lateinit var verifierSiop: OidcSiopVerifier beforeEach { + Napier.takeLogarithm() + Napier.base(DebugAntilog()) holderCryptoService = DefaultCryptoService() verifierCryptoService = DefaultCryptoService() relyingPartyUrl = "https://example.com/rp/${uuid4()}" diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsExtensions.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsExtensions.kt index 3ff0ae2e5..afe352bee 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsExtensions.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsExtensions.kt @@ -66,6 +66,15 @@ object JwsExtensions { fun Int.encodeToByteArray(): ByteArray = byteArrayOf((this ushr 24).toByte(), (this ushr 16).toByte(), (this ushr 8).toByte(), (this).toByte()) + /** + * Encode as a four-byte array + */ + fun Long.encodeToByteArray(): ByteArray = + byteArrayOf( + (this ushr 56).toByte(), (this ushr 48).toByte(), (this ushr 40).toByte(), (this ushr 32).toByte(), + (this ushr 24).toByte(), (this ushr 16).toByte(), (this ushr 8).toByte(), (this).toByte() + ) + /** * Strips the leading 0x00 byte of an ASN.1-encoded Integer, * that will be there if the first bit of the value is set, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt new file mode 100644 index 000000000..448b362d9 --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt @@ -0,0 +1,133 @@ +package at.asitplus.wallet.lib.jws + +import at.asitplus.wallet.lib.CryptoPublicKey +import at.asitplus.wallet.lib.jws.JwsExtensions.encodeToByteArray +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import kotlinx.datetime.Instant + +data class TbsCertificate( + val version: Int = 2, + val serialNumber: Long, + val signatureAlgorithm: JwsAlgorithm, + val issuer: String, + val validFrom: Instant, + val validUntil: Instant, + val subject: String, + val subjectPublicKey: CryptoPublicKey +) { + fun encodeToDer(): ByteArray { + return (version.encodeAsVersion() + + serialNumber.encodeToDer() + + signatureAlgorithm.encodeToDer() + + issuer.encodeAsCommonName() + + (validFrom.encodeToDer() + validUntil.encodeToDer()).sequence() + + subject.encodeAsCommonName() + + subjectPublicKey.encodeToDer()) + .sequence() + } +} + +data class X509Certificate( + val tbsCertificate: TbsCertificate, + val signatureAlgorithm: JwsAlgorithm, + val signature: ByteArray +) { + fun encodeToDer(): ByteArray { + return (tbsCertificate.encodeToDer() + + signatureAlgorithm.encodeToDer() + + signature.encodeAsBitString()).sequence() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as X509Certificate + + if (tbsCertificate != other.tbsCertificate) return false + if (signatureAlgorithm != other.signatureAlgorithm) return false + if (!signature.contentEquals(other.signature)) return false + + return true + } + + override fun hashCode(): Int { + var result = tbsCertificate.hashCode() + result = 31 * result + signatureAlgorithm.hashCode() + result = 31 * result + signature.contentHashCode() + return result + } +} + +private fun String.encodeAsCommonName(): ByteArray { + return ("550403".decodeToByteArray(Base16()).oid() + this.encodeToDer()).sequence().set().sequence() +} + +private fun Int.encodeAsVersion(): ByteArray = encodeToDer().wrapInAsn1Tag(0xA0.toByte()) + +private fun Int.encodeToDer(): ByteArray = + encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray().wrapInAsn1Tag(0x02) + +private fun Long.encodeToDer(): ByteArray = + encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray().wrapInAsn1Tag(0x02) + +private fun CryptoPublicKey.encodeToDer(): ByteArray = when (this) { + is CryptoPublicKey.Ec -> this.encodeToDer() +} + +private fun CryptoPublicKey.Ec.encodeToDer(): ByteArray { + val ecKeyTag = "2A8648CE3D0201".decodeToByteArray(Base16()).oid() + val ecEncryptionNullTag = "2A8648CE3D030107".decodeToByteArray(Base16()).oid() + val content = (byteArrayOf(0x04.toByte()) + x + y).encodeAsBitString() + return ((ecKeyTag + ecEncryptionNullTag).sequence() + content).sequence() +} + +private fun ByteArray.encodeAsBitString(): ByteArray = (byteArrayOf(0x00) + this).wrapInAsn1Tag(0x03) + +private fun String.encodeToDer(): ByteArray = this.encodeToByteArray().wrapInAsn1Tag(0x0c) + +private fun Instant.encodeToDer(): ByteArray { + val matchResult = + Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9]+)Z") + .matchEntire(toString()) + ?: throw IllegalArgumentException("instant serialization failed") + val year = matchResult.groups[1]?.value ?: throw IllegalArgumentException("instant serialization failed") + val month = matchResult.groups[2]?.value ?: throw IllegalArgumentException("instant serialization failed") + val day = matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization failed") + val hour = matchResult.groups[4]?.value ?: throw IllegalArgumentException("instant serialization failed") + val minute = matchResult.groups[5]?.value ?: throw IllegalArgumentException("instant serialization failed") + val seconds = matchResult.groups[6]?.value ?: throw IllegalArgumentException("instant serialization failed") + val string = "$year$month$day$hour$minute${seconds}Z" + return string.encodeToByteArray().wrapInAsn1Tag(0x17) +} + +private fun JwsAlgorithm.encodeToDer(): ByteArray { + return when (this) { + JwsAlgorithm.ES256 -> "2A8648CE3D040302".decodeToByteArray(Base16()).oid().sequence() + else -> TODO() + } +} + +private fun ByteArray.sequence() = this.wrapInAsn1Tag(0x30) + +private fun ByteArray.set() = this.wrapInAsn1Tag(0x31) + +private fun ByteArray.oid() = this.wrapInAsn1Tag(0x06) + +private fun ByteArray.wrapInAsn1Tag(tag: Byte): ByteArray { + return byteArrayOf(tag) + this.size.encodeLength() + this +} + +private fun Int.encodeLength(): ByteArray { + if (this < 128) { + return byteArrayOf(this.toByte()) + } + if (this < 0x100) { + return byteArrayOf(0x81.toByte(), this.toByte()) + } + if (this < 0x8000) { + return byteArrayOf(0x82.toByte(), (this ushr 8).toByte(), this.toByte()) + } + throw IllegalArgumentException("length $this") +} \ No newline at end of file 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 bfe8c98c7..096dd25a5 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 @@ -6,13 +6,9 @@ import at.asitplus.KmmResult import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.cbor.CoseAlgorithm import at.asitplus.wallet.lib.data.Base64Strict -import at.asitplus.wallet.lib.jws.EcCurve -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.JwsAlgorithm +import at.asitplus.wallet.lib.jws.* import at.asitplus.wallet.lib.jws.JwsExtensions.convertToAsn1Signature +import io.ktor.util.* import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer @@ -21,6 +17,10 @@ import kotlinx.cinterop.allocArrayOf import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.reinterpret +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.plus import platform.CoreFoundation.CFDataRef import platform.CoreFoundation.CFDictionaryCreateMutable import platform.Foundation.CFBridgingRelease @@ -73,19 +73,25 @@ actual class DefaultCryptoService : CryptoService { val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) val data = CFBridgingRelease(publicKeyData) as NSData 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? + val tbsCertificate = TbsCertificate(version = 2, serialNumber = 3, signatureAlgorithm = JwsAlgorithm.ES256, issuer = "SelfSigned", validFrom = Clock.System.now(), validUntil = Clock.System.now().plus(10, DateTimeUnit.MINUTE), subject = "SelfSigned", subjectPublicKey = cryptoPublicKey) + val signature = signInt(tbsCertificate.encodeToDer()) + this.certificate = X509Certificate(tbsCertificate = tbsCertificate, signatureAlgorithm = JwsAlgorithm.ES256, signature = signature).encodeToDer() } - override suspend fun sign(input: ByteArray): KmmResult { + private fun signInt(input: ByteArray): ByteArray { memScoped { val inputData = CFBridgingRetain(toData(input)) as CFDataRef val signature = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, inputData, null) val data = CFBridgingRelease(signature) as NSData - return KmmResult.success(data.toByteArray()) + return data.toByteArray() } } + override suspend fun sign(input: ByteArray): KmmResult { + return KmmResult.success(signInt(input)) + } + override fun encrypt( key: ByteArray, iv: ByteArray, diff --git a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt new file mode 100644 index 000000000..dec2ee0fa --- /dev/null +++ b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt @@ -0,0 +1,95 @@ +package at.asitplus.wallet.lib.jws + +import at.asitplus.wallet.lib.CryptoPublicKey +import at.asitplus.wallet.lib.agent.jcaName +import at.asitplus.wallet.lib.jws.JwsExtensions.ensureSize +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import kotlinx.datetime.toKotlinInstant +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.math.BigInteger +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.Signature +import java.security.cert.CertificateFactory +import java.security.interfaces.ECPublicKey +import java.time.Instant +import java.util.Date +import kotlin.math.absoluteValue +import kotlin.random.Random +import kotlin.time.Duration.Companion.days + +class X509CertificateJvmTest : FreeSpec({ + + lateinit var ecCurve: EcCurve + lateinit var keyPair: KeyPair + + beforeTest { + ecCurve = EcCurve.SECP_256_R_1 + keyPair = KeyPairGenerator.getInstance("EC").also { + it.initialize(256) + }.genKeyPair() + } + + "Certificates match" { + val ecPublicKey = keyPair.public as ECPublicKey + val keyX = ecPublicKey.w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + val keyY = ecPublicKey.w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) + val keyId = MultibaseHelper.calcKeyId(ecCurve, keyX, keyY)!! + val cryptoPublicKey = CryptoPublicKey.Ec(curve = ecCurve, keyId = keyId, x = keyX, y = keyY) + + // create certificate with bouncycastle + 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 commonName = "DefaultCryptoService" + val issuer = X500Name("CN=$commonName") + val builder = X509v3CertificateBuilder( + /* issuer = */ issuer, + /* serial = */ serialNumber, + /* notBefore = */ notBeforeDate, + /* notAfter = */ notAfterDate, + /* subject = */ issuer, + /* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(keyPair.public.encoded) + ) + val signatureAlgorithm = JwsAlgorithm.ES256 + val contentSigner: ContentSigner = JcaContentSignerBuilder(signatureAlgorithm.jcaName).build(keyPair.private) + val certificateHolder = builder.build(contentSigner) + + // create certificate with our structure + val tbsCertificate = TbsCertificate( + version = 2, + serialNumber = serialNumber.toLong(), + issuer = commonName, + validFrom = notBeforeDate.toInstant().toKotlinInstant(), + validUntil = notAfterDate.toInstant().toKotlinInstant(), + signatureAlgorithm = signatureAlgorithm, + subject = commonName, + subjectPublicKey = cryptoPublicKey + ) + val signed = Signature.getInstance(signatureAlgorithm.jcaName).apply { + initSign(keyPair.private) + update(tbsCertificate.encodeToDer()) + }.sign() + val x509Certificate = X509Certificate(tbsCertificate, signatureAlgorithm, signed) + + val kotlinEncoded = x509Certificate.encodeToDer() + val jvmEncoded = certificateHolder.encoded + println("Certificates will never entirely match because of randomness in ECDSA signature") + //kotlinEncoded shouldBe jvmEncoded + println(kotlinEncoded.encodeToString(Base16())) + println(jvmEncoded.encodeToString(Base16())) + + kotlinEncoded.drop(7).take(228) shouldBe jvmEncoded.drop(7).take(228) + + val parsedFromKotlinCertificate = CertificateFactory.getInstance("X.509").generateCertificate(kotlinEncoded.inputStream()) + parsedFromKotlinCertificate.verify(keyPair.public) + } + +}) From 749937a7802f14b332ff25278ef7db113568259e Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Wed, 20 Sep 2023 21:59:22 +0200 Subject: [PATCH 11/14] Debug instant serialization errors --- .../asitplus/wallet/lib/jws/X509Certificate.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt index 448b362d9..4f0158782 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt @@ -91,13 +91,13 @@ private fun Instant.encodeToDer(): ByteArray { val matchResult = Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9]+)Z") .matchEntire(toString()) - ?: throw IllegalArgumentException("instant serialization failed") - val year = matchResult.groups[1]?.value ?: throw IllegalArgumentException("instant serialization failed") - val month = matchResult.groups[2]?.value ?: throw IllegalArgumentException("instant serialization failed") - val day = matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization failed") - val hour = matchResult.groups[4]?.value ?: throw IllegalArgumentException("instant serialization failed") - val minute = matchResult.groups[5]?.value ?: throw IllegalArgumentException("instant serialization failed") - val seconds = matchResult.groups[6]?.value ?: throw IllegalArgumentException("instant serialization failed") + ?: throw IllegalArgumentException("instant serialization failed: $this") + val year = matchResult.groups[1]?.value ?: throw IllegalArgumentException("instant serialization year failed: $this") + val month = matchResult.groups[2]?.value ?: throw IllegalArgumentException("instant serialization month failed: $this") + val day = matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization day failed: $this") + val hour = matchResult.groups[4]?.value ?: throw IllegalArgumentException("instant serialization hour failed: $this") + val minute = matchResult.groups[5]?.value ?: throw IllegalArgumentException("instant serialization minute failed: $this") + val seconds = matchResult.groups[6]?.value ?: throw IllegalArgumentException("instant serialization seconds failed: $this") val string = "$year$month$day$hour$minute${seconds}Z" return string.encodeToByteArray().wrapInAsn1Tag(0x17) } @@ -130,4 +130,4 @@ private fun Int.encodeLength(): ByteArray { return byteArrayOf(0x82.toByte(), (this ushr 8).toByte(), this.toByte()) } throw IllegalArgumentException("length $this") -} \ No newline at end of file +} From 3d0cf5ce551389975d48d7598fdaa47425583afc Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 21 Sep 2023 09:02:58 +0200 Subject: [PATCH 12/14] Implement Kotlin-style DSL for DER encoding --- .../asitplus/wallet/lib/asn1/Asn1Encoder.kt | 89 +++++++++++ .../wallet/lib/jws/X509Certificate.kt | 147 +++++++----------- .../wallet/lib/agent/DefaultCryptoService.kt | 18 ++- .../wallet/lib/jws/X509CertificateJvmTest.kt | 6 +- 4 files changed, 164 insertions(+), 96 deletions(-) create mode 100644 vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt new file mode 100644 index 000000000..6ee6787ba --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt @@ -0,0 +1,89 @@ +package at.asitplus.wallet.lib.asn1 + +import at.asitplus.wallet.lib.CryptoPublicKey +import at.asitplus.wallet.lib.jws.JwsAlgorithm +import at.asitplus.wallet.lib.jws.JwsExtensions.encodeToByteArray +import at.asitplus.wallet.lib.jws.TbsCertificate +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import kotlinx.datetime.Instant + + +fun tag(tag: Int, block: () -> ByteArray): ByteArray { + val value = block() + return byteArrayOf(tag.toByte()) + value.size.encodeLength() + value +} + +fun long(block: () -> Long) = tag(0x02) { block().encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray() } + +fun int(block: () -> Int) = tag(0x02) { block().encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray() } + +fun bitString(block: () -> ByteArray) = tag(0x03) { (byteArrayOf(0x00) + block()) } + +fun oid(block: () -> String): ByteArray = tag(0x06) { block().decodeToByteArray(Base16()) } + +fun sequence(block: () -> List) = tag(0x30) { block().fold(byteArrayOf()) { acc, bytes -> acc + bytes } } + +fun set(block: () -> List) = tag(0x31) { block().fold(byteArrayOf()) { acc, bytes -> acc + bytes } } + +fun utf8String(block: () -> String) = tag(0x0c) { block().encodeToByteArray() } + +fun commonName(block: () -> String) = oid { "550403" } + utf8String { block() } + +fun subjectPublicKey(block: () -> CryptoPublicKey) = when (val value = block()) { + is CryptoPublicKey.Ec -> value.encodeToDer() +} + +fun utcTime(block: () -> Instant): ByteArray { + val value = block() + val matchResult = + Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9]+)Z") + .matchEntire(value.toString()) + ?: throw IllegalArgumentException("instant serialization failed: ${value}") + val year = + matchResult.groups[1]?.value + ?: throw IllegalArgumentException("instant serialization year failed: ${value}") + val month = + matchResult.groups[2]?.value + ?: throw IllegalArgumentException("instant serialization month failed: ${value}") + val day = + matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization day failed: ${value}") + val hour = + matchResult.groups[4]?.value + ?: throw IllegalArgumentException("instant serialization hour failed: ${value}") + val minute = + matchResult.groups[5]?.value + ?: throw IllegalArgumentException("instant serialization minute failed: ${value}") + val seconds = + matchResult.groups[6]?.value + ?: throw IllegalArgumentException("instant serialization seconds failed: ${value}") + val string = "$year$month$day$hour$minute${seconds}Z" + return tag(0x17) { string.encodeToByteArray() } +} + +fun tbsCertificate(block: () -> TbsCertificate) = block().encodeToDer() + +fun sigAlg(block: () -> JwsAlgorithm): ByteArray = when (val value = block()) { + JwsAlgorithm.ES256 -> sequence { listOf(oid { "2A8648CE3D040302" }) } + else -> throw IllegalArgumentException("sigAlg: $value") +} + +private fun CryptoPublicKey.Ec.encodeToDer(): ByteArray { + val ecKeyTag = oid { "2A8648CE3D0201" } + val ecEncryptionNullTag = oid { "2A8648CE3D030107" } + val content = bitString { (byteArrayOf(0x04.toByte()) + x + y) } + return sequence { listOf(sequence { listOf(ecKeyTag, ecEncryptionNullTag) }, content) } +} + +private fun Int.encodeLength(): ByteArray { + if (this < 128) { + return byteArrayOf(this.toByte()) + } + if (this < 0x100) { + return byteArrayOf(0x81.toByte(), this.toByte()) + } + if (this < 0x8000) { + return byteArrayOf(0x82.toByte(), (this ushr 8).toByte(), this.toByte()) + } + throw IllegalArgumentException("length $this") +} diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt index 4f0158782..b01da3d06 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt @@ -1,42 +1,80 @@ package at.asitplus.wallet.lib.jws import at.asitplus.wallet.lib.CryptoPublicKey -import at.asitplus.wallet.lib.jws.JwsExtensions.encodeToByteArray -import io.matthewnelson.encoding.base16.Base16 -import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import at.asitplus.wallet.lib.asn1.bitString +import at.asitplus.wallet.lib.asn1.commonName +import at.asitplus.wallet.lib.asn1.int +import at.asitplus.wallet.lib.asn1.long +import at.asitplus.wallet.lib.asn1.sequence +import at.asitplus.wallet.lib.asn1.set +import at.asitplus.wallet.lib.asn1.sigAlg +import at.asitplus.wallet.lib.asn1.subjectPublicKey +import at.asitplus.wallet.lib.asn1.tag +import at.asitplus.wallet.lib.asn1.tbsCertificate +import at.asitplus.wallet.lib.asn1.utcTime import kotlinx.datetime.Instant +/** + * Very simple implementation of the meat of an X.509 Certificate: + * The structure that gets signed + */ data class TbsCertificate( val version: Int = 2, val serialNumber: Long, val signatureAlgorithm: JwsAlgorithm, - val issuer: String, + val issuerCommonName: String, val validFrom: Instant, val validUntil: Instant, - val subject: String, - val subjectPublicKey: CryptoPublicKey + val subjectCommonName: String, + val publicKey: CryptoPublicKey ) { - fun encodeToDer(): ByteArray { - return (version.encodeAsVersion() + - serialNumber.encodeToDer() + - signatureAlgorithm.encodeToDer() + - issuer.encodeAsCommonName() + - (validFrom.encodeToDer() + validUntil.encodeToDer()).sequence() + - subject.encodeAsCommonName() + - subjectPublicKey.encodeToDer()) - .sequence() + fun encodeToDer() = sequence { + listOf( + tag(0xA0) { + int { version } + }, + long { serialNumber }, + sigAlg { signatureAlgorithm }, + sequence { + listOf(set { + listOf(sequence { + listOf(commonName { issuerCommonName }) + }) + }) + }, + sequence { + listOf( + utcTime { validFrom }, + utcTime { validUntil } + ) + }, + sequence { + listOf(set { + listOf(sequence { + listOf(commonName { subjectCommonName }) + }) + }) + }, + subjectPublicKey { publicKey } + ) } + } +/** + * Very simple implementation of an X.509 Certificate + */ data class X509Certificate( val tbsCertificate: TbsCertificate, val signatureAlgorithm: JwsAlgorithm, val signature: ByteArray ) { - fun encodeToDer(): ByteArray { - return (tbsCertificate.encodeToDer() + - signatureAlgorithm.encodeToDer() + - signature.encodeAsBitString()).sequence() + fun encodeToDer() = sequence { + listOf( + tbsCertificate { tbsCertificate }, + sigAlg { signatureAlgorithm }, + bitString { signature } + ) } override fun equals(other: Any?): Boolean { @@ -60,74 +98,3 @@ data class X509Certificate( } } -private fun String.encodeAsCommonName(): ByteArray { - return ("550403".decodeToByteArray(Base16()).oid() + this.encodeToDer()).sequence().set().sequence() -} - -private fun Int.encodeAsVersion(): ByteArray = encodeToDer().wrapInAsn1Tag(0xA0.toByte()) - -private fun Int.encodeToDer(): ByteArray = - encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray().wrapInAsn1Tag(0x02) - -private fun Long.encodeToDer(): ByteArray = - encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray().wrapInAsn1Tag(0x02) - -private fun CryptoPublicKey.encodeToDer(): ByteArray = when (this) { - is CryptoPublicKey.Ec -> this.encodeToDer() -} - -private fun CryptoPublicKey.Ec.encodeToDer(): ByteArray { - val ecKeyTag = "2A8648CE3D0201".decodeToByteArray(Base16()).oid() - val ecEncryptionNullTag = "2A8648CE3D030107".decodeToByteArray(Base16()).oid() - val content = (byteArrayOf(0x04.toByte()) + x + y).encodeAsBitString() - return ((ecKeyTag + ecEncryptionNullTag).sequence() + content).sequence() -} - -private fun ByteArray.encodeAsBitString(): ByteArray = (byteArrayOf(0x00) + this).wrapInAsn1Tag(0x03) - -private fun String.encodeToDer(): ByteArray = this.encodeToByteArray().wrapInAsn1Tag(0x0c) - -private fun Instant.encodeToDer(): ByteArray { - val matchResult = - Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9]+)Z") - .matchEntire(toString()) - ?: throw IllegalArgumentException("instant serialization failed: $this") - val year = matchResult.groups[1]?.value ?: throw IllegalArgumentException("instant serialization year failed: $this") - val month = matchResult.groups[2]?.value ?: throw IllegalArgumentException("instant serialization month failed: $this") - val day = matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization day failed: $this") - val hour = matchResult.groups[4]?.value ?: throw IllegalArgumentException("instant serialization hour failed: $this") - val minute = matchResult.groups[5]?.value ?: throw IllegalArgumentException("instant serialization minute failed: $this") - val seconds = matchResult.groups[6]?.value ?: throw IllegalArgumentException("instant serialization seconds failed: $this") - val string = "$year$month$day$hour$minute${seconds}Z" - return string.encodeToByteArray().wrapInAsn1Tag(0x17) -} - -private fun JwsAlgorithm.encodeToDer(): ByteArray { - return when (this) { - JwsAlgorithm.ES256 -> "2A8648CE3D040302".decodeToByteArray(Base16()).oid().sequence() - else -> TODO() - } -} - -private fun ByteArray.sequence() = this.wrapInAsn1Tag(0x30) - -private fun ByteArray.set() = this.wrapInAsn1Tag(0x31) - -private fun ByteArray.oid() = this.wrapInAsn1Tag(0x06) - -private fun ByteArray.wrapInAsn1Tag(tag: Byte): ByteArray { - return byteArrayOf(tag) + this.size.encodeLength() + this -} - -private fun Int.encodeLength(): ByteArray { - if (this < 128) { - return byteArrayOf(this.toByte()) - } - if (this < 0x100) { - return byteArrayOf(0x81.toByte(), this.toByte()) - } - if (this < 0x8000) { - return byteArrayOf(0x82.toByte(), (this ushr 8).toByte(), this.toByte()) - } - throw IllegalArgumentException("length $this") -} 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 096dd25a5..c8efdb606 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 @@ -17,7 +17,6 @@ import kotlinx.cinterop.allocArrayOf import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.reinterpret -import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.plus @@ -73,9 +72,22 @@ actual class DefaultCryptoService : CryptoService { val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) val data = CFBridgingRelease(publicKeyData) as NSData this.cryptoPublicKey = CryptoPublicKey.Ec.fromAnsiX963Bytes(EcCurve.SECP_256_R_1, data.toByteArray())!! - val tbsCertificate = TbsCertificate(version = 2, serialNumber = 3, signatureAlgorithm = JwsAlgorithm.ES256, issuer = "SelfSigned", validFrom = Clock.System.now(), validUntil = Clock.System.now().plus(10, DateTimeUnit.MINUTE), subject = "SelfSigned", subjectPublicKey = cryptoPublicKey) + val tbsCertificate = TbsCertificate( + version = 2, + serialNumber = 3, + signatureAlgorithm = JwsAlgorithm.ES256, + issuerCommonName = "SelfSigned", + validFrom = Clock.System.now(), + validUntil = Clock.System.now().plus(10, DateTimeUnit.MINUTE), + subjectCommonName = "SelfSigned", + publicKey = cryptoPublicKey + ) val signature = signInt(tbsCertificate.encodeToDer()) - this.certificate = X509Certificate(tbsCertificate = tbsCertificate, signatureAlgorithm = JwsAlgorithm.ES256, signature = signature).encodeToDer() + this.certificate = X509Certificate( + tbsCertificate = tbsCertificate, + signatureAlgorithm = JwsAlgorithm.ES256, + signature = signature + ).encodeToDer() } private fun signInt(input: ByteArray): ByteArray { diff --git a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt index dec2ee0fa..4a4f346ac 100644 --- a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt +++ b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/X509CertificateJvmTest.kt @@ -66,12 +66,12 @@ class X509CertificateJvmTest : FreeSpec({ val tbsCertificate = TbsCertificate( version = 2, serialNumber = serialNumber.toLong(), - issuer = commonName, + issuerCommonName = commonName, validFrom = notBeforeDate.toInstant().toKotlinInstant(), validUntil = notAfterDate.toInstant().toKotlinInstant(), signatureAlgorithm = signatureAlgorithm, - subject = commonName, - subjectPublicKey = cryptoPublicKey + subjectCommonName = commonName, + publicKey = cryptoPublicKey ) val signed = Signature.getInstance(signatureAlgorithm.jcaName).apply { initSign(keyPair.private) From 75e9a3f657f0974db323949b93ac357896a38c6a Mon Sep 17 00:00:00 2001 From: Christian Kollmann Date: Thu, 21 Sep 2023 09:25:00 +0200 Subject: [PATCH 13/14] Fix ASN.1 encoding of kotlinx Instants --- .../asitplus/wallet/lib/asn1/Asn1Encoder.kt | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt index 6ee6787ba..87500efb6 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt @@ -36,29 +36,22 @@ fun subjectPublicKey(block: () -> CryptoPublicKey) = when (val value = block()) fun utcTime(block: () -> Instant): ByteArray { val value = block() - val matchResult = - Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\\.([0-9]+)Z") - .matchEntire(value.toString()) - ?: throw IllegalArgumentException("instant serialization failed: ${value}") - val year = - matchResult.groups[1]?.value - ?: throw IllegalArgumentException("instant serialization year failed: ${value}") - val month = - matchResult.groups[2]?.value - ?: throw IllegalArgumentException("instant serialization month failed: ${value}") - val day = - matchResult.groups[3]?.value ?: throw IllegalArgumentException("instant serialization day failed: ${value}") - val hour = - matchResult.groups[4]?.value - ?: throw IllegalArgumentException("instant serialization hour failed: ${value}") - val minute = - matchResult.groups[5]?.value - ?: throw IllegalArgumentException("instant serialization minute failed: ${value}") - val seconds = - matchResult.groups[6]?.value - ?: throw IllegalArgumentException("instant serialization seconds failed: ${value}") - val string = "$year$month$day$hour$minute${seconds}Z" - return tag(0x17) { string.encodeToByteArray() } + val matchResult = Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})") + .matchAt(value.toString(), 0) + ?: throw IllegalArgumentException("instant serialization failed: ${value}") + val year = matchResult.groups[1]?.value + ?: throw IllegalArgumentException("instant serialization year failed: ${value}") + val month = matchResult.groups[2]?.value + ?: throw IllegalArgumentException("instant serialization month failed: ${value}") + val day = matchResult.groups[3]?.value + ?: throw IllegalArgumentException("instant serialization day failed: ${value}") + val hour = matchResult.groups[4]?.value + ?: throw IllegalArgumentException("instant serialization hour failed: ${value}") + val minute = matchResult.groups[5]?.value + ?: throw IllegalArgumentException("instant serialization minute failed: ${value}") + val seconds = matchResult.groups[6]?.value + ?: throw IllegalArgumentException("instant serialization seconds failed: ${value}") + return tag(0x17) { "$year$month$day$hour$minute${seconds}Z".encodeToByteArray() } } fun tbsCertificate(block: () -> TbsCertificate) = block().encodeToDer() From d2d4fd4bce478097d8880d2376c50835aa4198c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 21 Sep 2023 15:24:13 +0200 Subject: [PATCH 14/14] Refactor ASN.1 DSL --- .../asitplus/wallet/lib/asn1/Asn1Encoder.kt | 93 +++++++++++++------ .../wallet/lib/jws/X509Certificate.kt | 69 +++++--------- 2 files changed, 90 insertions(+), 72 deletions(-) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt index 87500efb6..e0673082e 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt @@ -8,36 +8,72 @@ import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import kotlinx.datetime.Instant +class SequenceBuilder { -fun tag(tag: Int, block: () -> ByteArray): ByteArray { - val value = block() - return byteArrayOf(tag.toByte()) + value.size.encodeLength() + value -} + internal val elements = mutableListOf() + + fun long(block: () -> Long) = apply { elements += block().encodeToAsn1() } + + fun bitString(block: () -> ByteArray) = apply { elements += block().encodeToBitString() } + + fun oid(block: () -> String) = apply { elements += block().encodeToOid() } + + fun utf8String(block: () -> String) = apply { elements += asn1Tag(0x0c, block().encodeToByteArray()) } -fun long(block: () -> Long) = tag(0x02) { block().encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray() } + fun version(block: () -> Int) = apply { elements += asn1Tag(0xA0, block().encodeToAsn1()) } -fun int(block: () -> Int) = tag(0x02) { block().encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray() } + fun commonName(block: () -> String) = apply { + oid { "550403" } + utf8String { block() } + } + + fun subjectPublicKey(block: () -> CryptoPublicKey) = apply { elements += block().encodeToAsn1() } -fun bitString(block: () -> ByteArray) = tag(0x03) { (byteArrayOf(0x00) + block()) } + fun tbsCertificate(block: () -> TbsCertificate) = apply { elements += block().encodeToDer() } -fun oid(block: () -> String): ByteArray = tag(0x06) { block().decodeToByteArray(Base16()) } + fun sigAlg(block: () -> JwsAlgorithm) = apply { elements += block().encodeToAsn1() } -fun sequence(block: () -> List) = tag(0x30) { block().fold(byteArrayOf()) { acc, bytes -> acc + bytes } } + fun utcTime(block: () -> Instant) = apply { elements += block().encodeToAsn1() } -fun set(block: () -> List) = tag(0x31) { block().fold(byteArrayOf()) { acc, bytes -> acc + bytes } } + fun sequence(init: SequenceBuilder.() -> Unit) = apply { + val seq = SequenceBuilder() + seq.init() + elements += asn1Tag(0x30, seq.elements.fold(byteArrayOf()) { acc, bytes -> acc + bytes }) + } -fun utf8String(block: () -> String) = tag(0x0c) { block().encodeToByteArray() } + fun set(init: SequenceBuilder.() -> Unit) = apply { + val seq = SequenceBuilder() + seq.init() + elements += asn1Tag(0x31, seq.elements.fold(byteArrayOf()) { acc, bytes -> acc + bytes }) + } +} -fun commonName(block: () -> String) = oid { "550403" } + utf8String { block() } -fun subjectPublicKey(block: () -> CryptoPublicKey) = when (val value = block()) { - is CryptoPublicKey.Ec -> value.encodeToDer() +fun sequence(init: SequenceBuilder.() -> Unit): ByteArray { + val seq = SequenceBuilder() + seq.init() + return asn1Tag(0x30, seq.elements.fold(byteArrayOf()) { acc, bytes -> acc + bytes }) } -fun utcTime(block: () -> Instant): ByteArray { - val value = block() +private fun Int.encodeToAsn1() = asn1Tag(0x02, encodeToDer()) + +private fun Int.encodeToDer() = encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray() + +private fun Long.encodeToAsn1() = asn1Tag(0x02, encodeToDer()) + +private fun Long.encodeToDer() = encodeToByteArray().dropWhile { it == 0.toByte() }.toByteArray() + +private fun ByteArray.encodeToBitString() = asn1Tag(0x03, (byteArrayOf(0x00) + this)) + +private fun asn1Tag(tag: Int, value: ByteArray) = byteArrayOf(tag.toByte()) + value.size.encodeLength() + value + +private fun String.encodeToOid() = asn1Tag(0x06, decodeToByteArray(Base16())) + +private fun Instant.encodeToAsn1(): ByteArray { + val value = this.toString() + if (value.isEmpty()) return asn1Tag(0x17, byteArrayOf()) val matchResult = Regex("[0-9]{2}([0-9]{2})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})") - .matchAt(value.toString(), 0) + .matchAt(value, 0) ?: throw IllegalArgumentException("instant serialization failed: ${value}") val year = matchResult.groups[1]?.value ?: throw IllegalArgumentException("instant serialization year failed: ${value}") @@ -51,21 +87,22 @@ fun utcTime(block: () -> Instant): ByteArray { ?: throw IllegalArgumentException("instant serialization minute failed: ${value}") val seconds = matchResult.groups[6]?.value ?: throw IllegalArgumentException("instant serialization seconds failed: ${value}") - return tag(0x17) { "$year$month$day$hour$minute${seconds}Z".encodeToByteArray() } + return asn1Tag(0x17, "$year$month$day$hour$minute${seconds}Z".encodeToByteArray()) } -fun tbsCertificate(block: () -> TbsCertificate) = block().encodeToDer() - -fun sigAlg(block: () -> JwsAlgorithm): ByteArray = when (val value = block()) { - JwsAlgorithm.ES256 -> sequence { listOf(oid { "2A8648CE3D040302" }) } - else -> throw IllegalArgumentException("sigAlg: $value") +private fun JwsAlgorithm.encodeToAsn1() = when (this) { + JwsAlgorithm.ES256 -> sequence { oid { "2A8648CE3D040302" } } + else -> throw IllegalArgumentException("sigAlg: $this") } -private fun CryptoPublicKey.Ec.encodeToDer(): ByteArray { - val ecKeyTag = oid { "2A8648CE3D0201" } - val ecEncryptionNullTag = oid { "2A8648CE3D030107" } - val content = bitString { (byteArrayOf(0x04.toByte()) + x + y) } - return sequence { listOf(sequence { listOf(ecKeyTag, ecEncryptionNullTag) }, content) } +private fun CryptoPublicKey.encodeToAsn1() = when (this) { + is CryptoPublicKey.Ec -> sequence { + sequence { + oid { "2A8648CE3D0201" } + oid { "2A8648CE3D030107" } + } + bitString { (byteArrayOf(0x04.toByte()) + x + y) } + } } private fun Int.encodeLength(): ByteArray { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt index b01da3d06..c88a424d5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt @@ -1,17 +1,7 @@ package at.asitplus.wallet.lib.jws import at.asitplus.wallet.lib.CryptoPublicKey -import at.asitplus.wallet.lib.asn1.bitString -import at.asitplus.wallet.lib.asn1.commonName -import at.asitplus.wallet.lib.asn1.int -import at.asitplus.wallet.lib.asn1.long import at.asitplus.wallet.lib.asn1.sequence -import at.asitplus.wallet.lib.asn1.set -import at.asitplus.wallet.lib.asn1.sigAlg -import at.asitplus.wallet.lib.asn1.subjectPublicKey -import at.asitplus.wallet.lib.asn1.tag -import at.asitplus.wallet.lib.asn1.tbsCertificate -import at.asitplus.wallet.lib.asn1.utcTime import kotlinx.datetime.Instant /** @@ -29,36 +19,29 @@ data class TbsCertificate( val publicKey: CryptoPublicKey ) { fun encodeToDer() = sequence { - listOf( - tag(0xA0) { - int { version } - }, - long { serialNumber }, - sigAlg { signatureAlgorithm }, - sequence { - listOf(set { - listOf(sequence { - listOf(commonName { issuerCommonName }) - }) - }) - }, - sequence { - listOf( - utcTime { validFrom }, - utcTime { validUntil } - ) - }, - sequence { - listOf(set { - listOf(sequence { - listOf(commonName { subjectCommonName }) - }) - }) - }, - subjectPublicKey { publicKey } - ) + version { version } + long { serialNumber } + sigAlg { signatureAlgorithm } + sequence { + set { + sequence { + commonName { issuerCommonName } + } + } + } + sequence { + utcTime { validFrom } + utcTime { validUntil } + } + sequence { + set { + sequence { + commonName { subjectCommonName } + } + } + } + subjectPublicKey { publicKey } } - } /** @@ -70,11 +53,9 @@ data class X509Certificate( val signature: ByteArray ) { fun encodeToDer() = sequence { - listOf( - tbsCertificate { tbsCertificate }, - sigAlg { signatureAlgorithm }, - bitString { signature } - ) + tbsCertificate { tbsCertificate } + sigAlg { signatureAlgorithm } + bitString { signature } } override fun equals(other: Any?): Boolean {