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 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 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..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,16 +1,13 @@ 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.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 /** @@ -33,7 +30,7 @@ data class JwmAttachment( fun decodeString(): String? { if (data.base64 != null) - return data.base64.decodeToByteArrayOrNull(Base64())?.decodeToString() + return data.base64.decodeToByteArrayOrNull(Base64Strict)?.decodeToString() if (data.jws != null) return data.jws return null @@ -42,7 +39,7 @@ data class JwmAttachment( fun decodeBinary(): ByteArray? { if (data.base64 != null) - return data.base64.decodeToByteArrayOrNull(Base64()) + return data.base64.decodeToByteArrayOrNull(Base64Strict) return null .also { Napier.w("Could not binary decode JWM attachment") } } @@ -60,7 +57,7 @@ data class JwmAttachment( id = uuid4().toString(), mediaType = "application/base64", data = JwmAttachmentData( - base64 = data.encodeToByteArray().encodeToString(Base64()) + base64 = data.encodeToByteArray().encodeToString(Base64Strict) ) ) @@ -68,7 +65,7 @@ data class JwmAttachment( id = uuid4().toString(), mediaType = "application/base64", data = JwmAttachmentData( - base64 = data.encodeToString(Base64()) + base64 = data.encodeToString(Base64Strict) ) ) @@ -78,7 +75,7 @@ data class JwmAttachment( filename = filename, parent = parent, data = JwmAttachmentData( - base64 = data.encodeToString(Base64()) + base64 = data.encodeToString(Base64Strict) ) ) 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-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-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/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerService.kt index 157c88e0a..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,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.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 @@ -22,8 +23,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.matthewnelson.encoding.base64.Base64 +import io.ktor.http.URLBuilder 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(Base64UrlStrict) ) is Issuer.IssuedCredential.Vc -> CredentialResponseParameters( 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-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-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() - -} 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..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 @@ -28,12 +29,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 @@ -221,7 +219,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(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 04a0742ee..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 @@ -24,10 +25,7 @@ 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 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.serialization.cbor.ByteStringWrapper @@ -90,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(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") @@ -190,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())) + 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 +203,65 @@ 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 +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()).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 +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())) + 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/asn1/Asn1Encoder.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt new file mode 100644 index 000000000..e0673082e --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/asn1/Asn1Encoder.kt @@ -0,0 +1,119 @@ +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 + +class SequenceBuilder { + + 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 version(block: () -> Int) = apply { elements += asn1Tag(0xA0, block().encodeToAsn1()) } + + fun commonName(block: () -> String) = apply { + oid { "550403" } + utf8String { block() } + } + + fun subjectPublicKey(block: () -> CryptoPublicKey) = apply { elements += block().encodeToAsn1() } + + fun tbsCertificate(block: () -> TbsCertificate) = apply { elements += block().encodeToDer() } + + fun sigAlg(block: () -> JwsAlgorithm) = apply { elements += block().encodeToAsn1() } + + fun utcTime(block: () -> Instant) = apply { elements += block().encodeToAsn1() } + + fun sequence(init: SequenceBuilder.() -> Unit) = apply { + val seq = SequenceBuilder() + seq.init() + elements += asn1Tag(0x30, seq.elements.fold(byteArrayOf()) { acc, bytes -> acc + bytes }) + } + + fun set(init: SequenceBuilder.() -> Unit) = apply { + val seq = SequenceBuilder() + seq.init() + elements += asn1Tag(0x31, seq.elements.fold(byteArrayOf()) { acc, bytes -> acc + bytes }) + } +} + + +fun sequence(init: SequenceBuilder.() -> Unit): ByteArray { + val seq = SequenceBuilder() + seq.init() + return asn1Tag(0x30, seq.elements.fold(byteArrayOf()) { acc, bytes -> acc + bytes }) +} + +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, 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 asn1Tag(0x17, "$year$month$day$hour$minute${seconds}Z".encodeToByteArray()) +} + +private fun JwsAlgorithm.encodeToAsn1() = when (this) { + JwsAlgorithm.ES256 -> sequence { oid { "2A8648CE3D040302" } } + else -> throw IllegalArgumentException("sigAlg: $this") +} + +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 { + 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/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/data/Json.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/Json.kt index d8b0fefbf..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 @@ -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,18 @@ val jsonSerializer by lazy { } } } + +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/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..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,8 +1,6 @@ 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 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 @@ -18,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(Base64Strict)) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64()) ?: 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 7510394ac..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,7 +1,6 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.component.base64.decodeBase64ToArray -import io.matthewnelson.encoding.base64.Base64 +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 @@ -17,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(Base64UrlStrict)) } override fun deserialize(decoder: Decoder): ByteArray { - return decoder.decodeString().decodeToByteArrayOrNull(Base64()) ?: 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 bdfb19b38..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())}, y=${ - y?.encodeToString(Base64()) - })" + 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 ea9ec61ad..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,7 +1,8 @@ package at.asitplus.wallet.lib.jws +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 @@ -22,11 +23,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(Base64UrlStrict) + + ".${encryptedKey?.encodeToString(Base64UrlStrict) ?: ""}" + + ".${iv.encodeToString(Base64UrlStrict)}" + + ".${ciphertext.encodeToString(Base64UrlStrict)}" + + ".${authTag.encodeToString(Base64UrlStrict)}" } 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(Base64Strict) ?: 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(Base64Strict) + val iv = stringList[2].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64()) + val ciphertext = stringList[3].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWE: $it") } - val authTag = stringList[4].decodeToByteArrayOrNull(Base64()) + 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/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/JwsService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt index 54b006761..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,11 +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.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 @@ -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(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(Base64 { encodeToUrlSafe = true; padEncoded = false }) + 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(Base64 { encodeToUrlSafe = true; padEncoded = false }) + 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 8d89787a1..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.Base64Strict +import at.asitplus.wallet.lib.data.Base64UrlStrict 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 @@ -16,11 +16,7 @@ data class JwsSigned( val plainSignatureInput: String, ) { fun serialize(): String { - return "${plainSignatureInput}.${ - signature.encodeToString(Base64 { - encodeToUrlSafe = true; padEncoded = false - }) - }" + return "${plainSignatureInput}.${signature.encodeToString(Base64UrlStrict)}" } override fun equals(other: Any?): Boolean { @@ -47,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(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()) + val payload = stringList[1].decodeToByteArrayOrNull(Base64Strict) ?: return null.also { Napier.w("Could not parse JWS: $it") } - val signature = stringList[2].decodeToByteArrayOrNull(Base64()) + 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 5793f52c8..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,8 +1,7 @@ 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 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 @@ -44,11 +43,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(Base64Strict)}" private fun multibaseDecode(it: String?) = if (it != null && it.startsWith("m")) { - it.removePrefix("m").decodeToByteArrayOrNull(Base64()) + it.removePrefix("m").decodeToByteArrayOrNull(Base64Strict) } else null // 0x1200 would be with compression, so we'll use 0x1290 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..c88a424d5 --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/X509Certificate.kt @@ -0,0 +1,81 @@ +package at.asitplus.wallet.lib.jws + +import at.asitplus.wallet.lib.CryptoPublicKey +import at.asitplus.wallet.lib.asn1.sequence +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 issuerCommonName: String, + val validFrom: Instant, + val validUntil: Instant, + val subjectCommonName: String, + val publicKey: CryptoPublicKey +) { + fun encodeToDer() = sequence { + version { version } + long { serialNumber } + sigAlg { signatureAlgorithm } + sequence { + set { + sequence { + commonName { issuerCommonName } + } + } + } + sequence { + utcTime { validFrom } + utcTime { validUntil } + } + sequence { + set { + sequence { + 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() = sequence { + tbsCertificate { tbsCertificate } + sigAlg { signatureAlgorithm } + bitString { signature } + } + + 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 + } +} + 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/agent/ValidatorVcTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt index 83b84b87a..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 @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.AtomicAttribute2023 +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 @@ -17,8 +18,7 @@ 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.core.Encoder.Companion.encodeToString import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.hours @@ -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(), @@ -400,8 +419,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(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/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))}") } } 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..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,14 @@ package at.asitplus.wallet.lib.jws -import io.matthewnelson.component.base64.Base64 -import io.matthewnelson.component.base64.encodeBase64 + +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.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(Base64Strict)}"""" + serialized shouldContain """"${second.encodeToString(Base64Strict)}"""" serialized shouldContain """"$kid"""" } @@ -39,8 +40,12 @@ 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(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 84cdcc89f..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 @@ -5,14 +5,11 @@ 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 -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.data.Base64Strict +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 import kotlinx.cinterop.MemScope @@ -20,6 +17,9 @@ import kotlinx.cinterop.allocArrayOf import kotlinx.cinterop.get import kotlinx.cinterop.memScoped import kotlinx.cinterop.reinterpret +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.plus import platform.CoreFoundation.CFDataRef import platform.CoreFoundation.CFDictionaryCreateMutable import platform.Foundation.CFBridgingRelease @@ -27,8 +27,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 @@ -60,7 +60,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 { @@ -72,19 +72,38 @@ 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, + 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() } - 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, @@ -95,7 +114,7 @@ actual class DefaultCryptoService : CryptoService { return KmmResult.success( AuthenticatedCiphertext( input.reversedArray(), - "authtag-${key.encodeBase64()}".encodeToByteArray() + "authtag-${key.encodeToString(Base64Strict)}".encodeToByteArray() ) ) } @@ -108,7 +127,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(Base64Strict)}".encodeToByteArray())) KmmResult.success(input.reversedArray()) else KmmResult.failure(IllegalArgumentException()) @@ -188,6 +207,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) 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..4a4f346ac --- /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(), + issuerCommonName = commonName, + validFrom = notBeforeDate.toInstant().toKotlinInstant(), + validUntil = notAfterDate.toInstant().toKotlinInstant(), + signatureAlgorithm = signatureAlgorithm, + subjectCommonName = commonName, + publicKey = 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) + } + +})