diff --git a/.gitignore b/.gitignore
index a1f44262..b049cf5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,12 @@
*/generated
.gradle
+demoapp/build
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.kotlin
-provider/src/androidInstrumentedTest/kotlin/generated
+*/src/androidInstrumentedTest/kotlin/generated
### IntelliJ IDEA ###
.idea
@@ -40,6 +41,21 @@ bin/
### Mac OS ###
.DS_Store
+.Trashes
+*.swp
+*~.nib
+DerivedData/
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+*.xccheckout
+xcuserdata/
+*.moved-aside
### Gradle ###
local.properties
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6035854..590dff04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,13 @@
## 3.0
-### NEXT
+### 3.7.0 (Supreme 0.2.0)
+* Implement supreme signing capabilities
+* Introduce Attestation Data Structure
+* Dependency Updates:
+ * Kotlin 2.0.20
+ * kotlinx.serialization 1.7.2 stable (bye, bye unofficial snapshot dependency!)
+ * kotlinx-datetime 0.6.1
### 3.6.1
* Externalise `UVarInt` to multibase
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index fdf2cdc6..d0c1f80c 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -1,6 +1,6 @@
# Development
-Development happens in branch [development](https://github.com/a-sit-plus/kmp-crypto/tree/development). The main branch always tracks the latest release.
+Development happens in branch [development](https://github.com/a-sit-plus/signum/tree/development). The main branch always tracks the latest release.
Hence, create PRs against `development`. Use dedicated `release/x.y.z` branches to prepare releases and create release PRs against `main`, which will then be merged back into `development`.
**Clone recursively, since we depend on a forked swift-klib plugin which is includes ad a git submodule"
@@ -53,4 +53,4 @@ To publish locally for testing, one can skip the signing tasks:
## Creating a new release
-Create a release branch and do the usual commits, i.e. setting the version number and so on. Push it to Github. Run the workflow "Build iOS Framework", and attach the artefacts to the release info page on GitHub. Use the link from there to update the [Swift Package](https://github.com/a-sit-plus/swift-package-kmp-crypto), modifying `Package.swift` and entering the URLs. The checksum is the output of `sha256sum *framework.zip`.
+Create a release branch and do the usual commits, i.e. setting the version number and so on. Push it to Github. Run the workflow "Build iOS Framework", and attach the artefacts to the release info page on GitHub. Use the link from there to update the [Swift Package](https://github.com/a-sit-plus/swift-package-signum), modifying `Package.swift` and entering the URLs. The checksum is the output of `sha256sum *framework.zip`.
diff --git a/README.md b/README.md
index e06584cd..a81882f0 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ This last bit means that
**you can work with X509 Certificates, public keys, CSRs and arbitrary ASN.1 structures on iOS.**
The very first bit means that you can verify signatures on the JVM, Android and on iOS.
-**Do check out the full API docs [here](https://a-sit-plus.github.io/kmp-crypto/)**!
+**Do check out the full API docs [here](https://a-sit-plus.github.io/signum/)**!
## Usage
@@ -99,6 +99,88 @@ material._
+### Signature Creation
+
+To create a signature, obtain a `Signer` instance.
+You can do this using `Signer.Ephemeral` to create a signer for a throwaway keypair:
+```kotlin
+val signer = Signer.Ephemeral {}.getOrThrow()
+val plaintext = "You have this.".encodeToByteArray()
+val signature = signer.sign(plaintext).signature
+println("Signed using ${signer.signatureAlgorithm}: $signature")
+```
+
+If you want to create multiple signatures using the same ephemeral key, you can obtain an `EphemeralKey` instance, then create signers from it:
+```kotlin
+val key = EphemeralKey { rsa {} }.getOrThrow()
+val sha256Signer = key.getSigner { rsa { digest = Digests.SHA256 } }.getOrThrow()
+val sha384Signer = key.getSigner { rsa { digest = Digests.SHA384 } }.getOrThrow()
+```
+
+The instances can be configured using the configuration DSL.
+Any unspecified parameters use sensible, secure defaults.
+
+#### Platform Signers
+
+On Android and iOS, signers using the systems' secure key storage can be retrieved.
+To do this, use `PlatformSigningProvider` (in common code), or interact with `AndroidKeystoreProvider`/`IosKeychainProvider` (in platform-specific code).
+
+New keys can be created using `createSigningKey(alias: String) { /* configuration */ }`,
+and signers for existing keys can be retrieved using `getSignerForKey(alias: String) { /* configuration */ }`.
+
+For example, creating an elliptic-curve key over P256, stored in secure hardware, and with key attestation using a random challenge provided by your server, might be done like this:
+```kotlin
+val serverChallenge: ByteArray = TODO("This was unpredictably chosen by your server.")
+PlatformSigningProvider.createSigningKey(alias = "Swordfish") {
+ ec {
+ // you don't even need to specify the curve (P256 is the default) but we'll do it for demonstration purposes
+ curve = ECCurve.SECP_256_R_1
+ // you could specify the supported digests explicity - if you do not, the curve's native digest (for P256, this is SHA256) is supported
+ }
+ // see https://a-sit-plus.github.io/signum/supreme/at.asitplus.signum.supreme.sign/-platform-signing-key-configuration-base/-secure-hardware-configuration/index.html
+ hardware {
+ // you could use PREFERRED if you want the operation to succeed (without hardware backing) on devices that do not support it
+ backing = REQUIRED
+ attestation { challenge = serverChallenge }
+ protection {
+ timeout = 5.seconds
+ factors {
+ biometry = true
+ deviceLock = false
+ }
+ }
+ }
+}
+```
+
+If this operation succeeds, it returns a `Signer`. The same `Signer` could later be retrieved using `PlatformSigningProvider.getSignerForKey(alias: String)`.
+
+When you use this `Signer` to sign data, the user would be prompted to authorize the signature using an enrolled fingerprint, because that's what you specified when creating the key.
+You can configure the authentication prompt:
+```kotlin
+val plaintext = "A message".encodeToByteArray()
+val signature = signer.sign(plaintext) {
+ unlockPrompt {
+ message = "Signing a message to Bobby"
+ }
+}.signature
+```
+... but you cannot change the fact that you configured this key to need biometry. Consider this when creating your keys.
+
+On the JVM, no native secure hardware storage is available.
+File-based keystores can be accessed using [`JKSProvider { file { /* ... */ } }`](https://a-sit-plus.github.io/signum/supreme/at.asitplus.signum.supreme.os/-j-k-s-provider/.index.html).
+Other keystores can be accessed using `JKSProvider { withBackingObject{ /* ... */ } }` or `JksProvider { customAccessor{ /* ... */ } }`.
+
+#### Key Attestation
+
+The Android KeyStore offers key attestation certificates for hardware-backed keys.
+These certificates are exposed by the signer's `.attestation` property.
+
+For iOS, Apple does not provide this capability.
+We instead piggy-back onto iOS App Attestation to provide a home-brew "key attestation" scheme.
+The guarantees are different: you are trusting the OS, not the actual secure hardware; and you are trusting that our library properly interfaces with the OS.
+Attestation types are serializable for transfer, and correspond to those in Indispensable's attestation module.
+
### Signature Verification
To verify a signature, obtain a `Verifier` instance using `verifierFor(k: PublicKey)`, either directly on a `SignatureAlgorithm`, or on one of the specialized algorithms (`X509SignatureAlgorithm`, `CoseAlgorithm`, ...).
@@ -108,7 +190,7 @@ As an example, here's how to verify a basic signature using a public key:
```kotlin
val publicKey: CryptoPublicKey.EC = TODO("You have this and trust it.")
val plaintext = "You want to trust this.".encodeToByteArray()
-val signature = TODO("This was sent alongside the plaintext.")
+val signature: CryptoSignature = TODO("This was sent alongside the plaintext.")
val verifier = SignatureAlgorithm.ECDSAwithSHA256.verifierFor(publicKey).getOrThrow()
val isValid = verifier.verify(plaintext, signature).isSuccess
println("Looks good? $isValid")
diff --git a/build.gradle.kts b/build.gradle.kts
index 96223ddd..e7270272 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,8 @@
import org.jetbrains.dokka.gradle.DokkaMultiModuleTask
plugins {
- id("at.asitplus.gradle.conventions") version "2.0.0+20240725"
- id("com.android.library") version "8.2.0" apply (false)
+ id("at.asitplus.gradle.conventions") version "2.0.20+20240829"
+ id("com.android.library") version "8.2.2" apply (false)
}
group = "at.asitplus.signum"
diff --git a/demoapp/DEVELOPMENT.md b/demoapp/DEVELOPMENT.md
new file mode 100644
index 00000000..fa22a5e2
--- /dev/null
+++ b/demoapp/DEVELOPMENT.md
@@ -0,0 +1,5 @@
+**REQUIRES a MacOS Host to build all modules**
+
+* recursively clone this repo
+* set `sdk.dir=/absulute/path/to/Android/sdk` inside `signum/local.properties`
+* import the this project into Android studio
\ No newline at end of file
diff --git a/demoapp/README.MD b/demoapp/README.MD
new file mode 100644
index 00000000..7b840d1d
--- /dev/null
+++ b/demoapp/README.MD
@@ -0,0 +1,33 @@
+# Supreme Multiplatform (JVM, Android, iOS) Demo App
+
+
+![img.png](img.png)
+
+This app showcases the _Supreme_ KMP Crypto provider on Android and on iOS. Demoing the JVM target would require additional configuration due to limitations of Kotlin.
+It was decided to avoid this clutter for the demo app, since the Supreme test suite already showcases the JVM provider usage.
+
+It is possible to generate key pairs, sign data, and verify the signature.
+
+Generation of attestation statements is also supported, although on iOS, only P-256 keys can be attested due to platform constreaints.
+The default JVM provider does not natively support the creation of attestation statements.
+
+## Before running!
+ - check your system with [KDoctor](https://github.com/Kotlin/kdoctor)
+ - install JDK 17 on your machine
+ - add `local.properties` file to the project root and set a path to Android SDK there
+
+### Android
+To run the application on android device/emulator:
+ - open project in Android Studio and run imported android run configuration
+
+To build the application bundle:
+ - run `./gradlew :composeApp:assembleDebug`
+ - find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk`
+
+### iOS
+To run the application on iPhone device/simulator:
+ - Open `iosApp/iosApp.xcproject` in Xcode and run standard configuration
+ - Or use [Kotlin Multiplatform Mobile plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) for Android Studio
+
+
+
diff --git a/demoapp/build.gradle.kts b/demoapp/build.gradle.kts
new file mode 100644
index 00000000..b235b462
--- /dev/null
+++ b/demoapp/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ alias(libs.plugins.multiplatform).apply(false)
+ alias(libs.plugins.compose).apply(false)
+ alias(libs.plugins.android.application).apply(false)
+ alias(libs.plugins.buildConfig).apply(false)
+}
+
+allprojects {
+ repositories {
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
+ mavenCentral()
+ google()
+ }
+}
\ No newline at end of file
diff --git a/demoapp/composeApp/build.gradle.kts b/demoapp/composeApp/build.gradle.kts
new file mode 100644
index 00000000..c980810f
--- /dev/null
+++ b/demoapp/composeApp/build.gradle.kts
@@ -0,0 +1,113 @@
+import com.android.build.api.dsl.Packaging
+
+plugins {
+ alias(libs.plugins.multiplatform)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.compose.runtime)
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.buildConfig)
+}
+
+kotlin {
+ jvm()
+ jvmToolchain(17)
+ androidTarget {
+ compilations.all {
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ }
+ }
+
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "ComposeApp"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ all {
+ languageSettings {
+ optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
+ }
+ }
+ commonMain.dependencies {
+ implementation("at.asitplus.signum:supreme:+") {
+ isChanging = true
+ }
+ implementation(compose.runtime)
+ implementation(compose.material3)
+ implementation(compose.materialIconsExtended)
+ @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+ implementation(compose.components.resources)
+ implementation(libs.voyager.navigator)
+ implementation(libs.composeImageLoader)
+ implementation(libs.napier)
+ implementation(libs.kotlinx.coroutines.core)
+ }
+
+ commonTest.dependencies {
+ implementation(kotlin("test"))
+ }
+
+ androidMain.dependencies {
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.activityCompose)
+ implementation(libs.compose.uitooling)
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.androidx.biometric)
+ }
+
+
+ jvmMain.dependencies {
+ implementation(compose.desktop.currentOs)
+ }
+
+ }
+}
+
+android {
+ namespace = "at.asitplus.cryptotest"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 30
+
+ applicationId = "at.asitplus.cryptotest.androidApp"
+ versionCode = 1
+ versionName = "1.0.0"
+ }
+ sourceSets["main"].apply {
+ manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ res.srcDirs("src/androidMain/resources")
+ resources.srcDirs("src/commonMain/resources")
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ buildFeatures {
+ compose = true
+ }
+
+ packaging {
+ resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
+ }
+}
+
+
+compose.desktop {
+ application {
+ mainClass = "at.asitplus.cryptotest.MainKt"
+ }
+}
+buildConfig {
+ // BuildConfig configuration here.
+ // https://github.com/gmazzo/gradle-buildconfig-plugin#usage-in-kts
+}
+
diff --git a/demoapp/composeApp/src/androidMain/AndroidManifest.xml b/demoapp/composeApp/src/androidMain/AndroidManifest.xml
new file mode 100644
index 00000000..9a3c04ef
--- /dev/null
+++ b/demoapp/composeApp/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demoapp/composeApp/src/androidMain/kotlin/at/asitplus/cryptotest/App.android.kt b/demoapp/composeApp/src/androidMain/kotlin/at/asitplus/cryptotest/App.android.kt
new file mode 100644
index 00000000..ed9ea44a
--- /dev/null
+++ b/demoapp/composeApp/src/androidMain/kotlin/at/asitplus/cryptotest/App.android.kt
@@ -0,0 +1,21 @@
+package at.asitplus.cryptotest
+
+import android.app.Application
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.fragment.app.FragmentActivity
+import at.asitplus.signum.supreme.os.PlatformSigningProvider
+import at.asitplus.signum.supreme.os.SigningProvider
+
+actual val Provider: SigningProvider = PlatformSigningProvider
+
+class AndroidApp : Application()
+
+class AppActivity : FragmentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ App()
+ }
+ }
+}
diff --git a/demoapp/composeApp/src/androidMain/kotlin/at/asitplus/cryptotest/theme/Theme.android.kt b/demoapp/composeApp/src/androidMain/kotlin/at/asitplus/cryptotest/theme/Theme.android.kt
new file mode 100644
index 00000000..1fc8ec90
--- /dev/null
+++ b/demoapp/composeApp/src/androidMain/kotlin/at/asitplus/cryptotest/theme/Theme.android.kt
@@ -0,0 +1,24 @@
+package at.asitplus.cryptotest.theme
+
+import android.app.Activity
+import android.graphics.Color
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+@Composable
+internal actual fun SystemAppearance(isDark: Boolean) {
+ val view = LocalView.current
+ val systemBarColor = Color.TRANSPARENT
+ LaunchedEffect(isDark) {
+ val window = (view.context as Activity).window
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ window.statusBarColor = systemBarColor
+ window.navigationBarColor = systemBarColor
+ WindowCompat.getInsetsController(window, window.decorView).apply {
+ isAppearanceLightStatusBars = isDark
+ isAppearanceLightNavigationBars = isDark
+ }
+ }
+}
\ No newline at end of file
diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt
new file mode 100644
index 00000000..bd831df8
--- /dev/null
+++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt
@@ -0,0 +1,538 @@
+package at.asitplus.cryptotest
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DarkMode
+import androidx.compose.material.icons.filled.LightMode
+import androidx.compose.material3.Button
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import at.asitplus.KmmResult
+import at.asitplus.signum.indispensable.CryptoSignature
+import at.asitplus.signum.indispensable.ECCurve
+import at.asitplus.signum.indispensable.RSAPadding
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm
+import at.asitplus.signum.indispensable.X509SignatureAlgorithm
+import at.asitplus.signum.indispensable.nativeDigest
+import at.asitplus.signum.indispensable.pki.X509Certificate
+import at.asitplus.signum.supreme.dsl.PREFERRED
+import at.asitplus.signum.supreme.sign.Signer
+import at.asitplus.signum.supreme.sign.makeVerifier
+import at.asitplus.signum.supreme.sign.verify
+import at.asitplus.cryptotest.theme.AppTheme
+import at.asitplus.cryptotest.theme.LocalThemeIsDark
+import at.asitplus.signum.supreme.asKmmResult
+import at.asitplus.signum.supreme.os.PlatformSignerConfigurationBase
+import at.asitplus.signum.supreme.os.PlatformSigningKeyConfigurationBase
+import at.asitplus.signum.supreme.os.PlatformSigningProvider
+import at.asitplus.signum.supreme.os.SignerConfiguration
+import at.asitplus.signum.supreme.os.SigningProvider
+import at.asitplus.signum.supreme.os.jsonEncoded
+import io.github.aakira.napier.DebugAntilog
+import io.github.aakira.napier.Napier
+import io.ktor.util.decodeBase64Bytes
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newSingleThreadContext
+import kotlin.random.Random
+import kotlin.reflect.KProperty
+import kotlin.time.Duration.Companion.seconds
+
+val SAMPLE_CERT_CHAIN = listOf(
+ "MIIDljCCAxygAwIBAgISBAkE/SHlMi5J8uQGoGCZBnhSMAoGCCqGSM49BAMDMDIx\n" +
+ "CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF\n" +
+ "MTAeFw0yNDAzMTMyMDQ2MjZaFw0yNDA2MTEyMDQ2MjVaMBwxGjAYBgNVBAMTEXN0\n" +
+ "YWNrb3ZlcmZsb3cuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENMSrkEQf\n" +
+ "2x8dEAh73snPfgxMIK+VYUyIIYA+NuRhhyZuL2ZV9N4ZUibe/eEad3Y8HND3Kuz/\n" +
+ "2vxFzJvR8nlKSqOCAiYwggIiMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggr\n" +
+ "BgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUeQJ7DtZq\n" +
+ "02WUcs0cMmOa/eJEuxcwHwYDVR0jBBgwFoAUWvPtK/w2wjd5uVIw6lRvz1XLLqww\n" +
+ "VQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vZTEuby5sZW5jci5v\n" +
+ "cmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9lMS5pLmxlbmNyLm9yZy8wMQYDVR0RBCow\n" +
+ "KIITKi5zdGFja292ZXJmbG93LmNvbYIRc3RhY2tvdmVyZmxvdy5jb20wEwYDVR0g\n" +
+ "BAwwCjAIBgZngQwBAgEwggECBgorBgEEAdZ5AgQCBIHzBIHwAO4AdQA7U3d1Pi25\n" +
+ "gE6LMFsG/kA7Z9hPw/THvQANLXJv4frUFwAAAY45x+icAAAEAwBGMEQCICqwZ2ic\n" +
+ "dHGogPX6/nRhsJ2AMWROA2MkZ+zZ/8dvzaCoAiBDqexmj0syXLpaCAhZ7Jjps+QN\n" +
+ "UHsHX8F/VE2eQ4fmdAB1AEiw42vapkc0D+VqAvqdMOscUgHLVt0sgdm7v6s52IRz\n" +
+ "AAABjjnH6KcAAAQDAEYwRAIgRB4bHal+3msYGbblbfHhWcVm+95f7fkEWQabASE2\n" +
+ "qycCIFJ/P1mixU1zSN6L/hZSvP8RTgUxy/xvbfrcF8giDNA/MAoGCCqGSM49BAMD\n" +
+ "A2gAMGUCMDe8nbCNF3evyvyGNxKOaScHhZ9ScGi5zeEo4ogiY6f25FV3wzfE2enB\n" +
+ "3QUOvZLJbgIxAIc//kc6UgMSKC+FNL3LM3c4avx9jaKZwUvlcOvxrSExYvnmxqrA\n" +
+ "jC2PPx8F/hF+ww==",
+ "MIICxjCCAk2gAwIBAgIRALO93/inhFu86QOgQTWzSkUwCgYIKoZIzj0EAwMwTzEL\n" +
+ "MAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNo\n" +
+ "IEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDIwHhcNMjAwOTA0MDAwMDAwWhcN\n" +
+ "MjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3MgRW5j\n" +
+ "cnlwdDELMAkGA1UEAxMCRTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQkXC2iKv0c\n" +
+ "S6Zdl3MnMayyoGli72XoprDwrEuf/xwLcA/TmC9N/A8AmzfwdAVXMpcuBe8qQyWj\n" +
+ "+240JxP2T35p0wKZXuskR5LBJJvmsSGPwSSB/GjMH2m6WPUZIvd0xhajggEIMIIB\n" +
+ "BDAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB\n" +
+ "MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFFrz7Sv8NsI3eblSMOpUb89V\n" +
+ "yy6sMB8GA1UdIwQYMBaAFHxClq7eS0g7+pL4nozPbYupcjeVMDIGCCsGAQUFBwEB\n" +
+ "BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gyLmkubGVuY3Iub3JnLzAnBgNVHR8E\n" +
+ "IDAeMBygGqAYhhZodHRwOi8veDIuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYG\n" +
+ "Z4EMAQIBMA0GCysGAQQBgt8TAQEBMAoGCCqGSM49BAMDA2cAMGQCMHt01VITjWH+\n" +
+ "Dbo/AwCd89eYhNlXLr3pD5xcSAQh8suzYHKOl9YST8pE9kLJ03uGqQIwWrGxtO3q\n" +
+ "YJkgsTgDyj2gJrjubi1K9sZmHzOa25JK1fUpE8ZwYii6I4zPPS/Lgul/",
+ "MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\n" +
+ "CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\n" +
+ "R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\n" +
+ "MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\n" +
+ "ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\n" +
+ "EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\n" +
+ "+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\n" +
+ "ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\n" +
+ "AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\n" +
+ "zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\n" +
+ "tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\n" +
+ "/q4AaOeMSQ+2b1tbFfLn"
+).map { X509Certificate.decodeFromDer(it.replace("\n", "").decodeBase64Bytes()) }
+
+/* because we also want it to work on the jvm;
+you don't need this workaround for ios/android, just use PlatformSigningProvider directly */
+expect val Provider: SigningProvider
+
+const val ALIAS = "Bartschlüssel"
+val SIGNER_CONFIG: (SignerConfiguration.()->Unit) = {
+ if (this is PlatformSignerConfigurationBase) {
+ unlockPrompt {
+ message = "We're signing a thing!"
+ cancelText = "No! Stop!"
+ }
+ }
+ rsa {
+ padding = RSAPadding.PKCS1
+ }
+}
+
+val context = newSingleThreadContext("crypto").also { Napier.base(DebugAntilog()) }
+
+private class getter(private val fn: ()->T) {
+ operator fun getValue(nothing: Nothing?, property: KProperty<*>): T = fn()
+}
+
+@OptIn(ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class)
+@Composable
+internal fun App() {
+
+ AppTheme {
+ var attestation by remember { mutableStateOf(false) }
+ var biometricAuth by remember { mutableStateOf(" Disabled") }
+ val algos = listOf(
+ X509SignatureAlgorithm.ES256,
+ X509SignatureAlgorithm.ES384,
+ X509SignatureAlgorithm.ES512,
+ X509SignatureAlgorithm.RS1,
+ X509SignatureAlgorithm.RS256,
+ X509SignatureAlgorithm.RS384,
+ X509SignatureAlgorithm.RS512)
+ var keyAlgorithm by remember { mutableStateOf(X509SignatureAlgorithm.ES256) }
+ var inputData by remember { mutableStateOf("Foo") }
+ var currentSigner by remember { mutableStateOf?>(null) }
+ val currentKey by getter { currentSigner?.mapCatching(Signer::publicKey) }
+ val currentKeyStr by getter {
+ currentKey?.fold(onSuccess = {
+ it.toString()
+ },
+ onFailure = {
+ Napier.e("Key failed", it)
+ "${it::class.simpleName ?: ""}: ${it.message}"
+ }) ?: ""
+ }
+ val currentAttestation by getter { (currentSigner?.getOrNull() as? Signer.Attestable<*>)?.attestation }
+ val currentAttestationStr by getter { currentAttestation?.jsonEncoded ?: "" }
+ val signingPossible by getter { currentKey?.isSuccess == true }
+ var signatureData by remember { mutableStateOf?>(null) }
+ val signatureDataStr by getter {
+ signatureData?.fold(onSuccess = Any::toString) {
+ Napier.e("Signature failed", it)
+ "${it::class.simpleName ?: ""}: ${it.message}"
+ } ?: ""
+ }
+ val verifyPossible by getter { signatureData?.isSuccess == true }
+ var verifyState by remember { mutableStateOf?>(null) }
+ val verifySucceededStr by getter {
+ verifyState?.fold(onSuccess = {
+ "Verify OK!"
+ }, onFailure = {
+ "${it::class.simpleName ?: ""}: ${it.message}"
+ }) ?: " "
+ }
+ var canGenerate by remember { mutableStateOf(true) }
+
+ var genTextOverride by remember { mutableStateOf(null) }
+ val genText by getter { genTextOverride ?: "Generate"}
+
+ Column(modifier = Modifier.fillMaxSize().verticalScroll(ScrollState(0), enabled = true).windowInsetsPadding(WindowInsets.safeDrawing)) {
+
+ Row(
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = "Supreme Demo",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(
+ top = 16.dp,
+ start = 16.dp,
+ end = 16.dp,
+ bottom = 0.dp
+ )
+ )
+
+ Spacer(modifier = Modifier.weight(1.0f))
+
+ var isDark by LocalThemeIsDark.current
+ IconButton(
+ onClick = { isDark = !isDark }
+ ) {
+ Icon(
+ modifier = Modifier.padding(8.dp).size(20.dp),
+ imageVector = if (isDark) Icons.Default.LightMode else Icons.Default.DarkMode,
+ contentDescription = null
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row {
+ Text(
+ "Attestation",
+ modifier = Modifier.padding(top = 11.dp)
+ )
+ Checkbox(checked = attestation,
+ modifier = Modifier.wrapContentSize(Alignment.TopStart).padding(0.dp),
+ onCheckedChange = {
+ attestation = it
+ })
+ }
+ Row {
+ Text(
+ "Biometric Auth",
+ modifier = Modifier.padding(
+ start = 0.dp,
+ top = 12.dp,
+ end = 4.dp,
+ bottom = 0.dp
+ )
+
+
+ )
+
+ var expanded by remember { mutableStateOf(false) }
+ Box(
+ modifier = Modifier.wrapContentSize(Alignment.TopStart).padding(top = 12.dp)
+ .background(MaterialTheme.colorScheme.primary)
+ ) {
+
+ Text(
+ biometricAuth,
+ modifier = Modifier.align(Alignment.BottomStart).width(78.dp)
+ .clickable(onClick = {
+ expanded = true
+
+ }),
+ color = MaterialTheme.colorScheme.onPrimary
+
+ )
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = {
+ expanded = false
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ listOf(
+ " Disabled",
+ " 0s",
+ " 10s",
+ " 20s",
+ " 60s"
+ ).forEachIndexed { _, s ->
+ DropdownMenuItem(text = { Text(text = s) },
+ onClick = {
+ expanded = false
+ biometricAuth = s
+ })
+ }
+ }
+ }
+ }
+ }
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text("Key Type", modifier = Modifier.padding(horizontal = 16.dp))
+ var expanded by remember { mutableStateOf(false) }
+ val displayedKeySize by getter { (if (expanded) " ▲ " else " ▼ ") + keyAlgorithm }
+ Box(
+ modifier = Modifier.fillMaxWidth().wrapContentSize(Alignment.TopStart)
+ .padding(horizontal = 16.dp).background(MaterialTheme.colorScheme.primary)
+ ) {
+
+ Text(
+ displayedKeySize,
+ modifier = Modifier.fillMaxWidth().align(Alignment.TopStart)
+ .clickable(onClick = {
+ expanded = true
+ }),
+ color = MaterialTheme.colorScheme.onPrimary
+
+ )
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = {
+ expanded = false
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ algos.forEachIndexed { index, s ->
+ DropdownMenuItem(text = { Text(text = s.toString()) },
+ onClick = {
+ keyAlgorithm = algos[index]
+ expanded = false
+ })
+ }
+ }
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Button(
+ enabled = canGenerate,
+ onClick = {
+ CoroutineScope(context).launch {
+ canGenerate = false
+ genTextOverride = "Creating…"
+ currentSigner = Provider.createSigningKey(ALIAS) {
+ when (val alg = keyAlgorithm.algorithm) {
+ is SignatureAlgorithm.ECDSA -> {
+ this@createSigningKey.ec {
+ curve = alg.requiredCurve ?:
+ ECCurve.entries.find { it.nativeDigest == alg.digest }!!
+ digests = setOf(alg.digest)
+ }
+ }
+ is SignatureAlgorithm.RSA -> {
+ this@createSigningKey.rsa {
+ digests = setOf(alg.digest)
+ paddings = RSAPadding.entries.toSet()
+ bits = 1024
+ }
+ }
+ else -> error("unreachable")
+ }
+
+ if (this is PlatformSigningKeyConfigurationBase) {
+ signer(SIGNER_CONFIG)
+
+ val timeout = runCatching {
+ biometricAuth.substringBefore("s").trim().toInt()
+ }.getOrNull()
+
+ if (attestation || timeout != null) {
+ hardware {
+ backing = PREFERRED
+ if (attestation) {
+ attestation {
+ challenge = Random.nextBytes(16)
+ }
+ }
+
+ if (timeout != null) {
+ protection {
+ this.timeout = timeout.seconds
+ factors {
+ biometry = true
+ deviceLock = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ verifyState = null
+
+ Napier.w { "created signing key! $currentSigner" }
+ Napier.w { "Signing possible: ${currentKey?.isSuccess}" }
+ canGenerate = true
+ genTextOverride = null
+ }
+ },
+ modifier = Modifier.padding(start = 16.dp)
+ ) {
+ Text(genText)
+ }
+
+ Button(
+ enabled = canGenerate,
+ onClick = {
+ CoroutineScope(context).launch {
+ canGenerate = false
+ genTextOverride = "Loading…"
+ Provider.getSignerForKey(ALIAS, SIGNER_CONFIG).let {
+ Napier.w { "Priv retrieved from native: $it" }
+ currentSigner = it
+ verifyState = null
+ }
+
+ //just to check
+ //loadPubKey().let { Napier.w { "PubKey retrieved from native: $it" } }
+ canGenerate = true
+ genTextOverride = null
+ }
+ },
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp)
+ ) {
+ Text("Load")
+ }
+
+ Button(
+ enabled = canGenerate,
+ onClick = {
+ CoroutineScope(context).launch {
+ canGenerate = false
+ genTextOverride = "Deleting…"
+ Provider.deleteSigningKey(ALIAS)
+ .onFailure { Napier.e("Failed to delete key", it) }
+ currentSigner = null
+ signatureData = null
+ verifyState = null
+ canGenerate = true
+ genTextOverride = null
+ }
+ },
+ modifier = Modifier.padding(end = 16.dp)
+ ) {
+ Text("Delete")
+ }
+
+ }
+ OutlinedTextField(value = currentKeyStr,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ minLines = 1,
+ maxLines = 5,
+ textStyle = TextStyle.Default.copy(fontSize = 10.sp),
+ readOnly = true, onValueChange = {}, label = { Text("Current Key") })
+
+
+ OutlinedTextField(value = inputData,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ enabled = true,
+ minLines = 1,
+ maxLines = 2,
+ textStyle = TextStyle.Default.copy(fontSize = 10.sp),
+ onValueChange = { inputData = it; verifyState = null },
+ label = { Text("Data to be signed") })
+
+ Button(
+ onClick = {
+
+ Napier.w { "input: $inputData" }
+ Napier.w { "signingKey: $currentKey" }
+ CoroutineScope(context).launch {
+ val data = inputData.encodeToByteArray()
+ currentSigner!!
+ .transform { it.sign(data).asKmmResult() }
+ .also { signatureData = it; verifyState = null }
+ }
+
+ },
+
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ enabled = signingPossible
+ ) {
+ Text("Sign")
+ }
+
+ if (signatureData != null) {
+ OutlinedTextField(value = signatureDataStr,
+ modifier = Modifier.fillMaxWidth().padding(16.dp),
+ minLines = 1,
+ textStyle = TextStyle.Default.copy(fontSize = 10.sp),
+ readOnly = true, onValueChange = {}, label = { Text("Detached Signature") })
+ }
+
+ if (verifyPossible) {
+ Button(
+ onClick = {
+ CoroutineScope(context).launch {
+ val signer = currentSigner!!.getOrThrow()
+ val data = inputData.encodeToByteArray()
+ val sig = signatureData!!.getOrThrow()
+ signer.makeVerifier()
+ .transform { it.verify(data, sig) }
+ .also { verifyState = it }
+ }
+ },
+
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ enabled = verifyPossible
+ ) {
+ Text("Verify")
+ }
+ }
+
+ if (verifyState != null) {
+ OutlinedTextField(value = verifySucceededStr,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ minLines = 1,
+ textStyle = TextStyle.Default.copy(fontSize = 10.sp),
+ readOnly = true,
+ onValueChange = {},
+ label = { Text("Verification Result") })
+ }
+
+ if (currentAttestation != null) {
+ OutlinedTextField(value = currentAttestationStr,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ minLines = 1,
+ textStyle = TextStyle.Default.copy(fontSize = 10.sp),
+ readOnly = true,
+ onValueChange = {},
+ label = { Text("Key Attestation") })
+ }
+ }
+ }
+}
diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/theme/Color.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/theme/Color.kt
new file mode 100644
index 00000000..ec42862e
--- /dev/null
+++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/theme/Color.kt
@@ -0,0 +1,71 @@
+package at.asitplus.cryptotest.theme
+
+import androidx.compose.ui.graphics.Color
+
+//generated by https://m3.material.io/theme-builder#/custom
+//Color palette was taken here: https://colorhunt.co/palettes/popular
+
+internal val md_theme_light_primary = Color(0xFF00687A)
+internal val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+internal val md_theme_light_primaryContainer = Color(0xFFABEDFF)
+internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26)
+internal val md_theme_light_secondary = Color(0xFF00696E)
+internal val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE)
+internal val md_theme_light_onSecondaryContainer = Color(0xFF002022)
+internal val md_theme_light_tertiary = Color(0xFF904D00)
+internal val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2)
+internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500)
+internal val md_theme_light_error = Color(0xFFBA1A1A)
+internal val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+internal val md_theme_light_onError = Color(0xFFFFFFFF)
+internal val md_theme_light_onErrorContainer = Color(0xFF410002)
+internal val md_theme_light_background = Color(0xFFFFFBFF)
+internal val md_theme_light_onBackground = Color(0xFF221B00)
+internal val md_theme_light_surface = Color(0xFFFFFBFF)
+internal val md_theme_light_onSurface = Color(0xFF221B00)
+internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7)
+internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B)
+internal val md_theme_light_outline = Color(0xFF70797B)
+internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0)
+internal val md_theme_light_inverseSurface = Color(0xFF3A3000)
+internal val md_theme_light_inversePrimary = Color(0xFF55D6F4)
+internal val md_theme_light_shadow = Color(0xFF000000)
+internal val md_theme_light_surfaceTint = Color(0xFF00687A)
+internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB)
+internal val md_theme_light_scrim = Color(0xFF000000)
+
+internal val md_theme_dark_primary = Color(0xFF55D6F4)
+internal val md_theme_dark_onPrimary = Color(0xFF003640)
+internal val md_theme_dark_primaryContainer = Color(0xFF004E5C)
+internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF)
+internal val md_theme_dark_secondary = Color(0xFF4CD9E2)
+internal val md_theme_dark_onSecondary = Color(0xFF00373A)
+internal val md_theme_dark_secondaryContainer = Color(0xFF004F53)
+internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE)
+internal val md_theme_dark_tertiary = Color(0xFFFFB77C)
+internal val md_theme_dark_onTertiary = Color(0xFF4D2700)
+internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900)
+internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2)
+internal val md_theme_dark_error = Color(0xFFFFB4AB)
+internal val md_theme_dark_errorContainer = Color(0xFF93000A)
+internal val md_theme_dark_onError = Color(0xFF690005)
+internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+internal val md_theme_dark_background = Color(0xFF221B00)
+internal val md_theme_dark_onBackground = Color(0xFFFFE264)
+internal val md_theme_dark_surface = Color(0xFF221B00)
+internal val md_theme_dark_onSurface = Color(0xFFFFE264)
+internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B)
+internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB)
+internal val md_theme_dark_outline = Color(0xFF899295)
+internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00)
+internal val md_theme_dark_inverseSurface = Color(0xFFFFE264)
+internal val md_theme_dark_inversePrimary = Color(0xFF00687A)
+internal val md_theme_dark_shadow = Color(0xFF000000)
+internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4)
+internal val md_theme_dark_outlineVariant = Color(0xFF3F484B)
+internal val md_theme_dark_scrim = Color(0xFF000000)
+
+
+internal val seed = Color(0xFF2C3639)
diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/theme/Theme.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/theme/Theme.kt
new file mode 100644
index 00000000..b1a69977
--- /dev/null
+++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/theme/Theme.kt
@@ -0,0 +1,129 @@
+package at.asitplus.cryptotest.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Shapes
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+private val LightColorScheme = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ outlineVariant = md_theme_dark_outlineVariant,
+ scrim = md_theme_dark_scrim,
+)
+
+private val AppShapes = Shapes(
+ extraSmall = RoundedCornerShape(2.dp),
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(8.dp),
+ large = RoundedCornerShape(16.dp),
+ extraLarge = RoundedCornerShape(32.dp)
+)
+
+private val AppTypography = Typography(
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp
+ )
+)
+
+internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) }
+
+@Composable
+internal fun AppTheme(
+ content: @Composable() () -> Unit
+) {
+ val systemIsDark = isSystemInDarkTheme()
+ val isDarkState = remember { mutableStateOf(systemIsDark) }
+ CompositionLocalProvider(
+ LocalThemeIsDark provides isDarkState
+ ) {
+ val isDark by isDarkState
+ SystemAppearance(!isDark)
+ MaterialTheme(
+ colorScheme = if (isDark) DarkColorScheme else LightColorScheme,
+ typography = AppTypography,
+ shapes = AppShapes,
+ content = {
+ Surface(content = content)
+ }
+ )
+ }
+}
+
+@Composable
+internal expect fun SystemAppearance(isDark: Boolean)
diff --git a/demoapp/composeApp/src/iosMain/kotlin/at/asitplus/cryptotest/App.ios.kt b/demoapp/composeApp/src/iosMain/kotlin/at/asitplus/cryptotest/App.ios.kt
new file mode 100644
index 00000000..e1627a17
--- /dev/null
+++ b/demoapp/composeApp/src/iosMain/kotlin/at/asitplus/cryptotest/App.ios.kt
@@ -0,0 +1,6 @@
+package at.asitplus.cryptotest
+
+import at.asitplus.signum.supreme.os.PlatformSigningProvider
+import at.asitplus.signum.supreme.os.SigningProvider
+
+actual val Provider: SigningProvider = PlatformSigningProvider
diff --git a/demoapp/composeApp/src/iosMain/kotlin/at/asitplus/cryptotest/theme/Theme.ios.kt b/demoapp/composeApp/src/iosMain/kotlin/at/asitplus/cryptotest/theme/Theme.ios.kt
new file mode 100644
index 00000000..db40f0bc
--- /dev/null
+++ b/demoapp/composeApp/src/iosMain/kotlin/at/asitplus/cryptotest/theme/Theme.ios.kt
@@ -0,0 +1,17 @@
+package at.asitplus.cryptotest.theme
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import platform.UIKit.UIApplication
+import platform.UIKit.UIStatusBarStyleDarkContent
+import platform.UIKit.UIStatusBarStyleLightContent
+import platform.UIKit.setStatusBarStyle
+
+@Composable
+internal actual fun SystemAppearance(isDark: Boolean) {
+ LaunchedEffect(isDark) {
+ UIApplication.sharedApplication.setStatusBarStyle(
+ if (isDark) UIStatusBarStyleDarkContent else UIStatusBarStyleLightContent
+ )
+ }
+}
\ No newline at end of file
diff --git a/demoapp/composeApp/src/iosMain/kotlin/main.kt b/demoapp/composeApp/src/iosMain/kotlin/main.kt
new file mode 100644
index 00000000..f8edd2ef
--- /dev/null
+++ b/demoapp/composeApp/src/iosMain/kotlin/main.kt
@@ -0,0 +1,9 @@
+import androidx.compose.ui.window.ComposeUIViewController
+import at.asitplus.cryptotest.App
+import io.github.aakira.napier.DebugAntilog
+import io.github.aakira.napier.Napier
+import platform.UIKit.UIViewController
+
+fun MainViewController(): UIViewController = ComposeUIViewController {
+ App()
+}
diff --git a/demoapp/composeApp/src/jvmMain/kotlin/at/asitplus/cryptotest/Main.kt b/demoapp/composeApp/src/jvmMain/kotlin/at/asitplus/cryptotest/Main.kt
new file mode 100644
index 00000000..0363f346
--- /dev/null
+++ b/demoapp/composeApp/src/jvmMain/kotlin/at/asitplus/cryptotest/Main.kt
@@ -0,0 +1,14 @@
+package at.asitplus.cryptotest
+
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import at.asitplus.signum.supreme.os.JKSProvider
+import at.asitplus.signum.supreme.os.SigningProvider
+
+actual val Provider: SigningProvider = JKSProvider.Ephemeral().getOrThrow()
+
+fun main() = application {
+ Window(onCloseRequest = ::exitApplication, title = "Supreme Demo") {
+ App()
+ }
+}
\ No newline at end of file
diff --git a/demoapp/composeApp/src/jvmMain/kotlin/at/asitplus/cryptotest/theme/Theme.jvm.kt b/demoapp/composeApp/src/jvmMain/kotlin/at/asitplus/cryptotest/theme/Theme.jvm.kt
new file mode 100644
index 00000000..52f19b44
--- /dev/null
+++ b/demoapp/composeApp/src/jvmMain/kotlin/at/asitplus/cryptotest/theme/Theme.jvm.kt
@@ -0,0 +1,8 @@
+package at.asitplus.cryptotest.theme
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+
+@Composable
+internal actual fun SystemAppearance(isDark: Boolean) {
+}
\ No newline at end of file
diff --git a/demoapp/gradle.properties b/demoapp/gradle.properties
new file mode 100644
index 00000000..eadd020f
--- /dev/null
+++ b/demoapp/gradle.properties
@@ -0,0 +1,16 @@
+#Gradle
+org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4096M"
+org.gradle.caching=true
+org.gradle.configuration-cache=false
+
+#Kotlin
+kotlin.code.style=official
+kotlin.js.compiler=ir
+
+#Android
+android.useAndroidX=true
+android.nonTransitiveRClass=true
+
+#Compose
+org.jetbrains.compose.experimental.uikit.enabled=true
+org.jetbrains.compose.experimental.jscanvas.enabled=true
diff --git a/demoapp/gradle/libs.versions.toml b/demoapp/gradle/libs.versions.toml
new file mode 100644
index 00000000..e315ecf6
--- /dev/null
+++ b/demoapp/gradle/libs.versions.toml
@@ -0,0 +1,35 @@
+[versions]
+
+biometric = "1.2.0-alpha05"
+kotlin = "2.0.20"
+compose = "1.6.11"
+agp = "8.2.2"
+androidx-appcompat = "1.7.0"
+androidx-activityCompose = "1.9.1"
+compose-uitooling = "1.6.8"
+voyager = "1.0.0"
+composeImageLoader = "1.7.1"
+napier = "2.7.1"
+buildConfig = "4.1.1"
+kotlinx-coroutines = "1.8.1"
+
+[libraries]
+
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
+androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
+androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
+compose-uitooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-uitooling" }
+voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
+composeImageLoader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "composeImageLoader" }
+napier = { module = "io.github.aakira:napier", version.ref = "napier" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
+
+[plugins]
+
+multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+compose-runtime = { id = "org.jetbrains.compose", version.ref = "compose" }
+android-application = { id = "com.android.application", version.ref = "agp" }
+buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildConfig" }
diff --git a/demoapp/gradle/wrapper/gradle-wrapper.jar b/demoapp/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7454180f
Binary files /dev/null and b/demoapp/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/demoapp/gradle/wrapper/gradle-wrapper.properties b/demoapp/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..39561b34
--- /dev/null
+++ b/demoapp/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/demoapp/gradlew b/demoapp/gradlew
new file mode 100755
index 00000000..43bed5da
--- /dev/null
+++ b/demoapp/gradlew
@@ -0,0 +1,186 @@
+
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: ${'$'}0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)${'$'}'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "${'$'}*"
+}
+
+die () {
+ echo
+ echo "${'$'}*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MSYS* | MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/demoapp/gradlew.bat b/demoapp/gradlew.bat
new file mode 100644
index 00000000..e34f4911
--- /dev/null
+++ b/demoapp/gradlew.bat
@@ -0,0 +1,90 @@
+
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/demoapp/img.png b/demoapp/img.png
new file mode 100644
index 00000000..8cfb4927
Binary files /dev/null and b/demoapp/img.png differ
diff --git a/demoapp/iosApp/iosApp.xcodeproj/project.pbxproj b/demoapp/iosApp/iosApp.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..fb7d63bf
--- /dev/null
+++ b/demoapp/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -0,0 +1,392 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; };
+ A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; };
+ A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ A93A953729CC810C00F8E227 /* CryptoTest App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CryptoTest App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; };
+ A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ CEC69C902BAB545100A8FEA5 /* iosApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iosApp.entitlements; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ A93A953429CC810C00F8E227 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ A93A952E29CC810C00F8E227 = {
+ isa = PBXGroup;
+ children = (
+ A93A953929CC810C00F8E227 /* iosApp */,
+ A93A953829CC810C00F8E227 /* Products */,
+ C4127409AE3703430489E7BC /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ A93A953829CC810C00F8E227 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ A93A953729CC810C00F8E227 /* CryptoTest App.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ A93A953929CC810C00F8E227 /* iosApp */ = {
+ isa = PBXGroup;
+ children = (
+ CEC69C902BAB545100A8FEA5 /* iosApp.entitlements */,
+ A93A953A29CC810C00F8E227 /* iosApp.swift */,
+ A93A953E29CC810D00F8E227 /* Assets.xcassets */,
+ A93A954029CC810D00F8E227 /* Preview Content */,
+ );
+ path = iosApp;
+ sourceTree = "";
+ };
+ A93A954029CC810D00F8E227 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ A93A954129CC810D00F8E227 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ C4127409AE3703430489E7BC /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ A93A953629CC810C00F8E227 /* iosApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */;
+ buildPhases = (
+ A9D80A052AAB5CDE006C8738 /* ShellScript */,
+ A93A953329CC810C00F8E227 /* Sources */,
+ A93A953429CC810C00F8E227 /* Frameworks */,
+ A93A953529CC810C00F8E227 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = iosApp;
+ productName = iosApp;
+ productReference = A93A953729CC810C00F8E227 /* CryptoTest App.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ A93A952F29CC810C00F8E227 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1420;
+ LastUpgradeCheck = 1420;
+ TargetAttributes = {
+ A93A953629CC810C00F8E227 = {
+ CreatedOnToolsVersion = 14.2;
+ };
+ };
+ };
+ buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = A93A952E29CC810C00F8E227;
+ productRefGroup = A93A953829CC810C00F8E227 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ A93A953629CC810C00F8E227 /* iosApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ A93A953529CC810C00F8E227 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */,
+ A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ A9D80A052AAB5CDE006C8738 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "cd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ A93A953329CC810C00F8E227 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ A93A954329CC810D00F8E227 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ A93A954429CC810D00F8E227 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ A93A954629CC810D00F8E227 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = 9CYHJNG644;
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "${inherited}",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlocking Private Keys for Signing";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "${inherited}",
+ "-framework",
+ ComposeApp,
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = at.asitplus.cryptotest.iosApp;
+ PRODUCT_NAME = "CryptoTest App";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ A93A954729CC810D00F8E227 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = 9CYHJNG644;
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = (
+ "${inherited}",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlocking Private Keys for Signing";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "${inherited}",
+ "-framework",
+ ComposeApp,
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = at.asitplus.cryptotest.iosApp;
+ PRODUCT_NAME = "CryptoTest App";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A93A954329CC810D00F8E227 /* Debug */,
+ A93A954429CC810D00F8E227 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A93A954629CC810D00F8E227 /* Debug */,
+ A93A954729CC810D00F8E227 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = A93A952F29CC810C00F8E227 /* Project object */;
+}
diff --git a/demoapp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demoapp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/demoapp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/demoapp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demoapp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/demoapp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/demoapp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/demoapp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000..eb878970
--- /dev/null
+++ b/demoapp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/demoapp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/demoapp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..13613e3e
--- /dev/null
+++ b/demoapp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/demoapp/iosApp/iosApp/Assets.xcassets/Contents.json b/demoapp/iosApp/iosApp/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/demoapp/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/demoapp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/demoapp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/demoapp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/demoapp/iosApp/iosApp/iosApp.entitlements b/demoapp/iosApp/iosApp/iosApp.entitlements
new file mode 100644
index 00000000..0c67376e
--- /dev/null
+++ b/demoapp/iosApp/iosApp/iosApp.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/demoapp/iosApp/iosApp/iosApp.swift b/demoapp/iosApp/iosApp/iosApp.swift
new file mode 100644
index 00000000..22639055
--- /dev/null
+++ b/demoapp/iosApp/iosApp/iosApp.swift
@@ -0,0 +1,32 @@
+import UIKit
+import SwiftUI
+import ComposeApp
+
+
+@main
+struct iosApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
+
+struct ContentView: View {
+ var body: some View {
+ ComposeView().ignoresSafeArea(.all)
+ }
+}
+
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
+
+ }
+
+
+
+}
diff --git a/demoapp/settings.gradle.kts b/demoapp/settings.gradle.kts
new file mode 100644
index 00000000..62d6c572
--- /dev/null
+++ b/demoapp/settings.gradle.kts
@@ -0,0 +1,36 @@
+rootProject.name = "CryptoTest-App"
+include(":composeApp")
+
+pluginManagement {
+ repositories {
+ google()
+ gradlePluginPortal()
+ mavenCentral()
+
+ //required for indispensable modules composite build
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
+ //required for indispensable modules composite build
+ maven {
+ url =
+ uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo")
+ name = "aspConventions"
+ }
+ }
+}
+
+includeBuild("..") {
+ dependencySubstitution {
+ substitute(module("at.asitplus.signum:indispensable")).using(project(":indispensable"))
+ substitute(module("at.asitplus.signum:indispensable-josef")).using(project(":indispensable-josef"))
+ substitute(module("at.asitplus.signum:indispensable-cosef")).using(project(":indispensable-cosef"))
+ substitute(module("at.asitplus.signum:supreme")).using(project(":supreme"))
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ mavenLocal()
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index 6ea3605d..e4aa60b0 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,6 +3,9 @@ kotlin.js.compiler=ir
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
artifactVersion = 3.7.0-SNAPSHOT
+supremeVersion=0.2.0-SNAPSHOT
+
+#for swift plugin
org.gradle.caching=false
org.gradle.configuration-cache=false
# This is not a well-defined property, the ASP convention plugin respects it, though
diff --git a/indispensable-cosef/build.gradle.kts b/indispensable-cosef/build.gradle.kts
index bddd3b55..f2be9fb0 100644
--- a/indispensable-cosef/build.gradle.kts
+++ b/indispensable-cosef/build.gradle.kts
@@ -25,11 +25,10 @@ kotlin {
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
}
- commonMain {
+ commonMain {
dependencies {
api(project(":indispensable"))
- //noinspection UseTomlInstead
- api("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.8.0-SNAPSHOT!!")
+ api(serialization("cbor"))
implementation(napier())
implementation(libs.multibase)
implementation(libs.bignum) //Intellij bug work-around
diff --git a/indispensable-cosef/src/jvmTest/kotlin/CoseKeySerializationTest.kt b/indispensable-cosef/src/jvmTest/kotlin/CoseKeySerializationTest.kt
index c969d31f..49451cee 100644
--- a/indispensable-cosef/src/jvmTest/kotlin/CoseKeySerializationTest.kt
+++ b/indispensable-cosef/src/jvmTest/kotlin/CoseKeySerializationTest.kt
@@ -23,19 +23,23 @@ import java.security.Security
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPublicKey
+private fun CryptoPublicKey.EC.withCompressionPreference(v: Boolean) =
+ if (v) CryptoPublicKey.EC.fromCompressed(curve, xBytes, yCompressed)
+ else CryptoPublicKey.EC.fromUncompressed(curve, xBytes, yBytes)
class CoseKeySerializationTest : FreeSpec({
Security.addProvider(BouncyCastleProvider())
"Serializing" - {
"Manual" - {
- val compressed = coseCompliantSerializer.encodeToByteArray(CryptoPublicKey.fromJcaPublicKey(
- KeyPairGenerator.getInstance("EC").apply {
- initialize(256)
- }.genKeyPair().public
- ).getOrThrow().run {
- this as CryptoPublicKey.EC
- this.copy(preferCompressedRepresentation = true)
- }.toCoseKey(CoseAlgorithm.ES256).getOrThrow()
+ val compressed = coseCompliantSerializer.encodeToByteArray(
+ CryptoPublicKey.fromJcaPublicKey(
+ KeyPairGenerator.getInstance("EC").apply {
+ initialize(256)
+ }.genKeyPair().public
+ ).getOrThrow().run {
+ this as CryptoPublicKey.EC
+ this.withCompressionPreference(true)
+ }.toCoseKey(CoseAlgorithm.ES256).getOrThrow()
)
val coseUncompressed = CryptoPublicKey.fromJcaPublicKey(
KeyPairGenerator.getInstance("EC").apply {
@@ -119,7 +123,7 @@ class CoseKeySerializationTest : FreeSpec({
.getOrThrow()
.run {
this as CryptoPublicKey.EC
- this.copy(preferCompressedRepresentation = true)
+ this.withCompressionPreference(true)
}.toCoseKey()
.getOrThrow()
diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt
index bcd3ecd8..2f0b0cd2 100644
--- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt
+++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/io/JwsCertificateSerializer.kt
@@ -1,26 +1,11 @@
package at.asitplus.signum.indispensable.josef.io
-import at.asitplus.signum.indispensable.io.Base64Strict
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import at.asitplus.signum.indispensable.io.TransformingSerializerTemplate
import at.asitplus.signum.indispensable.pki.X509Certificate
-import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
-import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-object JwsCertificateSerializer : KSerializer {
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor(serialName = "X509Certificate (JWS)", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): X509Certificate {
- return X509Certificate.decodeFromDer(decoder.decodeString().decodeToByteArray(Base64Strict))
- }
-
-
- override fun serialize(encoder: Encoder, value: X509Certificate) {
- encoder.encodeString(value.encodeToDer().encodeToString(Base64Strict))
- }
-}
+object JwsCertificateSerializer : TransformingSerializerTemplate(
+ parent = ByteArrayBase64Serializer,
+ encodeAs = X509Certificate::encodeToDer,
+ decodeAs = X509Certificate::decodeFromDer
+)
diff --git a/indispensable/build.gradle.kts b/indispensable/build.gradle.kts
index a8cda390..32879141 100644
--- a/indispensable/build.gradle.kts
+++ b/indispensable/build.gradle.kts
@@ -192,12 +192,6 @@ kotlin {
}
}
- jvmTest {
- dependencies {
- implementation("io.kotest.extensions:kotest-assertions-compiler:1.0.0")
- }
- }
-
}
}
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt
index 92ebbbe9..b74c60bd 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoPublicKey.kt
@@ -165,6 +165,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable {
* RSA Public key
*/
@Serializable
+ @ConsistentCopyVisibility
data class Rsa
@Throws(IllegalArgumentException::class)
private constructor(
@@ -224,7 +225,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable {
?: throw IllegalArgumentException("Unsupported key size $nTruncSize")
}
- override val oid = at.asitplus.signum.indispensable.asn1.KnownOIDs.rsaEncryption
+ override val oid = KnownOIDs.rsaEncryption
}
}
@@ -285,7 +286,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable {
return Rsa(Size.of(n), n, e)
}
- override val oid = at.asitplus.signum.indispensable.asn1.KnownOIDs.rsaEncryption
+ override val oid = KnownOIDs.rsaEncryption
}
}
@@ -296,6 +297,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable {
*/
@Serializable
@SerialName("EC")
+ @ConsistentCopyVisibility
data class EC private constructor(
val publicPoint: ECPoint.Normalized,
val preferCompressedRepresentation: Boolean = true
@@ -409,7 +411,7 @@ sealed class CryptoPublicKey : Asn1Encodable, Identifiable {
}
}
- override val oid = at.asitplus.signum.indispensable.asn1.KnownOIDs.ecPublicKey
+ override val oid = KnownOIDs.ecPublicKey
}
}
}
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Digest.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Digest.kt
index b66ebe44..9930957e 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Digest.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Digest.kt
@@ -5,3 +5,17 @@ import at.asitplus.signum.indispensable.asn1.KnownOIDs
import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
import at.asitplus.signum.indispensable.misc.BitLength
import at.asitplus.signum.indispensable.misc.bit
+
+enum class Digest(val outputLength: BitLength, override val oid: ObjectIdentifier) : Identifiable {
+ SHA1(160.bit, KnownOIDs.sha1),
+ SHA256(256.bit, KnownOIDs.sha_256),
+ SHA384(384.bit, KnownOIDs.sha_384),
+ SHA512(512.bit, KnownOIDs.sha_512);
+}
+
+/** A digest well-suited to operations on this curve, with output length near the curve's coordinate length. */
+val ECCurve.nativeDigest get() = when (this) {
+ ECCurve.SECP_256_R_1 -> Digest.SHA256
+ ECCurve.SECP_384_R_1 -> Digest.SHA384
+ ECCurve.SECP_521_R_1 -> Digest.SHA512
+}
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECCurve.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECCurve.kt
index 49cc2b30..0b154c0d 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECCurve.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/ECCurve.kt
@@ -29,10 +29,12 @@ enum class ECCurve(
val jwkName: String,
override val oid: ObjectIdentifier,
) : Identifiable {
-
- SECP_256_R_1("P-256", at.asitplus.signum.indispensable.asn1.KnownOIDs.prime256v1),
- SECP_384_R_1("P-384", at.asitplus.signum.indispensable.asn1.KnownOIDs.secp384r1),
- SECP_521_R_1("P-521", at.asitplus.signum.indispensable.asn1.KnownOIDs.secp521r1);
+ /** NIST curve [secp256r1](https://neuromancer.sk/std/nist/P-256) */
+ SECP_256_R_1("P-256", KnownOIDs.prime256v1),
+ /** NIST curve [secp384r1](https://neuromancer.sk/std/nist/P-384) */
+ SECP_384_R_1("P-384", KnownOIDs.secp384r1),
+ /** NIST curve [secp521r1](https://neuromancer.sk/std/nist/P-521) */
+ SECP_521_R_1("P-521", KnownOIDs.secp521r1);
val IDENTITY: ECPoint by lazy {
ECPoint.General.unsafeFromXYZ(this, coordinateCreator.ZERO, coordinateCreator.ONE, coordinateCreator.ZERO)
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt
index f896330e..1659a602 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt
@@ -1,18 +1,5 @@
package at.asitplus.signum.indispensable
-import at.asitplus.signum.indispensable.asn1.Identifiable
-import at.asitplus.signum.indispensable.asn1.KnownOIDs
-import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
-import at.asitplus.signum.indispensable.misc.BitLength
-import at.asitplus.signum.indispensable.misc.bit
-
-enum class Digest(val outputLength: BitLength, override val oid: ObjectIdentifier) : Identifiable {
- SHA1(160.bit, at.asitplus.signum.indispensable.asn1.KnownOIDs.sha1),
- SHA256(256.bit, at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_256),
- SHA384(384.bit, at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_384),
- SHA512(512.bit, at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_512);
-}
-
enum class RSAPadding {
PKCS1,
PSS;
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt
index 8e7d90bf..2778efae 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt
@@ -19,33 +19,33 @@ enum class X509SignatureAlgorithm(
) : Asn1Encodable, Identifiable, SpecializedSignatureAlgorithm {
// ECDSA with SHA-size
- ES256(at.asitplus.signum.indispensable.asn1.KnownOIDs.ecdsaWithSHA256, true),
- ES384(at.asitplus.signum.indispensable.asn1.KnownOIDs.ecdsaWithSHA384, true),
- ES512(at.asitplus.signum.indispensable.asn1.KnownOIDs.ecdsaWithSHA512, true),
+ ES256(KnownOIDs.ecdsaWithSHA256, true),
+ ES384(KnownOIDs.ecdsaWithSHA384, true),
+ ES512(KnownOIDs.ecdsaWithSHA512, true),
// HMAC-size with SHA-size
- HS256(at.asitplus.signum.indispensable.asn1.KnownOIDs.hmacWithSHA256),
- HS384(at.asitplus.signum.indispensable.asn1.KnownOIDs.hmacWithSHA384),
- HS512(at.asitplus.signum.indispensable.asn1.KnownOIDs.hmacWithSHA512),
+ HS256(KnownOIDs.hmacWithSHA256),
+ HS384(KnownOIDs.hmacWithSHA384),
+ HS512(KnownOIDs.hmacWithSHA512),
// RSASSA-PSS with SHA-size
- PS256(at.asitplus.signum.indispensable.asn1.KnownOIDs.rsaPSS),
- PS384(at.asitplus.signum.indispensable.asn1.KnownOIDs.rsaPSS),
- PS512(at.asitplus.signum.indispensable.asn1.KnownOIDs.rsaPSS),
+ PS256(KnownOIDs.rsaPSS),
+ PS384(KnownOIDs.rsaPSS),
+ PS512(KnownOIDs.rsaPSS),
// RSASSA-PKCS1-v1_5 with SHA-size
- RS256(at.asitplus.signum.indispensable.asn1.KnownOIDs.sha256WithRSAEncryption),
- RS384(at.asitplus.signum.indispensable.asn1.KnownOIDs.sha384WithRSAEncryption),
- RS512(at.asitplus.signum.indispensable.asn1.KnownOIDs.sha512WithRSAEncryption),
+ RS256(KnownOIDs.sha256WithRSAEncryption),
+ RS384(KnownOIDs.sha384WithRSAEncryption),
+ RS512(KnownOIDs.sha512WithRSAEncryption),
// RSASSA-PKCS1-v1_5 using SHA-1
- RS1(at.asitplus.signum.indispensable.asn1.KnownOIDs.sha1WithRSAEncryption);
+ RS1(KnownOIDs.sha1WithRSAEncryption);
private fun encodePSSParams(bits: Int): Asn1Sequence {
val shaOid = when (bits) {
- 256 -> at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_256
- 384 -> at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_384
- 512 -> at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_512
+ 256 -> KnownOIDs.sha_256
+ 384 -> KnownOIDs.sha_384
+ 512 -> KnownOIDs.sha_512
else -> TODO()
}
return Asn1.Sequence {
@@ -59,7 +59,7 @@ enum class X509SignatureAlgorithm(
}
+Tagged(1.toUByte()) {
+Asn1.Sequence {
- +at.asitplus.signum.indispensable.asn1.KnownOIDs.pkcs1_MGF
+ +KnownOIDs.pkcs1_MGF
+Asn1.Sequence {
+shaOid
+Null()
@@ -140,7 +140,7 @@ enum class X509SignatureAlgorithm(
val second = (seq.nextChild() as Asn1Tagged).verifyTag(1.toUByte()).single() as Asn1Sequence
val mgf = (second.nextChild() as Asn1Primitive).readOid()
- if (mgf != at.asitplus.signum.indispensable.asn1.KnownOIDs.pkcs1_MGF) throw IllegalArgumentException("Illegal OID: $mgf")
+ if (mgf != KnownOIDs.pkcs1_MGF) throw IllegalArgumentException("Illegal OID: $mgf")
val inner = second.nextChild() as Asn1Sequence
val innerHash = (inner.nextChild() as Asn1Primitive).readOid()
if (innerHash != sigAlg) throw IllegalArgumentException("HashFunction mismatch! Expected: $sigAlg, is: $innerHash")
@@ -154,9 +154,9 @@ enum class X509SignatureAlgorithm(
return sigAlg.let {
when (it) {
- at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_256 -> PS256.also { if (saltLen != 256 / 8) throw IllegalArgumentException("Non-recommended salt length used: $saltLen") }
- at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_384 -> PS384.also { if (saltLen != 384 / 8) throw IllegalArgumentException("Non-recommended salt length used: $saltLen") }
- at.asitplus.signum.indispensable.asn1.KnownOIDs.sha_512 -> PS512.also { if (saltLen != 512 / 8) throw IllegalArgumentException("Non-recommended salt length used: $saltLen") }
+ KnownOIDs.sha_256 -> PS256.also { if (saltLen != 256 / 8) throw IllegalArgumentException("Non-recommended salt length used: $saltLen") }
+ KnownOIDs.sha_384 -> PS384.also { if (saltLen != 384 / 8) throw IllegalArgumentException("Non-recommended salt length used: $saltLen") }
+ KnownOIDs.sha_512 -> PS512.also { if (saltLen != 512 / 8) throw IllegalArgumentException("Non-recommended salt length used: $saltLen") }
else -> throw IllegalArgumentException("Unsupported OID: $it")
}
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt
index bd3d867b..97673eaf 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt
@@ -1,21 +1,23 @@
package at.asitplus.signum.indispensable.io
-import at.asitplus.catching
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.pki.X509Certificate
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.base64.Base64ConfigBuilder
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.listSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-/**
- * Strict Base64 URL encode
- */
+/** Strict Base64 URL encode */
val Base64UrlStrict = Base64(config = Base64ConfigBuilder().apply {
lineBreakInterval = 0
encodeToUrlSafe = true
@@ -24,9 +26,7 @@ val Base64UrlStrict = Base64(config = Base64ConfigBuilder().apply {
}.build())
-/**
- * Strict Base64 encoder
- */
+/** Strict Base64 encoder */
val Base64Strict = Base64(config = Base64ConfigBuilder().apply {
lineBreakInterval = 0
encodeToUrlSafe = false
@@ -34,48 +34,78 @@ val Base64Strict = Base64(config = Base64ConfigBuilder().apply {
padEncoded = true
}.build())
+sealed class TemplateSerializer(serialName: String = "") : KSerializer {
+ protected val realSerialName =
+ serialName.ifEmpty { this::class.simpleName
+ ?: throw IllegalArgumentException("Anonymous classes must specify a serialName explicitly") }
+}
-/**
- * De-/serializes Base64 strings to/from [ByteArray]
- */
-object ByteArrayBase64Serializer : KSerializer {
+open class TransformingSerializerTemplate
+ (private val parent: KSerializer, private val encodeAs: (ValueT)->EncodedT,
+ private val decodeAs: (EncodedT)->ValueT, serialName: String = "")
+ : TemplateSerializer(serialName) {
override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("ByteArrayBase64Serializer", PrimitiveKind.STRING)
-
- override fun serialize(encoder: Encoder, value: ByteArray) {
- encoder.encodeString(value.encodeToString(Base64Strict))
+ when (val kind = parent.descriptor.kind) {
+ is PrimitiveKind -> PrimitiveSerialDescriptor(realSerialName, kind)
+ else -> SerialDescriptor(realSerialName, parent.descriptor)
+ }
+
+ override fun serialize(encoder: Encoder, value: ValueT) {
+ val v = try { encodeAs(value) }
+ catch (x: Throwable) { throw SerializationException("Encoding failed", x) }
+ encoder.encodeSerializableValue(parent, v)
}
- /**
- * @throws SerializationException on error
- */
- override fun deserialize(decoder: Decoder): ByteArray {
- return catching { decoder.decodeString().decodeToByteArray(Base64Strict) }
- .getOrElse { throw SerializationException("Base64 decoding failed", it) }
+ override fun deserialize(decoder: Decoder): ValueT {
+ val v = decoder.decodeSerializableValue(parent)
+ try { return decodeAs(v) }
+ catch (x: Throwable) { throw SerializationException("Decoding failed", x) }
}
-
}
-
-/**
- * De-/serializes Base64Url strings to/from [ByteArray]
- */
-object ByteArrayBase64UrlSerializer : KSerializer {
+/** De-/serializes Base64 strings to/from [ByteArray] */
+object ByteArrayBase64Serializer: TransformingSerializerTemplate(
+ parent = String.serializer(),
+ encodeAs = { it.encodeToString(Base64Strict) },
+ decodeAs = { it.decodeToByteArray(Base64Strict) }
+)
+
+/** De-/serializes Base64Url strings to/from [ByteArray] */
+object ByteArrayBase64UrlSerializer: TransformingSerializerTemplate(
+ parent = String.serializer(),
+ encodeAs = { it.encodeToString(Base64UrlStrict) },
+ decodeAs = { it.decodeToByteArray(Base64UrlStrict) }
+)
+
+/** De-/serializes X509Certificate as Base64Url-encoded String */
+object X509CertificateBase64UrlSerializer: TransformingSerializerTemplate(
+ parent = ByteArrayBase64UrlSerializer,
+ encodeAs = X509Certificate::encodeToDer,
+ decodeAs = X509Certificate::decodeFromDer
+)
+
+/** De-/serializes a public key as a Base64Url-encoded IOS encoding public key */
+object IosPublicKeySerializer: TransformingSerializerTemplate(
+ parent = ByteArrayBase64UrlSerializer,
+ encodeAs = CryptoPublicKey::iosEncoded,
+ decodeAs = CryptoPublicKey::fromIosEncoded)
+
+sealed class ListSerializerTemplate(
+ using: KSerializer, serialName: String = "")
+ : TemplateSerializer>(serialName) {
override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("ByteArrayBase64UrlSerializer", PrimitiveKind.STRING)
+ SerialDescriptor(realSerialName, listSerialDescriptor(using.descriptor))
- override fun serialize(encoder: Encoder, value: ByteArray) {
- encoder.encodeString(value.encodeToString(Base64UrlStrict))
- }
+ private val realSerializer = ListSerializer(using)
+ override fun serialize(encoder: Encoder, value: List) =
+ encoder.encodeSerializableValue(realSerializer, value)
+
+ override fun deserialize(decoder: Decoder): List =
+ decoder.decodeSerializableValue(realSerializer)
- //cannot be annotated with @Throws here because interfaces do not have annotations
- /**
- * @throws SerializationException on error
- */
- override fun deserialize(decoder: Decoder): ByteArray =
- catching { decoder.decodeString().decodeToByteArray(Base64UrlStrict) }
- .getOrElse { throw SerializationException("Base64 decoding failed", it) }
+}
-}
\ No newline at end of file
+object CertificateChainBase64UrlSerializer: ListSerializerTemplate(
+ using = X509CertificateBase64UrlSerializer)
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt
index 9b17d7bf..f25b4a6b 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/AlternativeNames.kt
@@ -18,6 +18,7 @@ import at.asitplus.signum.indispensable.pki.AlternativeNames.Companion.findSubje
* See [RFC 5280, Section 4.2.1.6](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6)
* for details on the properties of this container class, as they are named accordingly.
*/
+@ConsistentCopyVisibility
data class AlternativeNames
@Throws(Throwable::class)
private constructor(private val extensions: List) {
@@ -109,12 +110,12 @@ private constructor(private val extensions: List) {
companion object {
@Throws(Asn1Exception::class)
fun List.findSubjectAltNames() = runRethrowing {
- find(at.asitplus.signum.indispensable.asn1.KnownOIDs.subjectAltName_2_5_29_17)?.let { AlternativeNames(it) }
+ find(KnownOIDs.subjectAltName_2_5_29_17)?.let { AlternativeNames(it) }
}
@Throws(Asn1Exception::class)
fun List.findIssuerAltNames() = runRethrowing {
- find(at.asitplus.signum.indispensable.asn1.KnownOIDs.issuerAltName_2_5_29_18)?.let { AlternativeNames(it) }
+ find(KnownOIDs.issuerAltName_2_5_29_18)?.let { AlternativeNames(it) }
}
/**not for public use, since it forces [Asn1EncapsulatingOctetString]*/
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt
index 0175baa7..38b927f8 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequest.kt
@@ -39,7 +39,7 @@ data class TbsCertificationRequest(
) : this(version, subjectName, publicKey, mutableListOf().also { attrs ->
if (extensions.isEmpty()) throw IllegalArgumentException("No extensions provided!")
attributes?.let { attrs.addAll(it) }
- attrs.add(Pkcs10CertificationRequestAttribute(at.asitplus.signum.indispensable.asn1.KnownOIDs.extensionRequest, Asn1.Sequence {
+ attrs.add(Pkcs10CertificationRequestAttribute(KnownOIDs.extensionRequest, Asn1.Sequence {
extensions.forEach { +it }
}))
})
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt
index ac849fcc..48faaed3 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/RelativeDistinguishedName.kt
@@ -44,7 +44,7 @@ sealed class AttributeTypeAndValue : Asn1Encodable, Identifiable {
constructor(str: Asn1String) : this(Asn1Primitive(str.tag, str.value.encodeToByteArray()))
companion object {
- val OID = at.asitplus.signum.indispensable.asn1.KnownOIDs.commonName
+ val OID = KnownOIDs.commonName
}
}
@@ -56,7 +56,7 @@ sealed class AttributeTypeAndValue : Asn1Encodable, Identifiable {
constructor(str: Asn1String) : this(Asn1Primitive(str.tag, str.value.encodeToByteArray()))
companion object {
- val OID = at.asitplus.signum.indispensable.asn1.KnownOIDs.countryName
+ val OID = KnownOIDs.countryName
}
}
@@ -68,7 +68,7 @@ sealed class AttributeTypeAndValue : Asn1Encodable, Identifiable {
constructor(str: Asn1String) : this(Asn1Primitive(str.tag, str.value.encodeToByteArray()))
companion object {
- val OID = at.asitplus.signum.indispensable.asn1.KnownOIDs.organizationName
+ val OID = KnownOIDs.organizationName
}
}
@@ -80,7 +80,7 @@ sealed class AttributeTypeAndValue : Asn1Encodable, Identifiable {
constructor(str: Asn1String) : this(Asn1Primitive(str.tag, str.value.encodeToByteArray()))
companion object {
- val OID = at.asitplus.signum.indispensable.asn1.KnownOIDs.organizationalUnitName
+ val OID = KnownOIDs.organizationalUnitName
}
}
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt
index 14dcd1b8..79f89ea4 100644
--- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt
+++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateExtension.kt
@@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
* X.509 Certificate Extension
*/
@Serializable
+@ConsistentCopyVisibility
data class X509CertificateExtension @Throws(Asn1Exception::class) private constructor(
override val oid: ObjectIdentifier,
val value: Asn1Element,
diff --git a/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt b/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt
new file mode 100644
index 00000000..123c3eb2
--- /dev/null
+++ b/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt
@@ -0,0 +1,92 @@
+@file:OptIn(ExperimentalForeignApi::class)
+package at.asitplus.signum.indispensable
+
+import kotlinx.cinterop.ExperimentalForeignApi
+import platform.Security.SecKeyAlgorithm
+import platform.Security.kSecKeyAlgorithmECDSASignatureDigestX962SHA1
+import platform.Security.kSecKeyAlgorithmECDSASignatureDigestX962SHA256
+import platform.Security.kSecKeyAlgorithmECDSASignatureDigestX962SHA384
+import platform.Security.kSecKeyAlgorithmECDSASignatureDigestX962SHA512
+import platform.Security.kSecKeyAlgorithmECDSASignatureMessageX962SHA1
+import platform.Security.kSecKeyAlgorithmECDSASignatureMessageX962SHA256
+import platform.Security.kSecKeyAlgorithmECDSASignatureMessageX962SHA384
+import platform.Security.kSecKeyAlgorithmECDSASignatureMessageX962SHA512
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA1
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA384
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPSSSHA1
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPSSSHA256
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPSSSHA384
+import platform.Security.kSecKeyAlgorithmRSASignatureDigestPSSSHA512
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA1
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePSSSHA1
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePSSSHA256
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePSSSHA384
+import platform.Security.kSecKeyAlgorithmRSASignatureMessagePSSSHA512
+
+val SignatureAlgorithm.secKeyAlgorithm : SecKeyAlgorithm get() = when (this) {
+ is SignatureAlgorithm.ECDSA -> {
+ when (digest) {
+ Digest.SHA1 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA1
+ Digest.SHA256 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA256
+ Digest.SHA384 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA384
+ Digest.SHA512 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA512
+ else -> throw IllegalArgumentException("Raw signing is not supported on iOS")
+ }
+ }
+ is SignatureAlgorithm.RSA -> {
+ when (padding) {
+ RSAPadding.PSS -> when (digest) {
+ Digest.SHA1 -> kSecKeyAlgorithmRSASignatureMessagePSSSHA1
+ Digest.SHA256 -> kSecKeyAlgorithmRSASignatureMessagePSSSHA256
+ Digest.SHA384 -> kSecKeyAlgorithmRSASignatureMessagePSSSHA384
+ Digest.SHA512 -> kSecKeyAlgorithmRSASignatureMessagePSSSHA512
+ }
+ RSAPadding.PKCS1 -> when (digest) {
+ Digest.SHA1 -> kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA1
+ Digest.SHA256 -> kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256
+ Digest.SHA384 -> kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384
+ Digest.SHA512 -> kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512
+ }
+ }
+ }
+ is SignatureAlgorithm.HMAC -> TODO("HMAC is unsupported")
+}!!
+val SpecializedSignatureAlgorithm.secKeyAlgorithm get() =
+ this.algorithm.secKeyAlgorithm
+
+val SignatureAlgorithm.secKeyAlgorithmPreHashed: SecKeyAlgorithm get() = when (this) {
+ is SignatureAlgorithm.ECDSA -> {
+ when (digest) {
+ Digest.SHA1 -> kSecKeyAlgorithmECDSASignatureDigestX962SHA1
+ Digest.SHA256 -> kSecKeyAlgorithmECDSASignatureDigestX962SHA256
+ Digest.SHA384 -> kSecKeyAlgorithmECDSASignatureDigestX962SHA384
+ Digest.SHA512 -> kSecKeyAlgorithmECDSASignatureDigestX962SHA512
+ else -> throw IllegalArgumentException("Raw signing is not supported on iOS")
+ }
+ }
+ is SignatureAlgorithm.RSA -> {
+ when (padding) {
+ RSAPadding.PSS -> when (digest) {
+ Digest.SHA1 -> kSecKeyAlgorithmRSASignatureDigestPSSSHA1
+ Digest.SHA256 -> kSecKeyAlgorithmRSASignatureDigestPSSSHA256
+ Digest.SHA384 -> kSecKeyAlgorithmRSASignatureDigestPSSSHA384
+ Digest.SHA512 -> kSecKeyAlgorithmRSASignatureDigestPSSSHA512
+ }
+ RSAPadding.PKCS1 -> when (digest) {
+ Digest.SHA1 -> kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA1
+ Digest.SHA256 -> kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256
+ Digest.SHA384 -> kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA384
+ Digest.SHA512 -> kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512
+ }
+ }
+ }
+ is SignatureAlgorithm.HMAC -> TODO("HMAC is unsupported")
+}!!
+
+val SpecializedSignatureAlgorithm.secKeyAlgorithmPreHashed get() =
+ this.algorithm.secKeyAlgorithmPreHashed
diff --git a/indispensable/src/jvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt b/indispensable/src/jvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt
index 35e33af4..d18bdd44 100644
--- a/indispensable/src/jvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt
+++ b/indispensable/src/jvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt
@@ -37,6 +37,10 @@ val Digest.jcaPSSParams
Digest.SHA512 -> PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 64, 1)
}
+internal val isAndroid by lazy {
+ try { Class.forName("android.os.Build"); true } catch (_: ClassNotFoundException) { false }
+}
+
private fun sigGetInstance(alg: String, provider: String?) =
when (provider) {
null -> Signature.getInstance(alg)
@@ -52,10 +56,14 @@ fun SignatureAlgorithm.getJCASignatureInstance(provider: String? = null) = catch
is SignatureAlgorithm.RSA -> when (this.padding) {
RSAPadding.PKCS1 ->
sigGetInstance("${this.digest.jcaAlgorithmComponent}withRSA", provider)
- RSAPadding.PSS ->
- sigGetInstance("RSASSA-PSS", provider).also {
- it.setParameter(this.digest.jcaPSSParams)
- }
+ RSAPadding.PSS -> when (isAndroid) {
+ true ->
+ sigGetInstance("${this.digest.jcaAlgorithmComponent}withRSA/PSS", provider)
+ false ->
+ sigGetInstance("RSASSA-PSS", provider).also {
+ it.setParameter(this.digest.jcaPSSParams)
+ }
+ }
}
}
}
@@ -63,6 +71,28 @@ fun SignatureAlgorithm.getJCASignatureInstance(provider: String? = null) = catch
fun SpecializedSignatureAlgorithm.getJCASignatureInstance(provider: String? = null) =
this.algorithm.getJCASignatureInstance(provider)
+/** Get a pre-configured JCA instance for pre-hashed data for this algorithm */
+fun SignatureAlgorithm.getJCASignatureInstancePreHashed(provider: String? = null) = catching {
+ when (this) {
+ is SignatureAlgorithm.ECDSA -> sigGetInstance("NONEwithECDSA", provider)
+ is SignatureAlgorithm.RSA -> when (this.padding) {
+ RSAPadding.PKCS1 -> when (isAndroid) {
+ true -> sigGetInstance("NONEwithRSA", provider)
+ false -> throw UnsupportedOperationException("Pre-hashed RSA input is unsupported on JVM")
+ }
+ RSAPadding.PSS -> when (isAndroid) {
+ true -> sigGetInstance("NONEwithRSA/PSS", provider)
+ false -> throw UnsupportedOperationException("Pre-hashed RSA input is unsupported on JVM")
+ }
+ }
+ else -> TODO("$this is unsupported with pre-hashed data")
+ }
+}
+
+/** Get a pre-configured JCA instance for pre-hashed data for this algorithm */
+fun SpecializedSignatureAlgorithm.getJCASignatureInstancePreHashed(provider: String? = null) =
+ this.algorithm.getJCASignatureInstancePreHashed(provider)
+
val Digest.jcaName
get() = when (this) {
Digest.SHA256 -> "SHA-256"
diff --git a/indispensable/src/jvmTest/kotlin/ReadmeCompileTests.kt b/indispensable/src/jvmTest/kotlin/ReadmeCompileTests.kt
deleted file mode 100644
index c146a3d1..00000000
--- a/indispensable/src/jvmTest/kotlin/ReadmeCompileTests.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-import at.asitplus.signum.indispensable.asn1.Asn1
-import at.asitplus.signum.indispensable.asn1.Asn1.PrintableString
-import at.asitplus.signum.indispensable.asn1.Asn1.Tagged
-import at.asitplus.signum.indispensable.asn1.Asn1.UtcTime
-import at.asitplus.signum.indispensable.asn1.Asn1.Utf8String
-import at.asitplus.signum.indispensable.asn1.Asn1Primitive
-import at.asitplus.signum.indispensable.asn1.Asn1String
-import at.asitplus.signum.indispensable.asn1.BERTags
-import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
-import io.kotest.core.spec.style.FreeSpec
-import io.kotest.matchers.compilation.shouldCompile
-import kotlinx.datetime.Clock
-import kotlinx.datetime.Instant
-
-class ReadmeCompileTests : FreeSpec({
- "!Certificate Parsing" {
-
- """
-val cert = X509Certificate.decodeFromDer(certBytes)
-
-when (val pk = cert.publicKey) {
- is CryptoPublicKey.EC -> println(
- "Certificate with serial no. %{
- cert.tbsCertificate.serialNumber
- } contains an EC public key using curve %{pk.curve}"
- )
-
- is CryptoPublicKey.Rsa -> println(
- "Certificate with serial no. %{
- cert.tbsCertificate.serialNumber
- } contains a %{pk.bits.number} bit RSA public key"
- )
-}
-
-println("The full certificate is:\n%{Json { prettyPrint = true }.encodeToString(cert)}")
-
-println("Re-encoding it produces the same bytes? %{cert.encodeToDer() contentEquals certBytes}")
-""".replace('%','$').shouldCompile()
- }
-
- "!Creating a CSR" {
- """
-val ecPublicKey: ECPublicKey = TODO("From platform-specific code")
-val cryptoPublicKey = CryptoPublicKey.EC.fromJcaPublicKey(ecPublicKey).getOrThrow()
-
-val commonName = "DefaultCryptoService"
-val signatureAlgorithm = X509SignatureAlgorithm.ES256
-
-
-val tbsCsr = TbsCertificationRequest(
- version = 0,
- subjectName = listOf(RelativeDistinguishedName(AttributeTypeAndValue.CommonName(Asn1String.UTF8(commonName)))),
- publicKey = cryptoPublicKey
-)
-val signed: ByteArray = TODO("pass tbsCsr.encodeToDer() to platform code")
-val csr = Pkcs10CertificationRequest(tbsCsr, signatureAlgorithm, signed)
-
-println(csr.encodeToDer())
-""".shouldCompile()
- }
-
- "!ASN1 DSL for Creating ASN.1 Structures" {
- """
-Asn1.Sequence {
- +Tagged(1u) {
- +Asn1Primitive(BERTags.BOOLEAN, byteArrayOf(0x00))
- }
- +Asn1.Set {
- +Asn1.Sequence {
- +Asn1.SetOf {
- +PrintableString("World")
- +PrintableString("Hello")
- }
- +Asn1.Set {
- +PrintableString("World")
- +PrintableString("Hello")
- +Utf8String("!!!")
- }
-
- }
- }
- +Asn1.Null()
-
- +ObjectIdentifier("1.2.603.624.97")
-
- +Utf8String("Foo")
- +PrintableString("Bar")
-
- +Asn1.Set {
- +Asn1.Int(3)
- +Asn1.Long(-65789876543L)
- +Asn1.Bool(false)
- +Asn1.Bool(true)
- }
- +Asn1.Sequence {
- +Asn1.Null()
- +Asn1String.Numeric("12345")
- +UtcTime(Clock.System.now())
- }
-}
-""".shouldCompile()
- }
-})
diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt
index 1edcbd92..df7d9b2a 100644
--- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt
+++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt
@@ -10,9 +10,9 @@ import io.kotest.matchers.shouldNotBe
class DistinguishedNameTest : FreeSpec({
"DistinguishedName test equals and hashCode" - {
val oids = listOf(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.countryName, at.asitplus.signum.indispensable.asn1.KnownOIDs.country, at.asitplus.signum.indispensable.asn1.KnownOIDs.houseIdentifier,
- at.asitplus.signum.indispensable.asn1.KnownOIDs.organizationName, at.asitplus.signum.indispensable.asn1.KnownOIDs.organization, at.asitplus.signum.indispensable.asn1.KnownOIDs.organizationalUnit,
- at.asitplus.signum.indispensable.asn1.KnownOIDs.organizationalPerson, at.asitplus.signum.indispensable.asn1.KnownOIDs.brainpoolP512r1
+ KnownOIDs.countryName, KnownOIDs.country, KnownOIDs.houseIdentifier,
+ KnownOIDs.organizationName, KnownOIDs.organization, KnownOIDs.organizationalUnit,
+ KnownOIDs.organizationalPerson, KnownOIDs.brainpoolP512r1
)
withData(oids) { first ->
withData(oids) { second ->
diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EncodingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EncodingTest.kt
new file mode 100644
index 00000000..75f8d69a
--- /dev/null
+++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EncodingTest.kt
@@ -0,0 +1,12 @@
+package at.asitplus.signum.indispensable
+
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import at.asitplus.signum.indispensable.io.ByteArrayBase64UrlSerializer
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.matchers.shouldBe
+
+class EncodingTest: FreeSpec({
+ "Correct serialName is determined by encoders" {
+ ByteArrayBase64UrlSerializer.descriptor.serialName shouldBe "ByteArrayBase64UrlSerializer"
+ }
+})
diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt
index 24e30923..66f75be7 100644
--- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt
+++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt
@@ -122,9 +122,9 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({
),
publicKey = cryptoPublicKey,
attributes = listOf(
- Pkcs10CertificationRequestAttribute(at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage, Asn1Element.parse(keyUsage.encoded)),
+ Pkcs10CertificationRequestAttribute(KnownOIDs.keyUsage, Asn1Element.parse(keyUsage.encoded)),
Pkcs10CertificationRequestAttribute(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.extKeyUsage,
+ KnownOIDs.extKeyUsage,
Asn1Element.parse(extendedKeyUsage.encoded)
)
)
@@ -178,12 +178,12 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({
publicKey = cryptoPublicKey,
extensions = listOf(
X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage,
+ KnownOIDs.keyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(keyUsage.encoded))),
critical = true
),
X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.extKeyUsage,
+ KnownOIDs.extKeyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(extendedKeyUsage.encoded))),
critical = true
)
@@ -363,9 +363,9 @@ class Pkcs10CertificationRequestJvmTest : FreeSpec({
Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.27"), 1337.encodeToTlv())
val attr13 =
Pkcs10CertificationRequestAttribute(ObjectIdentifier("1.2.1840.13549.1.9.16.1337.26"), 1338.encodeToTlv())
- val attr2 = Pkcs10CertificationRequestAttribute(at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage, Asn1Element.parse(keyUsage.encoded))
+ val attr2 = Pkcs10CertificationRequestAttribute(KnownOIDs.keyUsage, Asn1Element.parse(keyUsage.encoded))
val attr3 =
- Pkcs10CertificationRequestAttribute(at.asitplus.signum.indispensable.asn1.KnownOIDs.extKeyUsage, Asn1Element.parse(extendedKeyUsage.encoded))
+ Pkcs10CertificationRequestAttribute(KnownOIDs.extKeyUsage, Asn1Element.parse(extendedKeyUsage.encoded))
attr1 shouldBe attr1
attr1 shouldBe attr11
diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt
index 2712a877..b3eb8f5d 100644
--- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt
+++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt
@@ -386,27 +386,27 @@ class X509CertificateJvmTest : FreeSpec({
val extendedKeyUsage = ExtendedKeyUsage(KeyPurposeId.anyExtendedKeyUsage)
val ext1 = X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage,
+ KnownOIDs.keyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(keyUsage.encoded))),
critical = true
)
val ext2 = X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage,
+ KnownOIDs.keyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(keyUsage.encoded))),
critical = true
)
val ext3 = X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.extKeyUsage,
+ KnownOIDs.extKeyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(extendedKeyUsage.encoded))),
critical = true
)
val ext4 = X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage,
+ KnownOIDs.keyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(extendedKeyUsage.encoded))),
critical = true
)
val ext5 = X509CertificateExtension(
- at.asitplus.signum.indispensable.asn1.KnownOIDs.keyUsage,
+ KnownOIDs.keyUsage,
value = Asn1EncapsulatingOctetString(listOf(Asn1Element.parse(keyUsage.encoded))),
critical = false
)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 76aec1ac..cd9cb47e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,6 +3,7 @@ pluginManagement {
repositories {
google()
mavenCentral()
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //KOTEST snapshot
gradlePluginPortal()
maven {
url = uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo")
@@ -16,12 +17,6 @@ dependencyResolutionManagement {
google()
mavenCentral()
}
- versionCatalogs {
- create("kotlincrypto") {
- // https://github.com/KotlinCrypto/version-catalog/blob/master/gradle/kotlincrypto.versions.toml
- from("org.kotlincrypto:version-catalog:0.5.2")
- }
- }
}
include(":indispensable")
diff --git a/supreme/build.gradle.kts b/supreme/build.gradle.kts
index 4e47a0f3..ce1bfd28 100644
--- a/supreme/build.gradle.kts
+++ b/supreme/build.gradle.kts
@@ -26,7 +26,8 @@ buildscript {
}
-version = "0.1.1-PRE"
+val supremeVersion: String by extra
+version = supremeVersion
wireAndroidInstrumentedTests()
@@ -52,19 +53,11 @@ kotlin {
implementation(coroutines())
implementation(napier())
api(project(":indispensable"))
- api(kotlincrypto.core.digest)
- implementation(kotlincrypto.hash.sha1)
- implementation(kotlincrypto.hash.sha2)
- implementation(kotlincrypto.secureRandom)
}
- sourceSets.jvmTest.dependencies {
- implementation("io.kotest.extensions:kotest-assertions-compiler:1.0.0")
- }
- /*
+
sourceSets.androidMain.dependencies {
implementation("androidx.biometric:biometric:1.2.0-alpha05")
}
- */
}
@@ -80,7 +73,7 @@ android {
namespace = "at.asitplus.signum.supreme"
compileSdk = 34
defaultConfig {
- minSdk = 33
+ minSdk = 30
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -102,11 +95,12 @@ android {
}
testOptions {
+ targetSdk=30
managedDevices {
localDevices {
create("pixel2api33") {
device = "Pixel 2"
- apiLevel = 33
+ apiLevel = 30
systemImageSource = "aosp-atd"
}
}
@@ -157,7 +151,7 @@ publishing {
scm {
connection.set("scm:git:git@github.com:a-sit-plus/signum.git")
developerConnection.set("scm:git:git@github.com:a-sit-plus/signum.git")
- url.set("https://github.com/a-sit-plus/kmp-crypto")
+ url.set("https://github.com/a-sit-plus/signum")
}
}
}
diff --git a/supreme/src/androidInstrumentedTest/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProviderTests.kt b/supreme/src/androidInstrumentedTest/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProviderTests.kt
new file mode 100644
index 00000000..706810bd
--- /dev/null
+++ b/supreme/src/androidInstrumentedTest/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProviderTests.kt
@@ -0,0 +1,35 @@
+package at.asitplus.signum.supreme.os
+
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.supreme.sign.verifierFor
+import at.asitplus.signum.supreme.sign.verify
+import at.asitplus.signum.supreme.signature
+import br.com.colman.kotest.FreeSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+import io.kotest.property.azstring
+import kotlin.random.Random
+
+class AndroidKeyStoreProviderTests : FreeSpec({
+ "Create attested keypair" {
+ val alias = Random.azstring(32)
+ val attestChallenge = Random.nextBytes(32)
+ val hardwareSigner = AndroidKeyStoreProvider.createSigningKey(alias) {
+ hardware {
+ attestation {
+ challenge = attestChallenge
+ }
+ }
+ }.getOrThrow()
+ val publicKey = hardwareSigner.publicKey
+ publicKey.shouldBeInstanceOf()
+
+ val plaintext = Random.nextBytes(64)
+ val signature = hardwareSigner.sign(plaintext).signature
+
+ SignatureAlgorithm.ECDSAwithSHA256.verifierFor(publicKey).getOrThrow()
+ .verify(plaintext, signature).getOrThrow() shouldBe Unit //no errors reported
+
+ }
+})
diff --git a/supreme/src/androidMain/AndroidManifest.xml b/supreme/src/androidMain/AndroidManifest.xml
new file mode 100644
index 00000000..edf0ce6c
--- /dev/null
+++ b/supreme/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/AndroidInitialization.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/AndroidInitialization.kt
new file mode 100644
index 00000000..f39f2006
--- /dev/null
+++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/AndroidInitialization.kt
@@ -0,0 +1,61 @@
+package at.asitplus.signum.supreme
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Application
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.ProviderInfo
+import android.net.Uri
+import android.os.Bundle
+import io.github.aakira.napier.Napier
+
+@SuppressLint("StaticFieldLeak")
+internal object AppLifecycleMonitor : Application.ActivityLifecycleCallbacks {
+ var currentActivity: Activity? = null
+
+ override fun onActivityResumed(activity: Activity) {
+ Napier.v { "Current activity is now: $activity" }
+ currentActivity = activity
+ }
+ override fun onActivityDestroyed(activity: Activity) {
+ if (currentActivity == activity) {
+ Napier.v { "Clearing current activity" }
+ currentActivity = null
+ }
+ }
+ override fun onActivityStarted(activity: Activity) {}
+ override fun onActivityStopped(activity: Activity) {}
+ override fun onActivityPaused(activity: Activity) {}
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
+
+}
+
+/** called exactly once, on application startup, as soon as the application context becomes available */
+private fun init(context: Application) {
+ context.registerActivityLifecycleCallbacks(AppLifecycleMonitor)
+ Napier.v { "Signum library initialized!" }
+}
+
+class InitProvider: ContentProvider() {
+ override fun onCreate(): Boolean {
+ init(context as? Application ?: return false)
+ return true
+ }
+
+ override fun attachInfo(context: Context?, info: ProviderInfo?) {
+ super.attachInfo(context, info)
+ require(info?.authority != ".SignumSupremeInitProvider")
+ { "You must specify an applicationId in your application's build.gradle(.kts) file!" }
+ }
+
+ private fun no(): Nothing { throw UnsupportedOperationException("This provider is only used for library initialization.") }
+ override fun insert(uri: Uri, values: ContentValues?) = no()
+ override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?) = no()
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?) = no()
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = no()
+ override fun getType(uri: Uri) = no()
+}
+
diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/hash/DigestImpl.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/hash/DigestImpl.kt
new file mode 100644
index 00000000..f2c85fee
--- /dev/null
+++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/hash/DigestImpl.kt
@@ -0,0 +1,10 @@
+package at.asitplus.signum.supreme.hash
+
+import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.jcaName
+import java.security.MessageDigest
+
+internal actual fun doDigest(digest: Digest, data: Sequence): ByteArray =
+ MessageDigest.getInstance(digest.jcaName).apply {
+ data.forEach { update(it) }
+ }.digest()
diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/hazmat/InternalsAccessors.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/hazmat/InternalsAccessors.kt
new file mode 100644
index 00000000..126900aa
--- /dev/null
+++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/hazmat/InternalsAccessors.kt
@@ -0,0 +1,22 @@
+package at.asitplus.signum.supreme.hazmat
+
+import at.asitplus.signum.indispensable.getJCASignatureInstance
+import at.asitplus.signum.supreme.HazardousMaterials
+import at.asitplus.signum.supreme.os.AndroidKeystoreSigner
+import at.asitplus.signum.supreme.sign.AndroidEphemeralSigner
+import at.asitplus.signum.supreme.sign.EphemeralKey
+import at.asitplus.signum.supreme.sign.EphemeralKeyBase
+import at.asitplus.signum.supreme.sign.Signer
+import java.security.PrivateKey
+
+/** The underlying JCA [PrivateKey] object. */
+@HazardousMaterials
+val EphemeralKey.jcaPrivateKey get() = (this as? EphemeralKeyBase<*>)?.privateKey as? PrivateKey
+
+/** The underlying JCA [PrivateKey] object. */
+@HazardousMaterials
+val Signer.jcaPrivateKey get() = when (this) {
+ is AndroidEphemeralSigner -> this.privateKey
+ is AndroidKeystoreSigner -> this.jcaPrivateKey
+ else -> null
+}
diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt
new file mode 100644
index 00000000..a48e144b
--- /dev/null
+++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt
@@ -0,0 +1,424 @@
+package at.asitplus.signum.supreme.os
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyInfo
+import android.security.keystore.KeyProperties
+import android.security.keystore.UserNotAuthenticatedException
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.AuthenticationResult
+import androidx.biometric.BiometricPrompt.CryptoObject
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import at.asitplus.KmmResult
+import at.asitplus.catching
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.CryptoSignature
+import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.RSAPadding
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.indispensable.asn1.Asn1StructuralException
+import at.asitplus.signum.indispensable.fromJcaPublicKey
+import at.asitplus.signum.indispensable.getJCASignatureInstance
+import at.asitplus.signum.indispensable.jcaName
+import at.asitplus.signum.indispensable.parseFromJca
+import at.asitplus.signum.indispensable.pki.X509Certificate
+import at.asitplus.signum.indispensable.pki.leaf
+import at.asitplus.signum.supreme.AppLifecycleMonitor
+import at.asitplus.signum.supreme.SignatureResult
+import at.asitplus.signum.supreme.UnlockFailed
+import at.asitplus.signum.supreme.UnsupportedCryptoException
+import at.asitplus.signum.supreme.dsl.DISCOURAGED
+import at.asitplus.signum.supreme.dsl.DSL
+import at.asitplus.signum.supreme.dsl.DSLConfigureFn
+import at.asitplus.signum.supreme.dsl.FeaturePreference
+import at.asitplus.signum.supreme.dsl.PREFERRED
+import at.asitplus.signum.supreme.dsl.REQUIRED
+import at.asitplus.signum.supreme.sign.SignatureInput
+import at.asitplus.signum.supreme.sign.SigningKeyConfiguration
+import at.asitplus.signum.supreme.signCatching
+import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger
+import io.github.aakira.napier.Napier
+import at.asitplus.signum.supreme.sign.Signer as SignerI
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.security.KeyFactory
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.PrivateKey
+import java.security.Signature
+import java.security.cert.CertificateFactory
+import java.security.spec.ECGenParameterSpec
+import java.security.spec.RSAKeyGenParameterSpec
+import java.time.Instant
+import java.util.Date
+import java.util.concurrent.Executors
+import javax.security.auth.x500.X500Principal
+import kotlin.math.max
+
+internal sealed interface FragmentContext {
+ @JvmInline value class OfActivity(val activity: FragmentActivity): FragmentContext
+ @JvmInline value class OfFragment(val fragment: Fragment): FragmentContext
+}
+
+private val keystoreContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+class AndroidKeymasterConfiguration internal constructor(): PlatformSigningKeyConfigurationBase.SecureHardwareConfiguration() {
+ /** Whether a StrongBox TPM is required. */
+ var strongBox: FeaturePreference = PREFERRED
+}
+class AndroidSigningKeyConfiguration internal constructor(): PlatformSigningKeyConfigurationBase() {
+ override val hardware = childOrNull(::AndroidKeymasterConfiguration)
+}
+
+class AndroidUnlockPromptConfiguration internal constructor(): UnlockPromptConfiguration() {
+ /** Explicitly specify the FragmentActivity to use for authentication prompts.
+ * You will not need to set this in most cases; the default is the current activity. */
+ lateinit var activity: FragmentActivity
+
+ /** Explicitly set the Fragment to base authentication prompts on.
+ * You will not need to set this in most cases; the default is the current activity.*/
+ lateinit var fragment: Fragment
+
+ internal val explicitContext: FragmentContext get() = when {
+ this::fragment.isInitialized -> FragmentContext.OfFragment(fragment)
+ else -> FragmentContext.OfActivity(activity)
+ }
+ internal val hasExplicitContext get() =
+ (this::fragment.isInitialized || this::activity.isInitialized)
+
+ internal val _subtitle = Stackable()
+ /** @see [BiometricPrompt.PromptInfo.Builder.setSubtitle] */
+ var subtitle by _subtitle
+
+ internal val _description = Stackable()
+ /** @see [BiometricPrompt.PromptInfo.Builder.setDescription] */
+ var description by _description
+
+ internal val _confirmationRequired = Stackable()
+ /** @see [BiometricPrompt.PromptInfo.Builder.setConfirmationRequired] */
+ var confirmationRequired by _confirmationRequired
+
+ internal val _allowedAuthenticators = Stackable()
+ /** @see [BiometricPrompt.PromptInfo.Builder.setAllowedAuthenticators] */
+ var allowedAuthenticators by _allowedAuthenticators
+
+ /** if the provided fingerprint could not be matched, but the user will be allowed to retry */
+ var invalidBiometryCallback: (()->Unit)? = null
+}
+
+class AndroidSignerConfiguration: PlatformSignerConfigurationBase() {
+ override val unlockPrompt = childOrDefault(::AndroidUnlockPromptConfiguration)
+}
+
+class AndroidSignerSigningConfiguration: PlatformSigningProviderSignerSigningConfigurationBase() {
+ override val unlockPrompt = childOrDefault(::AndroidUnlockPromptConfiguration)
+}
+
+/**
+ * Resolve [what] differently based on whether the [vA]lue was [spec]ified.
+ *
+ * * [spec] = `true`: Check if [valid] contains [nameMap] applied to [vA()][vA], return [vA()][vA] if yes, throw otherwise
+ * * [spec] = `false`: Check if [valid] contains exactly one element, if yes, return the [E] from [possible] for which [nameMap] returns that element, throw otherwise
+ */
+internal inline fun resolveOption(what: String, valid: Array, possible: Sequence, spec: Boolean, vA: ()->E, crossinline nameMap: (E)->String): E =
+ when (spec) {
+ true -> {
+ val v = vA()
+ val vStr = nameMap(v)
+ if (!valid.any { it.equals(vStr, ignoreCase=true) })
+ throw IllegalArgumentException("Key does not support $what $v; supported: ${valid.joinToString(", ")}")
+ v
+ }
+ false -> {
+ if (valid.size != 1)
+ throw IllegalArgumentException("Key supports multiple ${what}s (${valid.joinToString(", ")}). You need to specify $what in signer configuration.")
+ val only = valid.first()
+ possible.find {
+ nameMap(it).equals(only, ignoreCase=true)
+ } ?: throw UnsupportedCryptoException("Unsupported $what $only")
+ }
+ }
+
+/** A provider that manages keys in the [Android Key Store](https://developer.android.com/privacy-and-security/keystore). */
+object AndroidKeyStoreProvider:
+ PlatformSigningProviderI
+{
+
+ private val ks: KeyStore get() =
+ KeyStore.getInstance("AndroidKeyStore").apply { load(null, null) }
+
+ @SuppressLint("WrongConstant")
+ override suspend fun createSigningKey(
+ alias: String,
+ configure: DSLConfigureFn
+ ) = withContext(keystoreContext) { catching {
+ if (ks.containsAlias(alias)) {
+ throw NoSuchElementException("Key with alias $alias already exists")
+ }
+ val config = DSL.resolve(::AndroidSigningKeyConfiguration, configure)
+ val spec = KeyGenParameterSpec.Builder(
+ alias,
+ KeyProperties.PURPOSE_SIGN
+ ).apply {
+ when(val algSpec = config._algSpecific.v) {
+ is SigningKeyConfiguration.RSAConfiguration -> {
+ setAlgorithmParameterSpec(
+ RSAKeyGenParameterSpec(algSpec.bits, algSpec.publicExponent.toJavaBigInteger()))
+ setDigests(*algSpec.digests.map(Digest::jcaName).toTypedArray())
+ setSignaturePaddings(*algSpec.paddings.map {
+ when (it) {
+ RSAPadding.PKCS1 -> KeyProperties.SIGNATURE_PADDING_RSA_PKCS1
+ RSAPadding.PSS -> KeyProperties.SIGNATURE_PADDING_RSA_PSS
+ }
+ }.toTypedArray())
+ }
+ is SigningKeyConfiguration.ECConfiguration -> {
+ setAlgorithmParameterSpec(ECGenParameterSpec(algSpec.curve.jcaName))
+ setDigests(*algSpec.digests.map { it?.jcaName ?: KeyProperties.DIGEST_NONE }.toTypedArray())
+ }
+ }
+ setCertificateNotBefore(Date.from(Instant.now()))
+ setCertificateSubject(X500Principal("CN=$alias")) // TODO
+ config.hardware.v?.let { hw ->
+ setIsStrongBoxBacked(when (hw.strongBox) {
+ REQUIRED -> true
+ PREFERRED -> false // TODO
+ DISCOURAGED -> false
+ })
+ hw.attestation.v?.let {
+ setAttestationChallenge(it.challenge)
+ }
+ hw.protection.v?.let {
+ setInvalidatedByBiometricEnrollment(it.factors.v.biometry &&
+ !it.factors.v.biometryWithNewFactors)
+ setUserAuthenticationRequired(true)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ setUserAuthenticationParameters(it.timeout.inWholeSeconds.toInt(),
+ (if (it.factors.v.biometry) KeyProperties.AUTH_BIOMETRIC_STRONG else 0) or
+ (if (it.factors.v.deviceLock) KeyProperties.AUTH_DEVICE_CREDENTIAL else 0))
+ } else {
+ it.factors.v.let { factors -> when {
+ factors.biometry && !factors.deviceLock -> {
+ @Suppress("DEPRECATION")
+ setUserAuthenticationValidityDurationSeconds(-1)
+ }
+ else -> {
+ @Suppress("DEPRECATION")
+ setUserAuthenticationValidityDurationSeconds(max(1, it.timeout.inWholeSeconds.toInt()))
+ }
+ }}
+ }
+ }
+ }
+ }.build()
+ KeyPairGenerator.getInstance(when(config._algSpecific.v) {
+ is SigningKeyConfiguration.RSAConfiguration -> KeyProperties.KEY_ALGORITHM_RSA
+ is SigningKeyConfiguration.ECConfiguration -> KeyProperties.KEY_ALGORITHM_EC
+ }, "AndroidKeyStore").apply {
+ initialize(spec)
+ }.generateKeyPair()
+ return@catching getSignerForKey(alias, config.signer.v).getOrThrow()
+ }}
+
+ override suspend fun getSignerForKey(
+ alias: String,
+ configure: DSLConfigureFn
+ ): KmmResult = withContext(keystoreContext) { catching {
+ val config = DSL.resolve(::AndroidSignerConfiguration, configure)
+ val jcaPrivateKey = ks.getKey(alias, null) as? PrivateKey
+ ?: throw NoSuchElementException("No key for alias $alias exists")
+ val publicKey: CryptoPublicKey
+ val attestation: AndroidKeystoreAttestation?
+ ks.getCertificateChain(alias).let { chain ->
+ runCatching { chain.map { X509Certificate.decodeFromDer(it.encoded) } }.let { r ->
+ if (r.isSuccess) r.getOrThrow().let {
+ publicKey = it.leaf.publicKey
+ attestation = if (it.size > 1) AndroidKeystoreAttestation(it) else null
+ } else r.exceptionOrNull()!!.let {
+ if ((it is Asn1StructuralException) &&
+ (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) &&
+ (chain.size == 1) &&
+ (chain.first().encoded.takeLast(5) == listOf(0x03,0x03,0x00,0x30,0x00).map(Int::toByte))) {
+ Napier.v { "Correcting Android 10 AKS signature bug" }
+ publicKey = CertificateFactory.getInstance("X.509")
+ .generateCertificate(chain.first().encoded.inputStream())
+ .publicKey.let(CryptoPublicKey::fromJcaPublicKey).getOrThrow()
+ attestation = null
+ } else throw it
+ }
+ }
+ }
+
+ val keyInfo = KeyFactory.getInstance(jcaPrivateKey.algorithm)
+ .getKeySpec(jcaPrivateKey, KeyInfo::class.java)
+ val algorithm = when (publicKey) {
+ is CryptoPublicKey.EC -> {
+ val ecConfig = config.ec.v
+ val digest = resolveOption("digest", keyInfo.digests, Digest.entries.asSequence() + sequenceOf(null), ecConfig.digestSpecified, { ecConfig.digest }) { it?.jcaName ?: KeyProperties.DIGEST_NONE }
+ SignatureAlgorithm.ECDSA(digest, publicKey.curve)
+ }
+ is CryptoPublicKey.Rsa -> {
+ val rsaConfig = config.rsa.v
+ val digest = resolveOption("digest", keyInfo.digests, Digest.entries.asSequence(), rsaConfig.digestSpecified, { rsaConfig.digest }, Digest::jcaName)
+ val padding = resolveOption("padding", keyInfo.signaturePaddings, RSAPadding.entries.asSequence(), rsaConfig.paddingSpecified, { rsaConfig.padding }) {
+ when (it) {
+ RSAPadding.PKCS1 -> KeyProperties.SIGNATURE_PADDING_RSA_PKCS1
+ RSAPadding.PSS -> KeyProperties.SIGNATURE_PADDING_RSA_PSS
+ }
+ }
+ SignatureAlgorithm.RSA(digest, padding)
+ }
+ }
+
+ return@catching when (publicKey) {
+ is CryptoPublicKey.EC ->
+ AndroidKeystoreSigner.ECDSA(
+ jcaPrivateKey, alias, keyInfo, config, publicKey,
+ attestation, algorithm as SignatureAlgorithm.ECDSA)
+ is CryptoPublicKey.Rsa ->
+ AndroidKeystoreSigner.RSA(
+ jcaPrivateKey, alias, keyInfo, config, publicKey,
+ attestation, algorithm as SignatureAlgorithm.RSA)
+ }
+ }}
+
+ override suspend fun deleteSigningKey(alias: String) = catching { withContext(keystoreContext) {
+ ks.deleteEntry(alias)
+ }}
+}
+
+sealed class AndroidKeystoreSigner private constructor(
+ internal val jcaPrivateKey: PrivateKey,
+ final override val alias: String,
+ val keyInfo: KeyInfo,
+ private val config: AndroidSignerConfiguration,
+ final override val attestation: AndroidKeystoreAttestation?
+) : PlatformSigningProviderSigner, SignerI.Attestable {
+
+ final override val mayRequireUserUnlock: Boolean get() = this.needsAuthentication
+
+ private sealed interface AuthResult {
+ @JvmInline value class Success(val result: AuthenticationResult): AuthResult
+ data class Error(val code: Int, val message: String): AuthResult
+ }
+
+ private suspend fun attemptBiometry(config: DSL.ConfigStack, forSpecificKey: CryptoObject?) {
+ val channel = Channel(capacity = Channel.RENDEZVOUS)
+ val effectiveContext = config.getProperty(AndroidUnlockPromptConfiguration::explicitContext,
+ checker = AndroidUnlockPromptConfiguration::hasExplicitContext, default = {
+ (AppLifecycleMonitor.currentActivity as? FragmentActivity)?.let(FragmentContext::OfActivity)
+ ?: throw UnsupportedOperationException("The requested key with alias $alias requires unlock, but the current activity is not a FragmentActivity or could not be determined. " +
+ "Pass either { fragment = } or { activity = } inside authPrompt {}.")
+ })
+ val executor = when (effectiveContext) {
+ is FragmentContext.OfActivity -> ContextCompat.getMainExecutor(effectiveContext.activity)
+ is FragmentContext.OfFragment -> ContextCompat.getMainExecutor(effectiveContext.fragment.context)
+ }
+ executor.asCoroutineDispatcher().let(::CoroutineScope).launch {
+ val promptInfo = BiometricPrompt.PromptInfo.Builder().apply {
+ setTitle(config.getProperty(AndroidUnlockPromptConfiguration::_message,
+ default = UnlockPromptConfiguration.defaultMessage))
+ setNegativeButtonText(config.getProperty(AndroidUnlockPromptConfiguration::_cancelText,
+ default = UnlockPromptConfiguration.defaultCancelText))
+ config.getProperty(AndroidUnlockPromptConfiguration::_subtitle,null)?.let(this::setSubtitle)
+ config.getProperty(AndroidUnlockPromptConfiguration::_description,null)?.let(this::setDescription)
+ config.getProperty(AndroidUnlockPromptConfiguration::_allowedAuthenticators,null)?.let(this::setAllowedAuthenticators)
+ config.getProperty(AndroidUnlockPromptConfiguration::_confirmationRequired,null)?.let(this::setConfirmationRequired)
+ }.build()
+ val siphon = object: BiometricPrompt.AuthenticationCallback() {
+ private fun send(v: AuthResult) {
+ executor.asCoroutineDispatcher().let(::CoroutineScope).launch { channel.send(v) }
+ }
+ override fun onAuthenticationSucceeded(result: AuthenticationResult) {
+ send(AuthResult.Success(result))
+ }
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ send(AuthResult.Error(errorCode, errString.toString()))
+ }
+ override fun onAuthenticationFailed() {
+ config.forEach { it.invalidBiometryCallback?.invoke() }
+ }
+ }
+ val prompt = when (effectiveContext) {
+ is FragmentContext.OfActivity -> BiometricPrompt(effectiveContext.activity, executor, siphon)
+ is FragmentContext.OfFragment -> BiometricPrompt(effectiveContext.fragment, executor, siphon)
+ }
+ when (forSpecificKey) {
+ null -> prompt.authenticate(promptInfo)
+ else -> prompt.authenticate(promptInfo, forSpecificKey)
+ }
+ }
+ when (val result = channel.receive()) {
+ is AuthResult.Success -> return
+ is AuthResult.Error -> throw UnlockFailed("${result.message} (code ${result.code})")
+ }
+ }
+
+ internal suspend fun getJCASignature(signingConfig: AndroidSignerSigningConfiguration): Signature =
+ signatureAlgorithm.getJCASignatureInstance().getOrThrow().also {
+ if (needsAuthenticationForEveryUse) {
+ it.initSign(jcaPrivateKey)
+ attemptBiometry(DSL.ConfigStack(signingConfig.unlockPrompt.v, config.unlockPrompt.v), CryptoObject(it))
+ } else {
+ try {
+ it.initSign(jcaPrivateKey)
+ } catch (_: UserNotAuthenticatedException) {
+ attemptBiometry(DSL.ConfigStack(signingConfig.unlockPrompt.v, config.unlockPrompt.v), null)
+ it.initSign(jcaPrivateKey)
+ }
+ }
+ }
+
+ final override suspend fun trySetupUninterruptedSigning(configure: DSLConfigureFn) = catching {
+ if (needsAuthentication && !needsAuthenticationForEveryUse) {
+ withContext(keystoreContext) { getJCASignature(DSL.resolve(::AndroidSignerSigningConfiguration, configure)) }
+ }
+ }
+
+ final override suspend fun sign(
+ data: SignatureInput,
+ configure: DSLConfigureFn
+ ): SignatureResult<*> = withContext(keystoreContext) { signCatching {
+ require(data.format == null)
+ val jcaSig = getJCASignature(DSL.resolve(::AndroidSignerSigningConfiguration, configure))
+ .let { data.data.forEach(it::update); it.sign() }
+
+ return@signCatching when (this@AndroidKeystoreSigner) {
+ is ECDSA -> CryptoSignature.EC.parseFromJca(jcaSig).withCurve(publicKey.curve)
+ is RSA -> CryptoSignature.RSAorHMAC.parseFromJca(jcaSig)
+ }
+ }}
+
+ class ECDSA internal constructor(jcaPrivateKey: PrivateKey,
+ alias: String,
+ keyInfo: KeyInfo,
+ config: AndroidSignerConfiguration,
+ override val publicKey: CryptoPublicKey.EC,
+ attestation: AndroidKeystoreAttestation?,
+ override val signatureAlgorithm: SignatureAlgorithm.ECDSA)
+ : AndroidKeystoreSigner(jcaPrivateKey, alias, keyInfo, config, attestation), SignerI.ECDSA
+
+ class RSA internal constructor(jcaPrivateKey: PrivateKey,
+ alias: String,
+ keyInfo: KeyInfo,
+ config: AndroidSignerConfiguration,
+ override val publicKey: CryptoPublicKey.Rsa,
+ attestation: AndroidKeystoreAttestation?,
+ override val signatureAlgorithm: SignatureAlgorithm.RSA)
+ : AndroidKeystoreSigner(jcaPrivateKey, alias, keyInfo, config, attestation), SignerI.RSA
+}
+
+val AndroidKeystoreSigner.needsAuthentication inline get() =
+ keyInfo.isUserAuthenticationRequired
+val AndroidKeystoreSigner.needsAuthenticationForEveryUse inline get() =
+ keyInfo.isUserAuthenticationRequired &&
+ (keyInfo.userAuthenticationValidityDurationSeconds <= 0)
+
+internal actual fun getPlatformSigningProvider(configure: DSLConfigureFn): PlatformSigningProviderI<*,*,*> =
+ AndroidKeyStoreProvider
diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt
new file mode 100644
index 00000000..84548b6d
--- /dev/null
+++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt
@@ -0,0 +1,71 @@
+package at.asitplus.signum.supreme.sign
+
+import android.security.keystore.KeyProperties
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.CryptoSignature
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.indispensable.fromJcaPublicKey
+import at.asitplus.signum.indispensable.getJCASignatureInstancePreHashed
+import at.asitplus.signum.indispensable.jcaName
+import at.asitplus.signum.indispensable.parseFromJca
+import at.asitplus.signum.supreme.signCatching
+import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger
+import java.security.KeyPairGenerator
+import java.security.PrivateKey
+import java.security.spec.ECGenParameterSpec
+import java.security.spec.RSAKeyGenParameterSpec
+
+actual class EphemeralSigningKeyConfiguration internal actual constructor(): EphemeralSigningKeyConfigurationBase()
+actual class EphemeralSignerConfiguration internal actual constructor(): EphemeralSignerConfigurationBase()
+
+sealed class AndroidEphemeralSigner (internal val privateKey: PrivateKey) : Signer {
+ override val mayRequireUserUnlock = false
+ override suspend fun sign(data: SignatureInput) = signCatching {
+ val inputData = data.convertTo(signatureAlgorithm.preHashedSignatureFormat).getOrThrow()
+ signatureAlgorithm.getJCASignatureInstancePreHashed(provider = null).getOrThrow().run {
+ initSign(privateKey)
+ inputData.data.forEach { update(it) }
+ sign().let(::parseFromJca)
+ }
+ }
+
+ protected abstract fun parseFromJca(bytes: ByteArray): CryptoSignature.RawByteEncodable
+
+ class EC (config: EphemeralSignerConfiguration, privateKey: PrivateKey,
+ override val publicKey: CryptoPublicKey.EC, override val signatureAlgorithm: SignatureAlgorithm.ECDSA)
+ : AndroidEphemeralSigner(privateKey), Signer.ECDSA {
+
+ override fun parseFromJca(bytes: ByteArray) = CryptoSignature.EC.parseFromJca(bytes).withCurve(publicKey.curve)
+ }
+
+ class RSA (config: EphemeralSignerConfiguration, privateKey: PrivateKey,
+ override val publicKey: CryptoPublicKey.Rsa, override val signatureAlgorithm: SignatureAlgorithm.RSA)
+ : AndroidEphemeralSigner(privateKey), Signer.RSA {
+
+ override fun parseFromJca(bytes: ByteArray) = CryptoSignature.RSAorHMAC.parseFromJca(bytes)
+ }
+}
+
+internal actual fun makeEphemeralKey(configuration: EphemeralSigningKeyConfiguration) : EphemeralKey =
+ when (val alg = configuration._algSpecific.v) {
+ is SigningKeyConfiguration.ECConfiguration -> {
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC).run {
+ initialize(ECGenParameterSpec(alg.curve.jcaName))
+ generateKeyPair()
+ }.let { pair ->
+ EphemeralKeyBase.EC(AndroidEphemeralSigner::EC,
+ pair.private, CryptoPublicKey.fromJcaPublicKey(pair.public).getOrThrow() as CryptoPublicKey.EC,
+ digests = alg.digests)
+ }
+ }
+ is SigningKeyConfiguration.RSAConfiguration -> {
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA).run {
+ initialize(RSAKeyGenParameterSpec(alg.bits, alg.publicExponent.toJavaBigInteger()))
+ generateKeyPair()
+ }.let { pair ->
+ EphemeralKeyBase.RSA(AndroidEphemeralSigner::RSA,
+ pair.private, CryptoPublicKey.fromJcaPublicKey(pair.public).getOrThrow() as CryptoPublicKey.Rsa,
+ digests = alg.digests, paddings = alg.paddings)
+ }
+ }
+ }
diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt
index dce7f596..31fd2a02 100644
--- a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt
+++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/VerifierImpl.kt
@@ -33,8 +33,7 @@ private fun getSigInstance(alg: String, p: String?) =
@Throws(UnsupportedCryptoException::class)
internal actual fun checkAlgorithmKeyCombinationSupportedByECDSAPlatformVerifier
(signatureAlgorithm: SignatureAlgorithm.ECDSA, publicKey: CryptoPublicKey.EC,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
{
wrapping(asA=::UnsupportedCryptoException) {
getSigInstance("${signatureAlgorithm.digest.jcaAlgorithmComponent}withECDSA", config.provider)
@@ -46,8 +45,7 @@ internal actual fun checkAlgorithmKeyCombinationSupportedByECDSAPlatformVerifier
internal actual fun verifyECDSAImpl
(signatureAlgorithm: SignatureAlgorithm.ECDSA, publicKey: CryptoPublicKey.EC,
data: SignatureInput, signature: CryptoSignature.EC,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
{
val (input, alg) = when {
(data.format == null) -> /* input data is not hashed, let JCA do hashing */
@@ -73,8 +71,7 @@ private fun getRSAInstance(alg: SignatureAlgorithm.RSA, config: PlatformVerifier
@Throws(UnsupportedCryptoException::class)
internal actual fun checkAlgorithmKeyCombinationSupportedByRSAPlatformVerifier
(signatureAlgorithm: SignatureAlgorithm.RSA, publicKey: CryptoPublicKey.Rsa,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
{
wrapping(asA=::UnsupportedCryptoException) {
getRSAInstance(signatureAlgorithm, config)
@@ -86,8 +83,7 @@ internal actual fun checkAlgorithmKeyCombinationSupportedByRSAPlatformVerifier
internal actual fun verifyRSAImpl
(signatureAlgorithm: SignatureAlgorithm.RSA, publicKey: CryptoPublicKey.Rsa,
data: SignatureInput, signature: CryptoSignature.RSAorHMAC,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
{
getRSAInstance(signatureAlgorithm, config).run {
initVerify(publicKey.getJcaPublicKey().getOrThrow())
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/SignatureResult.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/SignatureResult.kt
new file mode 100644
index 00000000..9fca72ed
--- /dev/null
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/SignatureResult.kt
@@ -0,0 +1,63 @@
+package at.asitplus.signum.supreme
+
+import at.asitplus.KmmResult
+import at.asitplus.catching
+import at.asitplus.signum.indispensable.CryptoSignature
+import kotlin.jvm.JvmInline
+
+/** These map to SignatureResult.Failure instead of SignatureResult.Error */
+sealed class UserInitiatedCancellationReason(message: String?, cause: Throwable?): Throwable(message, cause)
+class UnlockFailed(message: String? = null, cause: Throwable? = null) : UserInitiatedCancellationReason(message, cause)
+
+sealed interface SignatureResult {
+ /** The signature succeeded. A signature is contained. */
+ @JvmInline value class Success(val signature: T): SignatureResult
+ /** The signature failed for expected reasons. Typically, this is because the user cancelled the operation. */
+ @JvmInline value class Failure(val problem: UserInitiatedCancellationReason): SignatureResult
+ /** The signature failed for an unexpected reason. The thrown exception is contained. */
+ @JvmInline value class Error(val exception: Throwable): SignatureResult
+ companion object {
+ /** Constructs a suitable failed SignatureResult from the exception.
+ * [UserInitiatedCancellationReason] and subclasses map to [Failure], anything else maps to [Error]. */
+ fun FromException(x: Throwable): SignatureResult = when (x) {
+ is UserInitiatedCancellationReason -> SignatureResult.Failure(x)
+ else -> SignatureResult.Error(x)
+ }
+ }
+}
+val SignatureResult<*>.isSuccess get() = (this is SignatureResult.Success)
+/** Retrieves the contained signature, asserting it exists. If it does not exist, throws the contained problem. */
+val SignatureResult.signature: T get() = when (this) {
+ is SignatureResult.Success -> this.signature
+ is SignatureResult.Failure -> throw this.problem
+ is SignatureResult.Error -> throw this.exception
+}
+/** Retrieves the contained signature, if one exists. */
+val SignatureResult.signatureOrNull: T? get() = when (this) {
+ is SignatureResult.Success -> this.signature
+ else -> null
+}
+/** Transforms this SignatureResult into a [KmmResult]. Both [Failure] and [Error] map to [KmmResult.Failure]. */
+fun SignatureResult.asKmmResult(): KmmResult = catching { this.signature }
+
+/** Modifies the contained [CryptoSignature], usually in order to reinterpret it as a more narrow type. */
+inline fun SignatureResult.map(block: (T)->S) =
+ when (this) {
+ is SignatureResult.Success -> SignatureResult.Success(block(this.signature))
+ is SignatureResult.Failure -> this
+ is SignatureResult.Error -> this
+ }
+
+/** Modifies the contained [CryptoSignature], usually in order to reinterpret it as a more narrow type. */
+inline fun SignatureResult
+ .modify(block: KmmResult.()->KmmResult) =
+ catching { this.signature }.block().fold(
+ onSuccess = { SignatureResult.Success(it) },
+ onFailure = { SignatureResult.FromException(it) })
+
+/** Runs the block, catches exceptions, and maps to [SignatureResult].
+ * @see SignatureResult.FromException */
+internal inline fun signCatching(fn: ()->CryptoSignature.RawByteEncodable): SignatureResult<*> =
+ runCatching { fn() }.fold(
+ onSuccess = { SignatureResult.Success(it) },
+ onFailure = { SignatureResult.FromException(it) })
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt
index 72a5921e..ff4c5257 100644
--- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt
@@ -1,6 +1,9 @@
package at.asitplus.signum.supreme
-sealed class CryptoException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause)
-class CryptoOperationFailed(message: String) : CryptoException(message)
+@RequiresOptIn(message = "Access to potentially hazardous platform-specific internals requires explicit opt-in. Specify @OptIn(HazardousMaterials::class). These accessors are unstable and may change without warning.")
+/** This is an internal property. It is exposed if you know what you are doing. You very likely don't actually need it. */
+annotation class HazardousMaterials
-class UnsupportedCryptoException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause)
+sealed class CryptoException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause)
+open class CryptoOperationFailed(message: String) : CryptoException(message)
+open class UnsupportedCryptoException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause)
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/ConfigurationDSL.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/ConfigurationDSL.kt
index b5d1d4d1..cb73a08f 100644
--- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/ConfigurationDSL.kt
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/ConfigurationDSL.kt
@@ -1,5 +1,7 @@
package at.asitplus.signum.supreme.dsl
+import kotlin.reflect.KProperty
+
/**
* The meta functionality that enables us to easily create DSLs.
* @see at.asitplus.signum.supreme.dsl.DSLInheritanceDemonstration
@@ -7,8 +9,24 @@ package at.asitplus.signum.supreme.dsl
*/
object DSL {
/** Resolve a DSL lambda to a concrete configuration */
- fun resolve(factory: ()->T, config: DSLConfigureFn): T =
- (if (config == null) factory() else factory().apply(config)).also(Data::validate)
+ fun resolve(factory: ()->T, config: DSLConfigureFn): T =
+ (if (config == null) factory() else factory().apply(config)).also(DSL.Data::validate)
+
+ /** A collection of equivalent DSL configuration structures which shadow each other.
+ * @see getProperty */
+ internal class ConfigStack(private vararg val stackedData: S): Iterable by stackedData.asIterable() {
+ /** Retrieve a property from a stack of (partially-)configured DSL data.
+ * Each element of the stack should have an indication of whether the property is set, and a value of the property (which is only accessed if the property is set).
+ * This is commonly implemented using `lateinit var`s (with `internal val .. get() = this::prop.isInitialized` as the property checker).*/
+ fun getProperty(getter: (S)->T, checker: (S)->Boolean, default: ()->T): T =
+ try { getter(stackedData.first(checker)) } catch (_: NoSuchElementException) { default() }
+ fun getProperty(getter: (S)->T, checker: (S)->Boolean, default: T) =
+ try { getter(stackedData.first(checker)) } catch (_: NoSuchElementException) { default }
+ fun getProperty(getter: (S)->Data.Stackable, default: ()->T) : T {
+ for (e in stackedData) { val d = getter(e); if (d.isSet) return d.value }; return default() }
+ fun getProperty(getter: (S)->Data.Stackable, default: T): T {
+ for (e in stackedData) { val d = getter(e); if (d.isSet) return d.value }; return default }
+ }
sealed interface Holder {
val v: T
@@ -18,9 +36,9 @@ object DSL {
operator fun invoke(configure: Target.()->Unit)
}
- /** Constructed by: [DSL.Data.child]. */
- class DirectHolder internal constructor(default: T, private val factory: ()->(T & Any))
- : Invokable {
+ /** Constructed by: [DSL.Data.childOrDefault] and [DSL.Data.childOrNull]. */
+ class DirectHolder internal constructor(default: T, private val factory: ()->(T & Any))
+ : Invokable {
private var _v: T = default
override val v: T get() = _v
@@ -28,7 +46,7 @@ object DSL {
}
/** Constructed by: [DSL.Data.subclassOf]. */
- class Generalized internal constructor(default: T): Holder {
+ class Generalized internal constructor(default: T): Holder {
private var _v: T = default
override val v: T get() = _v
@@ -41,19 +59,28 @@ object DSL {
* This constructs a new specialized child, configures it using the specified block,
* and stores it in the underlying generalized storage.
*/
- internal constructor(private val factory: ()->S) : Invokable {
+ internal constructor(private val factory: ()->S) : Invokable {
override val v: T get() = this@Generalized.v
override operator fun invoke(configure: S.()->Unit) { _v = resolve(factory, configure) }
}
}
/** Constructed by: [DSL.Data.integratedReceiver]. */
- class Integrated internal constructor(): Invokable<(T.() -> Unit)?, T> {
+ class Integrated internal constructor(): Invokable<(T.()->Unit)?, T> {
private var _v: (T.()->Unit)? = null
override val v: (T.()->Unit)? get() = _v
override operator fun invoke(configure: T.()->Unit) { _v = configure }
}
+ /** Constructed by: [DSL.Data.unsupported]. */
+ class Unsupported internal constructor(val error: String): Invokable {
+ override val v: Unit get() = Unit
+ override fun invoke(configure: T.() -> Unit) { throw UnsupportedOperationException(error); }
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): Nothing { throw UnsupportedOperationException(error) }
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Any) { throw UnsupportedOperationException(error) }
+ }
+
@DslMarker
annotation class Marker
@@ -61,24 +88,25 @@ object DSL {
@Marker
open class Data {
/**
- * Embeds a child; use as `val sub = child(::TypeOfSub)`.
- * Defaults to a default-constructed child.
+ * Embeds an optional child. Use as `val sub = childOrNull(::TypeOfSub)`.
+ * Defaults to `null`.
*
- * User code will invoke as `child { }`.
+ * User code will invoke as `sub { }`
* This constructs a new child and configures it using the specified block.
*/
- protected fun child(factory: ()->T): Invokable =
- DirectHolder(factory(), factory)
+ protected fun childOrNull(factory: ()->T): Invokable =
+ DirectHolder(null, factory)
/**
- * Embeds an optional child. Use as `val sub = childOrNull(::TypeOfSub)`.
- * Defaults to `null`.
+ * Embeds an optional child. Use as `val sub = childOrDefault(::TypeOfSub) { ... }
+ * Defaults to a child configured using the specified default block.
*
- * User code will invoke as `child { }`
+ * User code will invoke as `sub { }`
* This constructs a new child and configures it using the specified block.
+ * Note that the specified default block is **not** applied if user code configures the child.
*/
- protected fun childOrNull(factory: ()->T): Invokable =
- DirectHolder(null, factory)
+ protected fun childOrDefault(factory: ()->T, default: (T.()->Unit)? = null): Invokable =
+ DirectHolder(factory().also{ default?.invoke(it) }, factory)
/**
* Specifies a generalized holder of type T.
@@ -90,7 +118,7 @@ object DSL {
* Specialized invokable accessors can be spun off via `.option(::SpecializedClass)`.
* @see DSL.Generalized.option
*/
- protected fun subclassOf(): Generalized =
+ protected fun subclassOf(): Generalized =
Generalized(null)
/**
* Specifies a generalized holder of type T.
@@ -102,7 +130,7 @@ object DSL {
* Specialized invokable accessors can be spun off via `.option(::SpecializedClass)`.
* @see DSL.Generalized.option
*/
- protected fun subclassOf(default: T): Generalized =
+ protected fun subclassOf(default: T): Generalized =
Generalized(default)
/**
@@ -115,6 +143,28 @@ object DSL {
protected fun integratedReceiver(): Integrated =
Integrated()
+ /**
+ * Marks an inherited DSL substructure as unsupported. Attempts to use it throw [UnsupportedOperationException]. Use very sparingly.
+ */
+ protected fun unsupported(why: String): Unsupported =
+ Unsupported(why)
+
+ /**
+ * Convenience delegate for multiple points of configuration DSLs.
+ * It keeps track of whether the value has been explicitly set, and is compatible with [ConfigStack.getProperty].
+ *
+ * Use as `internal val _foo = Stackable(); var foo by _foo`, then access as `stack.getProperty(DSLType::_foo, default = 42)`.
+ */
+ internal class Stackable() {
+ private var _storage: T? = null
+ @Suppress("UNCHECKED_CAST")
+ internal val value: T get() { check(isSet); return _storage as T }
+ internal var isSet: Boolean = false
+ operator fun getValue(thisRef: Data, property: KProperty<*>): T { return value }
+ operator fun setValue(thisRef: Data, property: KProperty<*>, v: T) { _storage = v; isSet = true; }
+
+ }
+
/**
* Invoked by `DSL.resolve()` after the configuration block runs.
* Can be used for sanity checks.
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/DataTypes.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/DataTypes.kt
new file mode 100644
index 00000000..f06070e1
--- /dev/null
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/dsl/DataTypes.kt
@@ -0,0 +1,19 @@
+package at.asitplus.signum.supreme.dsl
+
+/** Tri-state setting for enabling a given feature.
+ * @see REQUIRED
+ * @see PREFERRED
+ * @see DISCOURAGED
+ */
+sealed interface FeaturePreference
+/** Marks this feature as non-negotiable and absolutely required.
+ If the feature is not available on the current platform, the operation may fail. */
+object REQUIRED : FeaturePreference
+/** Marks this feature as preferred.
+ If the feature is available on the current platform and with the specified configuration, it will be used.
+ If not, it will silently not be used. The effective state might be determined from the output. */
+object PREFERRED : FeaturePreference
+/** Marks this feature as discouraged.
+ If it is possible to complete the operation without using the feature, this will be done.
+ The feature will only be used if its use is required to allow the operation to succeed. */
+object DISCOURAGED : FeaturePreference
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/hash/DigestExtensions.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/hash/DigestExtensions.kt
index 1ffccc2c..20fbcdc9 100644
--- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/hash/DigestExtensions.kt
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/hash/DigestExtensions.kt
@@ -1,16 +1,7 @@
package at.asitplus.signum.supreme.hash
import at.asitplus.signum.indispensable.Digest
-import org.kotlincrypto.hash.sha1.SHA1
-import org.kotlincrypto.hash.sha2.SHA256
-import org.kotlincrypto.hash.sha2.SHA384
-import org.kotlincrypto.hash.sha2.SHA512
-operator fun Digest.invoke(): org.kotlincrypto.core.digest.Digest = when(this) {
- Digest.SHA1 -> SHA1()
- Digest.SHA256 -> SHA256()
- Digest.SHA384 -> SHA384()
- Digest.SHA512 -> SHA512()
-}
-inline fun Digest.digest(data: Sequence) = this().also { data.forEach(it::update) }.digest()
-inline fun Digest.digest(bytes: ByteArray) = this().digest(bytes)
+internal expect fun doDigest(digest: Digest, data: Sequence): ByteArray
+fun Digest.digest(data: Sequence) = doDigest(this, data)
+inline fun Digest.digest(bytes: ByteArray) = this.digest(sequenceOf(bytes))
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/os/Attestation.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/os/Attestation.kt
new file mode 100644
index 00000000..3ccfd17a
--- /dev/null
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/os/Attestation.kt
@@ -0,0 +1,125 @@
+package at.asitplus.signum.supreme.os
+
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.io.ByteArrayBase64UrlSerializer
+import at.asitplus.signum.indispensable.io.CertificateChainBase64UrlSerializer
+import at.asitplus.signum.indispensable.io.IosPublicKeySerializer
+import at.asitplus.signum.indispensable.io.X509CertificateBase64UrlSerializer
+import at.asitplus.signum.indispensable.pki.CertificateChain
+import at.asitplus.signum.indispensable.pki.X509Certificate
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonClassDiscriminator
+
+@Serializable
+@JsonClassDiscriminator("typ")
+sealed interface Attestation {
+ companion object {
+ fun fromJSON(v: String) = Json.decodeFromString(v)
+ }
+}
+
+@Serializable
+@SerialName("self")
+data class SelfAttestation (
+ @Serializable(with=X509CertificateBase64UrlSerializer::class)
+ @SerialName("x5c")
+ val certificate: X509Certificate) : Attestation
+
+@Serializable
+@SerialName("android-key")
+data class AndroidKeystoreAttestation (
+ @Serializable(with=CertificateChainBase64UrlSerializer::class)
+ @SerialName("x5c")
+ val certificateChain: CertificateChain) : Attestation
+
+@Serializable
+@SerialName("ios-appattest-assertion")
+data class IosLegacyHomebrewAttestation(
+ @Serializable(with=ByteArrayBase64UrlSerializer::class)
+ val attestation: ByteArray,
+ @Serializable(with=ByteArrayBase64UrlSerializer::class)
+ val assertion: ByteArray): Attestation {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is IosLegacyHomebrewAttestation) return false
+
+ if (!attestation.contentEquals(other.attestation)) return false
+ return assertion.contentEquals(other.assertion)
+ }
+
+ override fun hashCode(): Int {
+ var result = attestation.contentHashCode()
+ result = 31 * result + assertion.contentHashCode()
+ return result
+ }
+}
+
+val StrictJson = Json { ignoreUnknownKeys = true; isLenient = false }
+
+@Serializable
+@SerialName("ios-appattest")
+data class IosHomebrewAttestation(
+ @Serializable(with=ByteArrayBase64UrlSerializer::class)
+ val attestation: ByteArray,
+ @Serializable(with=ByteArrayBase64UrlSerializer::class)
+ val clientDataJSON: ByteArray): Attestation {
+
+ companion object { const val THE_PURPOSE = "ios app-attest: secure enclave protected key" }
+
+ @Serializable
+ @ConsistentCopyVisibility
+ data class ClientData private constructor(
+ private val purpose: String,
+ @Serializable(with=IosPublicKeySerializer::class)
+ val publicKey: CryptoPublicKey,
+ @Serializable(with=ByteArrayBase64UrlSerializer::class)
+ val challenge: ByteArray
+ ) {
+ constructor(publicKey: CryptoPublicKey, challenge: ByteArray) :
+ this(THE_PURPOSE, publicKey, challenge)
+
+ internal fun assertValidity() { if (purpose != THE_PURPOSE) throw IllegalStateException("Invalid purpose") }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as ClientData
+
+ if (purpose != other.purpose) return false
+ if (publicKey != other.publicKey) return false
+ return challenge.contentEquals(other.challenge)
+ }
+
+ override fun hashCode(): Int {
+ var result = purpose.hashCode()
+ result = 31 * result + publicKey.hashCode()
+ result = 31 * result + challenge.contentHashCode()
+ return result
+ }
+ }
+
+ val parsedClientData: ClientData by lazy {
+ StrictJson.decodeFromString(clientDataJSON.decodeToString())
+ .also(ClientData::assertValidity)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is IosHomebrewAttestation) return false
+
+ if (!attestation.contentEquals(other.attestation)) return false
+ return clientDataJSON.contentEquals(other.clientDataJSON)
+ }
+
+ override fun hashCode(): Int {
+ var result = attestation.contentHashCode()
+ result = 31 * result + clientDataJSON.contentHashCode()
+ return result
+ }
+}
+
+val Attestation.jsonEncoded: String get() = Json.encodeToString(this)
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/os/SigningProvider.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/os/SigningProvider.kt
new file mode 100644
index 00000000..6172c2f1
--- /dev/null
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/os/SigningProvider.kt
@@ -0,0 +1,207 @@
+package at.asitplus.signum.supreme.os
+
+import at.asitplus.KmmResult
+import at.asitplus.catching
+import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.RSAPadding
+import at.asitplus.signum.supreme.SignatureResult
+import at.asitplus.signum.supreme.dsl.DISCOURAGED
+import at.asitplus.signum.supreme.dsl.DSL
+import at.asitplus.signum.supreme.dsl.DSLConfigureFn
+import at.asitplus.signum.supreme.dsl.FeaturePreference
+import at.asitplus.signum.supreme.dsl.REQUIRED
+import at.asitplus.signum.supreme.sign.SignatureInput
+import at.asitplus.signum.supreme.sign.Signer
+import at.asitplus.signum.supreme.sign.SigningKeyConfiguration
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+open class SigningProviderSigningKeyConfigurationBase internal constructor() : SigningKeyConfiguration() {
+ /** Configure the signer that will be returned from [createSigningKey][SigningProviderI.createSigningKey] */
+ open val signer = integratedReceiver()
+}
+open class PlatformSigningKeyConfigurationBase internal constructor(): SigningProviderSigningKeyConfigurationBase() {
+ open class AttestationConfiguration internal constructor(): DSL.Data() {
+ /** The server-provided attestation challenge */
+ lateinit var challenge: ByteArray
+ override fun validate() {
+ require(this::challenge.isInitialized) { "Server-provided attestation challenge must be set" }
+ }
+ }
+
+ open class ProtectionFactorConfiguration internal constructor(): DSL.Data() {
+ /** Whether a biometric factor (fingerprint, facial recognition, ...) can authorize this key */
+ var biometry = true
+ /** Whether additional biometric factors can be added without invalidating the key */
+ var biometryWithNewFactors = false; set(v) { field = v; if (v) biometry = true }
+ /** Whether a device unlock code, PIN, etc. can authorize this key */
+ var deviceLock = true
+
+ override fun validate() {
+ require(biometry || deviceLock) { "At least one authentication factor must be permissible" }
+ require (biometry || !biometryWithNewFactors) { "You cannot allow future biometric factors but disallow current ones" }
+ }
+ }
+
+ open class ProtectionConfiguration internal constructor(): DSL.Data() {
+ /** The timeout before this key will need to be unlocked again. */
+ var timeout: Duration = 0.seconds
+ /** Which authentication factors can authorize this key;
+ * if multiple factors are specified, any one of them can authorize the key */
+ val factors = childOrDefault(::ProtectionFactorConfiguration)
+ }
+
+ open class SecureHardwareConfiguration: DSL.Data() {
+ /** Whether to use hardware-backed storage, such as Android Keymaster or Apple's Secure Enclave.
+ * @see FeaturePreference */
+ var backing: FeaturePreference = REQUIRED
+ open val attestation = childOrNull(::AttestationConfiguration)
+ open val protection = childOrNull(::ProtectionConfiguration)
+ override fun validate() {
+ super.validate()
+ require((backing != DISCOURAGED) || (attestation.v == null))
+ { "To obtain hardware attestation, enable secure hardware support (do not set backing = DISCOURAGED, use backing = PREFERRED or backing = REQUIRED instead)."}
+ }
+ }
+
+ /** Require that this key is stored in some kind of hardware-backed storage, such as Android Keymaster or Apple Secure Enclave. */
+ open val hardware = childOrNull(::SecureHardwareConfiguration)
+}
+
+open class ECSignerConfiguration internal constructor(): DSL.Data() {
+ /**
+ * Explicitly specify the digest to sign over.
+ * Omit to default to the only supported digest.
+ *
+ * If the key stored in hardware supports multiple digests, you need to explicitly specify the digest to use.
+ * (By default, hardware keys are configured to only support a single digest.)
+ *
+ * @see SigningKeyConfiguration.ECConfiguration.digests
+ */
+ var digest: Digest? = null; set(v) { digestSpecified = true; field = v }
+ internal var digestSpecified = false
+}
+open class RSASignerConfiguration internal constructor(): DSL.Data() {
+ /**
+ * Explicitly specify the digest to sign over.
+ * Omit to default to a reasonable default choice.
+ *
+ * If a key stored in hardware supports multiple digests, you need to explicitly specify the digest to use.
+ * (By default, hardware keys are configured to only support a single digest.)
+ *
+ * @see SigningKeyConfiguration.RSAConfiguration.digests
+ */
+ lateinit var digest: Digest
+ internal val digestSpecified get() = this::digest.isInitialized
+
+ /**
+ * Explicitly specify the padding to use.
+ * Omit to default to the only supported padding.
+ *
+ * If the key stored in hardware supports multiple padding modes, you need to explicitly specify the digest to use.
+ * (By default, hardware keys are configured to only support a single digest.)
+ *
+ * @see SigningKeyConfiguration.RSAConfiguration.paddings
+ */
+ lateinit var padding: RSAPadding
+ internal val paddingSpecified get() = this::padding.isInitialized
+
+
+}
+open class SignerConfiguration internal constructor(): DSL.Data() {
+ /** Algorithm-specific configuration for a returned ECDSA signer. Ignored for RSA keys. */
+ open val ec = childOrDefault(::ECSignerConfiguration)
+ /** Algorithm-specific configuration for a returned RSA signer. Ignored for ECDSA keys. */
+ open val rsa = childOrDefault(::RSASignerConfiguration)
+}
+
+open class UnlockPromptConfiguration: DSL.Data() {
+
+ internal val _message = Stackable()
+ /** The prompt message to show to the user when asking for unlock */
+ var message by _message
+
+ internal val _cancelText = Stackable()
+ /** The message to show on the cancellation button */
+ var cancelText by _cancelText
+
+ companion object {
+ const val defaultMessage = "Please authorize cryptographic signature"
+ const val defaultCancelText = "Cancel"
+ }
+}
+open class PlatformSignerConfigurationBase internal constructor(): SignerConfiguration() {
+ /** Configure the authorization prompt that will be shown to the user. */
+ open val unlockPrompt = childOrDefault(::UnlockPromptConfiguration)
+}
+
+open class PlatformSigningProviderSignerSigningConfigurationBase internal constructor(): DSL.Data() {
+ open val unlockPrompt = childOrDefault(::UnlockPromptConfiguration)
+}
+
+interface PlatformSigningProviderSigner
+ : Signer.WithAlias {
+
+ suspend fun trySetupUninterruptedSigning(configure: DSLConfigureFn = null) : KmmResult = KmmResult.success(Unit)
+ override suspend fun trySetupUninterruptedSigning() = trySetupUninterruptedSigning(null)
+
+ suspend fun sign(data: SignatureInput, configure: DSLConfigureFn = null) : SignatureResult<*>
+ suspend fun sign(data: ByteArray, configure: DSLConfigureFn = null) =
+ sign(SignatureInput(data), configure)
+ suspend fun sign(data: Sequence, configure: DSLConfigureFn = null) =
+ sign(SignatureInput(data), configure)
+ override suspend fun sign(data: SignatureInput) = sign(data, null)
+ override suspend fun sign(data: ByteArray) = sign(SignatureInput(data), null)
+ override suspend fun sign(data: Sequence) = sign(SignatureInput(data), null)
+}
+
+open class PlatformSigningProviderConfigurationBase internal constructor(): DSL.Data()
+internal expect fun getPlatformSigningProvider(configure: DSLConfigureFn): PlatformSigningProviderI<*,*,*>
+
+/** KT-71089 workaround
+ * @see PlatformSigningProvider */
+interface PlatformSigningProviderI,
+ out SignerConfigT: PlatformSignerConfigurationBase,
+ out KeyConfigT: PlatformSigningKeyConfigurationBase<*>>
+ : SigningProviderI {
+
+ companion object {
+ operator fun invoke(configure: DSLConfigureFn = null) =
+ catching { getPlatformSigningProvider(configure) }
+ }
+}
+/**
+ * An interface to some underlying persistent storage for private key material. Stored keys are identified by a unique string "alias" for each key.
+ * You can [create signing keys][createSigningKey], [get signers for existing keys][getSignerForKey], or [delete signing keys][deleteSigningKey].
+ *
+ * To obtain a platform signing provider in platform-agnostic code, use `PlatformSigningProvider`.
+ * In platform-specific code, it is currently recommended to directly interface with your platform signing provider to get platform-specific functionality.
+ * (Platform-specific types for `PlatformSigningProvider` are currently blocked by KT-71036.)
+ *
+ * Created keys can be configured using the [SigningKeyConfiguration] DSL.
+ * Signers can be configured using the [SignerConfiguration] DSL.
+ * When creating a key, the returned signer's configuration is embedded in the signing key configuration as `signer {}`.
+ *
+ * @see JKSProvider
+ * @see AndroidKeyStoreProvider
+ * @see IosKeychainProvider
+ */
+val PlatformSigningProvider get() = getPlatformSigningProvider(null)
+
+/** KT-71089 workaround
+ * @see SigningProvider */
+interface SigningProviderI> {
+ suspend fun createSigningKey(alias: String, configure: DSLConfigureFn = null): KmmResult
+ suspend fun getSignerForKey(alias: String, configure: DSLConfigureFn = null): KmmResult
+ suspend fun deleteSigningKey(alias: String): KmmResult
+
+ companion object {
+ fun Platform(configure: DSLConfigureFn = null) =
+ getPlatformSigningProvider(configure)
+ }
+}
+
+/** @see PlatformSigningProvider */
+typealias SigningProvider = SigningProviderI<*,*,*>
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt
new file mode 100644
index 00000000..aab02c3a
--- /dev/null
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt
@@ -0,0 +1,127 @@
+package at.asitplus.signum.supreme.sign
+
+import at.asitplus.KmmResult
+import at.asitplus.catching
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.RSAPadding
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.indispensable.nativeDigest
+import at.asitplus.signum.supreme.HazardousMaterials
+import at.asitplus.signum.supreme.dsl.DSL
+import at.asitplus.signum.supreme.dsl.DSLConfigureFn
+import at.asitplus.signum.supreme.os.SignerConfiguration
+
+internal expect fun makeEphemeralKey(configuration: EphemeralSigningKeyConfiguration) : EphemeralKey
+
+open class EphemeralSigningKeyConfigurationBase internal constructor(): SigningKeyConfiguration() {
+ class ECConfiguration internal constructor(): SigningKeyConfiguration.ECConfiguration() {
+ init { digests = (Digest.entries.asSequence() + sequenceOf(null)).toSet() }
+ }
+ override val ec = _algSpecific.option(::ECConfiguration)
+ class RSAConfiguration internal constructor(): SigningKeyConfiguration.RSAConfiguration() {
+ init { digests = Digest.entries.toSet(); paddings = RSAPadding.entries.toSet() }
+ }
+ override val rsa = _algSpecific.option(::RSAConfiguration)
+}
+expect class EphemeralSigningKeyConfiguration internal constructor(): EphemeralSigningKeyConfigurationBase
+
+typealias EphemeralSignerConfigurationBase = SignerConfiguration
+expect class EphemeralSignerConfiguration internal constructor(): SignerConfiguration
+
+/**
+ * An ephemeral keypair, not stored in any kind of persistent storage.
+ * Can be either [EC] or [RSA]. Has a [CryptoPublicKey], and you can obtain a [Signer] from it.
+ *
+ * To generate a key, use
+ * ```
+ * EphemeralKey {
+ * /* optional configuration */
+ * }
+ * ```
+ */
+sealed interface EphemeralKey {
+ val publicKey: CryptoPublicKey
+
+ /** Create a signer that signs using this [EphemeralKey].
+ * @see EphemeralSignerConfiguration */
+ fun signer(configure: DSLConfigureFn = null): KmmResult
+
+ /** An [EphemeralKey] suitable for ECDSA operations. */
+ interface EC: EphemeralKey {
+ override val publicKey: CryptoPublicKey.EC
+ override fun signer(configure: DSLConfigureFn): KmmResult
+ }
+ /** An [EphemeralKey] suitable for RSA operations. */
+ interface RSA: EphemeralKey {
+ override val publicKey: CryptoPublicKey.Rsa
+ override fun signer(configure: DSLConfigureFn): KmmResult
+ }
+ companion object {
+ operator fun invoke(configure: DSLConfigureFn = null) =
+ catching { makeEphemeralKey(DSL.resolve(::EphemeralSigningKeyConfiguration, configure)) }
+ }
+}
+
+internal sealed class EphemeralKeyBase
+ (internal val privateKey: PrivateKeyT): EphemeralKey {
+
+ class EC(
+ private val signerFactory: (EphemeralSignerConfiguration, PrivateKeyT, CryptoPublicKey.EC, SignatureAlgorithm.ECDSA)->SignerT,
+ privateKey: PrivateKeyT, override val publicKey: CryptoPublicKey.EC,
+ val digests: Set) : EphemeralKeyBase(privateKey), EphemeralKey.EC {
+
+ override fun signer(configure: DSLConfigureFn): KmmResult = catching {
+ val config = DSL.resolve(::EphemeralSignerConfiguration, configure)
+ val alg = config.ec.v
+ val digest = when (alg.digestSpecified) {
+ true -> {
+ require (digests.contains(alg.digest))
+ { "Digest ${alg.digest} unsupported (supported: ${digests.joinToString(",")}" }
+ alg.digest
+ }
+ false ->
+ sequenceOf(publicKey.curve.nativeDigest, Digest.SHA256, Digest.SHA384, Digest.SHA512)
+ .firstOrNull(digests::contains) ?: digests.first()
+ }
+ return@catching signerFactory(config, privateKey, publicKey, SignatureAlgorithm.ECDSA(digest, publicKey.curve))
+ }
+ }
+
+ class RSA(
+ private val signerFactory: (EphemeralSignerConfiguration, PrivateKeyT, CryptoPublicKey.Rsa, SignatureAlgorithm.RSA)->SignerT,
+ privateKey: PrivateKeyT, override val publicKey: CryptoPublicKey.Rsa,
+ val digests: Set, val paddings: Set) : EphemeralKeyBase(privateKey), EphemeralKey.RSA {
+
+ override fun signer(configure: DSLConfigureFn): KmmResult = catching {
+ val config = DSL.resolve(::EphemeralSignerConfiguration, configure)
+ val alg = config.rsa.v
+ val digest = when (alg.digestSpecified) {
+ true -> {
+ require (digests.contains(alg.digest))
+ { "Digest ${alg.digest} unsupported (supported: ${digests.joinToString(", ")}" }
+ alg.digest
+ }
+ false -> when {
+ digests.contains(Digest.SHA256) -> Digest.SHA256
+ digests.contains(Digest.SHA384) -> Digest.SHA384
+ digests.contains(Digest.SHA512) -> Digest.SHA512
+ else -> digests.first()
+ }
+ }
+ val padding = when (alg.paddingSpecified) {
+ true -> {
+ require (paddings.contains(alg.padding))
+ { "Padding ${alg.padding} unsupported (supported: ${paddings.joinToString(", ")}" }
+ alg.padding
+ }
+ false -> when {
+ paddings.contains(RSAPadding.PSS) -> RSAPadding.PSS
+ paddings.contains(RSAPadding.PKCS1) -> RSAPadding.PKCS1
+ else -> paddings.first()
+ }
+ }
+ return@catching signerFactory(config, privateKey, publicKey, SignatureAlgorithm.RSA(digest, padding))
+ }
+ }
+}
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/SignatureInput.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/SignatureInput.kt
index 2212cc6d..aca82702 100644
--- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/SignatureInput.kt
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/SignatureInput.kt
@@ -2,6 +2,7 @@ package at.asitplus.signum.supreme.sign
import at.asitplus.catching
import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.SignatureAlgorithm
import at.asitplus.signum.indispensable.misc.BitLength
import at.asitplus.signum.supreme.hash.digest
import com.ionspin.kotlin.bignum.integer.BigInteger
@@ -59,3 +60,9 @@ class SignatureInput private constructor (
}
}
}
+
+val SignatureAlgorithm.preHashedSignatureFormat: SignatureInputFormat get() = when(this) {
+ is SignatureAlgorithm.RSA -> this.digest
+ is SignatureAlgorithm.ECDSA -> this.digest
+ else -> TODO("HMAC unsupported")
+}
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt
new file mode 100644
index 00000000..8bd32255
--- /dev/null
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt
@@ -0,0 +1,141 @@
+package at.asitplus.signum.supreme.sign
+
+import at.asitplus.KmmResult
+import at.asitplus.signum.indispensable.CryptoPublicKey
+import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.ECCurve
+import at.asitplus.signum.indispensable.RSAPadding
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.indispensable.nativeDigest
+import at.asitplus.signum.supreme.SignatureResult
+import at.asitplus.signum.supreme.dsl.DSL
+import at.asitplus.signum.supreme.dsl.DSLConfigureFn
+import at.asitplus.signum.supreme.os.Attestation
+import at.asitplus.signum.supreme.os.SigningProvider
+import com.ionspin.kotlin.bignum.integer.BigInteger
+
+/** DSL for configuring a signing key.
+ *
+ * Defaults to an elliptic-curve key with a reasonable default configuration.
+ *
+ * @see ec
+ * @see rsa
+ */
+open class SigningKeyConfiguration internal constructor(): DSL.Data() {
+ sealed class AlgorithmSpecific: DSL.Data()
+
+ internal val _algSpecific = subclassOf(default = ECConfiguration())
+ /** Generates an elliptic-curve key. */
+ open val ec = _algSpecific.option(::ECConfiguration)
+ /** Generates an RSA key. */
+ open val rsa = _algSpecific.option(::RSAConfiguration)
+
+ open class ECConfiguration internal constructor() : AlgorithmSpecific() {
+ /** The [ECCurve] on which to generate the key. Defaults to [P-256][ECCurve.SECP_256_R_1] */
+ var curve: ECCurve = ECCurve.SECP_256_R_1
+
+ private var _digests: Set? = null
+ /** The digests supported by the key. If not specified, supports the curve's native digest only. */
+ open var digests: Set
+ get() = _digests ?: setOf(curve.nativeDigest)
+ set(v) { _digests = v }
+ }
+
+ open class RSAConfiguration internal constructor(): AlgorithmSpecific() {
+ companion object { val F0 = BigInteger(3); val F4 = BigInteger(65537) }
+ /** The digests supported by the key. If not specified, defaults to [SHA256][Digest.SHA256]. */
+ open var digests: Set = setOf(Digest.SHA256)
+ /** The paddings supported by the key. If not specified, defaults to [RSA-PSS][RSAPadding.PSS]. */
+ open var paddings: Set = setOf(RSAPadding.PSS)
+ /** The bit size of the generated key. If not specified, defaults to 3072 bits. */
+ var bits: Int = 3072
+ /** The public exponent to use. Defaults to F4.
+ * This is treated as advisory, and may be ignored by some platforms. */
+ var publicExponent: BigInteger = F4
+ }
+}
+
+/**
+ * Shared interface of all objects that can sign data.
+ * Signatures are created using the [signatureAlgorithm], and can be verified using [publicKey], potentially with a [verifierFor] this object.
+ *
+ * Signers for your platform can be accessed using your platform's [SigningProvider].
+ *
+ * Ephemeral signers can be obtained using
+ * ```
+ * Signer.Ephemeral {
+ * /* optional key configuration */
+ * }
+ * ```
+ * This will generate a throwaway [EphemeralKey] and return a Signer for it.
+ *
+ * Any actual instantiation will have an [AlgTrait], which will be either [ECDSA] or [RSA].
+ * Instantiations may also be [WithAlias], usually because they come from a [SigningProvider].
+ * They may also be [Attestable].
+ *
+ * Some signers [mayRequireUserUnlock]. If needed, they will ask for user interaction when you try to [sign] data.
+ * You can try to authenticate a signer ahead of time using [trySetupUninterruptedSigning]; but it might do nothing for some Signers.
+ * There is never a guarantee that signing is uninterrupted if [mayRequireUserUnlock] is true.
+ *
+ */
+interface Signer {
+ val signatureAlgorithm: SignatureAlgorithm
+ val publicKey: CryptoPublicKey
+ /** Whether the signer may ask for user interaction when [sign] is called */
+ val mayRequireUserUnlock: Boolean get() = true
+
+ /** Any [Signer] instantiation must be [ECDSA] or [RSA] */
+ sealed interface AlgTrait: Signer
+
+ /** A [Signer] that signs using ECDSA. */
+ interface ECDSA: AlgTrait {
+ override val signatureAlgorithm: SignatureAlgorithm.ECDSA
+ override val publicKey: CryptoPublicKey.EC
+ }
+
+ /** A [Signer] that signs using RSA. */
+ interface RSA: AlgTrait {
+ override val signatureAlgorithm: SignatureAlgorithm.RSA
+ override val publicKey: CryptoPublicKey.Rsa
+ }
+
+ /** Some [Signer]s are retrieved from a signing provider, such as a key store, and have a string [alias]. */
+ interface WithAlias: Signer {
+ val alias: String
+ }
+
+ /** Some [Signer]s might have an attestation of some sort */
+ interface Attestable: Signer {
+ val attestation: AttestationT?
+ }
+
+ /** Try to ensure that the Signer is ready to immediately sign data, on a best-effort basis.
+ * For example, if user authorization allows signing for a given timeframe, this will prompts for authorization now.
+ *
+ * If ahead-of-time authorization makes no sense for this [Signer], does nothing. */
+ suspend fun trySetupUninterruptedSigning(): KmmResult = KmmResult.success(Unit)
+
+ /** Signs data. Might ask for user confirmation first if this [Signer] [mayRequireUserUnlock]. */
+ suspend fun sign(data: SignatureInput): SignatureResult<*>
+ suspend fun sign(data: ByteArray) = sign(SignatureInput(data))
+ suspend fun sign(data: Sequence) = sign(SignatureInput(data))
+
+ companion object {
+ fun Ephemeral(configure: DSLConfigureFn = null) =
+ EphemeralKey(configure).transform(EphemeralKey::signer)
+ }
+}
+
+/**
+ * Get a verifier for signatures generated by this [Signer].
+ * @see SignatureAlgorithm.verifierFor
+ */
+fun Signer.makeVerifier(configure: ConfigurePlatformVerifier = null) = signatureAlgorithm.verifierFor(publicKey, configure)
+
+/**
+ * Gets a platform verifier for signatures generated by this [Signer].
+ * @see SignatureAlgorithm.platformVerifierFor
+ */
+fun Signer.makePlatformVerifier(configure: ConfigurePlatformVerifier = null) = signatureAlgorithm.platformVerifierFor(publicKey, configure)
+
+val Signer.ECDSA.curve get() = publicKey.curve
diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Verifier.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Verifier.kt
index 3b0fbd79..728aefa9 100644
--- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Verifier.kt
+++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Verifier.kt
@@ -2,6 +2,7 @@ package at.asitplus.signum.supreme.sign
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.recoverCatching
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.CryptoSignature
import at.asitplus.signum.indispensable.SignatureAlgorithm
@@ -54,18 +55,16 @@ sealed interface KotlinVerifier: Verifier
@Throws(UnsupportedCryptoException::class)
internal expect fun checkAlgorithmKeyCombinationSupportedByECDSAPlatformVerifier
(signatureAlgorithm: SignatureAlgorithm.ECDSA, publicKey: CryptoPublicKey.EC,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
internal expect fun verifyECDSAImpl
(signatureAlgorithm: SignatureAlgorithm.ECDSA, publicKey: CryptoPublicKey.EC,
data: SignatureInput, signature: CryptoSignature.EC,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
+
class PlatformECDSAVerifier
internal constructor (signatureAlgorithm: SignatureAlgorithm.ECDSA, publicKey: CryptoPublicKey.EC,
- configure: ConfigurePlatformVerifier
- )
+ configure: ConfigurePlatformVerifier)
: Verifier.EC(signatureAlgorithm, publicKey), PlatformVerifier {
private val config = DSL.resolve(::PlatformVerifierConfiguration, configure)
@@ -82,19 +81,17 @@ class PlatformECDSAVerifier
@Throws(UnsupportedCryptoException::class)
internal expect fun checkAlgorithmKeyCombinationSupportedByRSAPlatformVerifier
(signatureAlgorithm: SignatureAlgorithm.RSA, publicKey: CryptoPublicKey.Rsa,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
/** data is guaranteed to be in RAW_BYTES format. failure should throw. */
internal expect fun verifyRSAImpl
(signatureAlgorithm: SignatureAlgorithm.RSA, publicKey: CryptoPublicKey.Rsa,
data: SignatureInput, signature: CryptoSignature.RSAorHMAC,
- config: PlatformVerifierConfiguration
-)
+ config: PlatformVerifierConfiguration)
+
class PlatformRSAVerifier
internal constructor (signatureAlgorithm: SignatureAlgorithm.RSA, publicKey: CryptoPublicKey.Rsa,
- configure: ConfigurePlatformVerifier
- )
+ configure: ConfigurePlatformVerifier)
: Verifier.RSA(signatureAlgorithm, publicKey), PlatformVerifier {
private val config = DSL.resolve(::PlatformVerifierConfiguration, configure)
@@ -169,25 +166,22 @@ fun SignatureAlgorithm.platformVerifierFor
private fun SignatureAlgorithm.verifierForImpl
(publicKey: CryptoPublicKey, configure: ConfigurePlatformVerifier,
- allowKotlin: Boolean): KmmResult =
+ allowKotlin: Boolean): KmmResult =
when (this) {
is SignatureAlgorithm.ECDSA -> {
- require(publicKey is CryptoPublicKey.EC)
- { "Non-EC public key passed to ECDSA algorithm"}
- verifierForImpl(publicKey, configure, allowKotlin)
+ if(publicKey !is CryptoPublicKey.EC)
+ KmmResult.failure(IllegalArgumentException("Non-EC public key passed to ECDSA algorithm"))
+ else
+ verifierForImpl(publicKey, configure, allowKotlin)
}
is SignatureAlgorithm.RSA -> {
- require(publicKey is CryptoPublicKey.Rsa)
- { "Non-RSA public key passed to RSA algorithm"}
- verifierForImpl(publicKey, configure, allowKotlin)
+ if (publicKey !is CryptoPublicKey.Rsa)
+ KmmResult.failure(IllegalArgumentException("Non-RSA public key passed to RSA algorithm"))
+ else
+ verifierForImpl(publicKey, configure, allowKotlin)
}
- is SignatureAlgorithm.HMAC -> throw UnsupportedCryptoException("HMAC is unsupported")
- }
-
-private fun KmmResult.recoverCatching(fn: (Throwable)->R): KmmResult =
- when (val x = exceptionOrNull()) {
- null -> this
- else -> catching { fn(x) }
+ is SignatureAlgorithm.HMAC ->
+ KmmResult.failure(IllegalArgumentException("HMAC is unsupported"))
}
/**
@@ -219,7 +213,7 @@ fun SignatureAlgorithm.ECDSA.platformVerifierFor
private fun SignatureAlgorithm.ECDSA.verifierForImpl
(publicKey: CryptoPublicKey.EC, configure: ConfigurePlatformVerifier,
- allowKotlin: Boolean): KmmResult =
+ allowKotlin: Boolean): KmmResult =
catching { PlatformECDSAVerifier(this, publicKey, configure) }
.recoverCatching {
if (allowKotlin && (it is UnsupportedCryptoException))
@@ -256,7 +250,7 @@ fun SignatureAlgorithm.RSA.platformVerifierFor
private fun SignatureAlgorithm.RSA.verifierForImpl
(publicKey: CryptoPublicKey.Rsa, configure: ConfigurePlatformVerifier,
- allowKotlin: Boolean): KmmResult =
+ allowKotlin: Boolean): KmmResult =
catching { PlatformRSAVerifier(this, publicKey, configure) }
/** @see [SignatureAlgorithm.verifierFor] */
diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/TestUtils.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/TestUtils.kt
index 66074a47..8aba0429 100644
--- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/TestUtils.kt
+++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/TestUtils.kt
@@ -3,8 +3,6 @@ package at.asitplus.signum.supreme
import at.asitplus.KmmResult
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
-import kotlinx.coroutines.Runnable
-import kotlin.reflect.KClass
internal object succeed: Matcher> {
override fun test(value: KmmResult<*>) =
diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLInheritanceDemonstration.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLInheritanceDemonstration.kt
index af9e800c..6883ca25 100644
--- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLInheritanceDemonstration.kt
+++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLInheritanceDemonstration.kt
@@ -1,6 +1,5 @@
package at.asitplus.signum.supreme.dsl
-import at.asitplus.signum.supreme.dsl.DSL
import io.kotest.assertions.fail
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
@@ -13,7 +12,7 @@ private open class GenericOptions internal constructor(): DSL.Data() {
var genericSubValue: Int = 42
}
/* expose GenericSubOptions as a nested DSL child */
- open val subValue = child(GenericOptions::GenericSubOptions)
+ open val subValue = childOrDefault(::GenericSubOptions)
}
/* This is a more specific version of GenericOptions */
@@ -27,7 +26,7 @@ private class SpecificOptions internal constructor(): GenericOptions() {
var anotherSpecificSubValue: String? = null
}
/* this shadows the subValue member on the superclass with a more specific version */
- override val subValue = child(SpecificOptions::SpecificSubOptions)
+ override val subValue = childOrDefault(::SpecificSubOptions)
}
open class DSLInheritanceDemonstration : FreeSpec({
diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLVarianceDemonstration.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLVarianceDemonstration.kt
index c310cb42..22141a15 100644
--- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLVarianceDemonstration.kt
+++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/dsl/DSLVarianceDemonstration.kt
@@ -1,6 +1,5 @@
package at.asitplus.signum.supreme.dsl
-import at.asitplus.signum.supreme.dsl.DSL
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
@@ -21,8 +20,8 @@ private class Settings: DSL.Data() {
/* this is null by default; a default could be explicitly specified, making this non-nullable */
internal val _flavor = subclassOf()
/* and then we define user-visible accessors for the different flavors */
- val banana = _flavor.option(Settings::BananaFlavor)
- val strawberry = _flavor.option(Settings::StrawberryFlavor)
+ val banana = _flavor.option(::BananaFlavor)
+ val strawberry = _flavor.option(::StrawberryFlavor)
override fun validate() {
require(_flavor.v != null)
diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt
new file mode 100644
index 00000000..1d3d68a6
--- /dev/null
+++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt
@@ -0,0 +1,138 @@
+package at.asitplus.signum.supreme.sign
+
+import at.asitplus.signum.indispensable.Digest
+import at.asitplus.signum.indispensable.ECCurve
+import at.asitplus.signum.indispensable.RSAPadding
+import at.asitplus.signum.indispensable.SignatureAlgorithm
+import at.asitplus.signum.indispensable.nativeDigest
+import at.asitplus.signum.supreme.signature
+import at.asitplus.signum.supreme.succeed
+import com.ionspin.kotlin.bignum.integer.Quadruple
+import io.kotest.core.spec.style.FreeSpec
+import io.kotest.datatest.withData
+import io.kotest.matchers.collections.shouldBeIn
+import io.kotest.matchers.collections.shouldNotBeIn
+import io.kotest.matchers.should
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNot
+import io.kotest.matchers.types.shouldBeInstanceOf
+import kotlin.random.Random
+
+class EphemeralSignerCommonTests : FreeSpec({
+ "Functional" - {
+ "RSA" - {
+ withData(
+ nameFn = { (pad, dig, bits, pre) -> "$dig/$pad/${bits}bit${if (pre) "/pre" else ""}" },
+ sequence {
+ RSAPadding.entries.forEach { padding ->
+ Digest.entries.forEach { digest ->
+ when {
+ digest == Digest.SHA512 && padding == RSAPadding.PSS
+ -> listOf(2048, 3072, 4096)
+ digest == Digest.SHA384 || digest == Digest.SHA512 || padding == RSAPadding.PSS
+ -> listOf(1024,2048,3072,4096)
+ else
+ -> listOf(512, 1024, 2048, 3072, 4096)
+ }.forEach { keySize ->
+ yield(Quadruple(padding, digest, keySize, false))
+ yield(Quadruple(padding, digest, keySize, true))
+ }
+ }
+ }
+ }) { (padding, digest, keySize, preHashed) ->
+ val data = Random.Default.nextBytes(64)
+ val signer: Signer
+ val signature = try {
+ signer = Signer.Ephemeral {
+ rsa {
+ digests = setOf(digest); paddings = setOf(padding); bits = keySize
+ }
+ }.getOrThrow()
+ signer.sign(SignatureInput(data).let {
+ if (preHashed) it.convertTo(digest).getOrThrow() else it
+ }).signature
+ } catch (x: UnsupportedOperationException) {
+ return@withData
+ }
+ signer.signatureAlgorithm.shouldBeInstanceOf().let {
+ it.digest shouldBe digest
+ it.padding shouldBe padding
+ }
+
+ val verifier = signer.makeVerifier().getOrThrow()
+ verifier.verify(data, signature) should succeed
+ }
+ }
+ "ECDSA" - {
+ withData(
+ nameFn = { (crv, dig, pre) -> "$crv/$dig${if (pre) "/pre" else ""}" },
+ sequence {
+ ECCurve.entries.forEach { curve ->
+ Digest.entries.forEach { digest ->
+ yield(Triple(curve, digest, false))
+ yield(Triple(curve, digest, true))
+ }
+ }
+ }) { (crv, digest, preHashed) ->
+ val signer =
+ Signer.Ephemeral { ec { curve = crv; digests = setOf(digest) } }.getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf().let {
+ it.digest shouldBe digest
+ it.requiredCurve shouldBeIn setOf(null, crv)
+ }
+ val data = Random.Default.nextBytes(64)
+ val signature = signer.sign(SignatureInput(data).let {
+ if (preHashed) it.convertTo(digest).getOrThrow() else it
+ }).signature
+
+ val verifier = signer.makeVerifier().getOrThrow()
+ verifier.verify(data, signature) should succeed
+ }
+ }
+ }
+ "Configuration" - {
+ "ECDSA" - {
+ "No digest specified (defaults to native)" {
+ val curve = Random.of(ECCurve.entries)
+ val key = EphemeralKey { ec { this.curve = curve } }.getOrThrow()
+ val signer = key.signer().getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf().digest shouldBe curve.nativeDigest
+ }
+ "No digest specified, native disallowed, still succeeds" {
+ val curve = Random.of(ECCurve.entries)
+ val key = EphemeralKey { ec { this.curve = curve; digests = Digest.entries.filter { it != curve.nativeDigest }.toSet() } }.getOrThrow()
+ val signer = key.signer().getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf().digest shouldNotBeIn setOf(curve.nativeDigest, null)
+ }
+ "All digests legal by default" {
+ val curve = Random.of(ECCurve.entries)
+ val key = EphemeralKey { ec { this.curve = curve } }.getOrThrow()
+ val nonNativeDigest = Random.of(Digest.entries.filter {it != curve.nativeDigest})
+ val signer = key.signer { ec { digest = nonNativeDigest } }.getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf().digest shouldBe nonNativeDigest
+ }
+ "Illegal digests should fail" {
+ val curve = Random.of(ECCurve.entries)
+ val key = EphemeralKey { ec { this.curve = curve; digests = Digest.entries.filter {it != curve.nativeDigest}.toSet() } }.getOrThrow()
+ key.signer{ ec { digest = curve.nativeDigest } } shouldNot succeed
+ }
+ "Null digest should work as a default" {
+ val key = EphemeralKey { ec { this.curve = Random.of(ECCurve.entries); digests = setOf(null) } }.getOrThrow()
+ val signer = key.signer().getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf().digest shouldBe null
+ }
+ "Null digest should work if explicitly specified" {
+ val key = EphemeralKey { ec {} }.getOrThrow()
+ val signer = key.signer { ec { digest = null } }.getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf().digest shouldBe null
+ }
+ }
+ "RSA" - {
+ "No digest specified" {
+ val key = EphemeralKey { rsa {} }.getOrThrow()
+ val signer = key.signer().getOrThrow()
+ signer.signatureAlgorithm.shouldBeInstanceOf()
+ }
+ }
+ }
+})
diff --git a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/InteropUtils.kt b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/InteropUtils.kt
index ec157da6..a6e564af 100644
--- a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/InteropUtils.kt
+++ b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/InteropUtils.kt
@@ -3,24 +3,103 @@
package at.asitplus.signum.supreme
import kotlinx.cinterop.*
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import platform.CoreFoundation.CFDictionaryCreateMutable
+import platform.CoreFoundation.CFDictionaryGetValue
+import platform.CoreFoundation.CFDictionaryRef
+import platform.CoreFoundation.CFDictionarySetValue
+import platform.CoreFoundation.CFErrorRefVar
+import platform.CoreFoundation.CFMutableDictionaryRef
+import platform.CoreFoundation.CFTypeRef
+import platform.CoreFoundation.kCFBooleanFalse
+import platform.CoreFoundation.kCFBooleanTrue
+import platform.CoreFoundation.kCFTypeDictionaryKeyCallBacks
+import platform.CoreFoundation.kCFTypeDictionaryValueCallBacks
import platform.Foundation.CFBridgingRelease
+import platform.Foundation.CFBridgingRetain
import platform.Foundation.NSData
import platform.Foundation.NSError
import platform.Foundation.create
+import platform.Security.SecCopyErrorMessageString
+import platform.darwin.OSStatus
import platform.posix.memcpy
+import kotlin.experimental.ExperimentalNativeApi
+import kotlin.native.ref.createCleaner
+
+@OptIn(ExperimentalNativeApi::class)
+class AutofreeVariable> internal constructor(
+ arena: Arena,
+ private val variable: CPointerVarOf) {
+ companion object {
+ internal inline operator fun > invoke(): AutofreeVariable {
+ val arena = Arena()
+ val variable = arena.alloc>()
+ return AutofreeVariable(arena, variable)
+ }
+ }
+ @Suppress("UNUSED")
+ private val cleaner = createCleaner(arena, Arena::clear)
+ internal val ptr get() = variable.ptr
+ internal val value get() = variable.value
+}
-@OptIn(ExperimentalForeignApi::class)
internal fun NSData.toByteArray(): ByteArray = ByteArray(length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), bytes, length)
}
}
-@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
+@OptIn(BetaInteropApi::class)
internal fun ByteArray.toNSData(): NSData = memScoped {
NSData.create(bytes = allocArrayOf(this@toNSData), length = this@toNSData.size.toULong())
}
+private fun NSError.toNiceString(): String {
+ val sb = StringBuilder("[${if(domain != null) "$domain error, " else ""}code $code] $localizedDescription\n")
+ localizedFailureReason?.let { sb.append("Because: $it") }
+ localizedRecoverySuggestion?.let { sb.append("Try: $it") }
+ localizedRecoveryOptions?.let { sb.append("Try also:\n - ${it.joinToString("\n - ")}\n") }
+ return sb.toString()
+}
+
+class CFCryptoOperationFailed(thing: String, val osStatus: OSStatus) : CryptoOperationFailed(buildMessage(thing, osStatus)) {
+ companion object {
+ private fun buildMessage(thing: String, osStatus: OSStatus): String {
+ val errorMessage = SecCopyErrorMessageString(osStatus, null).takeFromCF()
+ return "Failed to $thing: [code $osStatus] ${errorMessage ?: "unspecified security error"}"
+ }
+ }
+}
+
+class CoreFoundationException(val nsError: NSError): Throwable(nsError.toNiceString())
+internal class corecall private constructor(val error: CPointer) {
+ /** Helper for calling Core Foundation functions, and bridging exceptions across.
+ *
+ * Usage:
+ * ```
+ * corecall { SomeCoreFoundationFunction(arg1, arg2, ..., error) }
+ * ```
+ * `error` is provided by the implicit receiver object, and will be mapped to a
+ * `CoreFoundationException` if an error occurs.
+ */
+ companion object {
+ @OptIn(BetaInteropApi::class)
+ operator fun invoke(call: corecall.()->T?) : T {
+ memScoped {
+ val errorH = alloc()
+ val result = corecall(errorH.ptr).call()
+ val error = errorH.value
+ when {
+ (result != null) && (error == null) -> return result
+ (result == null) && (error != null) ->
+ throw CoreFoundationException(error.takeFromCF())
+ else -> throw IllegalStateException("Invalid state returned by Core Foundation call")
+ }
+ }
+ }
+ }
+}
class SwiftException(message: String): Throwable(message)
internal class swiftcall private constructor(val error: CPointer>) {
/** Helper for calling swift-objc-mapped functions, and bridging exceptions across.
@@ -41,10 +120,73 @@ internal class swiftcall private constructor(val error: CPointer return result
- (result == null) && (error != null) -> throw SwiftException(error.localizedDescription)
+ (result == null) && (error != null) -> throw SwiftException(error.toNiceString())
+ else -> throw IllegalStateException("Invalid state returned by Swift")
+ }
+ }
+ }
+ }
+}
+
+internal class swiftasync private constructor(val callback: (T?, NSError?)->Unit) {
+ /** Helper for calling swift-objc-mapped async functions, and bridging exceptions across.
+ *
+ * Usage:
+ * ```
+ * swiftasync { SwiftObj.func(arg1, arg2, .., argN, callback) }
+ * ```
+ * `error` is provided by the implicit receiver object, and will be mapped to a
+ * `SwiftException` if the swift call throws.
+ */
+ companion object {
+ suspend operator fun invoke(call: swiftasync.()->Unit): T {
+ var result: T? = null
+ var error: NSError? = null
+ val mut = Mutex(true)
+ swiftasync { res, err -> result = res; error = err; mut.unlock() }.call()
+ mut.withLock {
+ val res = result
+ val err = error
+ when {
+ (res != null) && (err == null) -> return res
+ (res == null) && (err != null) -> throw SwiftException(err.toNiceString())
else -> throw IllegalStateException("Invalid state returned by Swift")
}
}
}
}
}
+
+internal inline fun Any?.giveToCF() = when(this) {
+ null -> this
+ is Boolean -> if (this) kCFBooleanTrue else kCFBooleanFalse
+ is CValuesRef<*> -> this
+ else -> CFBridgingRetain(this)
+} as T
+internal inline fun