Skip to content

Commit

Permalink
Provider: Signing (#94)
Browse files Browse the repository at this point in the history
* 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

---------

Co-authored-by: Bernd Prünster <bernd.pruenster@a-sit.at>
  • Loading branch information
iaik-jheher and JesusMcCloud authored Sep 4, 2024
1 parent 9cf9b8a commit 32bd91c
Show file tree
Hide file tree
Showing 99 changed files with 5,438 additions and 424 deletions.
18 changes: 17 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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`.
86 changes: 84 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -99,6 +99,88 @@ material._

<br>

### 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`, ...).
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
5 changes: 5 additions & 0 deletions demoapp/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions demoapp/README.MD
Original file line number Diff line number Diff line change
@@ -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



14 changes: 14 additions & 0 deletions demoapp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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()
}
}
113 changes: 113 additions & 0 deletions demoapp/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
}

22 changes: 22 additions & 0 deletions demoapp/composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name=".AndroidApp"
android:icon="@android:drawable/ic_menu_compass"
android:label="CryptoTest App"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
<activity
android:name=".AppActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Loading

0 comments on commit 32bd91c

Please sign in to comment.