Skip to content

Commit

Permalink
Feature/supreme attestation (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
JesusMcCloud authored Oct 3, 2024
1 parent 6cd4a20 commit 269d5c1
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 24 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 2.2.0
- Introduce new attestation format

## 2.1.3
- Fix Parsing of iOS Build Numbers
- Dependency Updates:
Expand Down
49 changes: 38 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,37 @@ attestations (and related assertions; see below).

This begs the question: How to enable key attestation on iOS?
After all, many applications exist, which require some proof that a key used for critical operations resides in hardware.
<br>
Here, the ability to obtain a so-called *assertion* comes to the rescue: iOS allows generating an *assertion* for some

#### Legacy Attestation Format (Deprecated, but still Supported since Version 2.2.0)
To emulate key attestation, the ability to obtain a so-called *assertion* comes to the rescue: iOS allows generating an *assertion* for some
data by signing it using the same key backing a previously obtained attestation.
By that logic, computing an assertion over the public key of a freshly generated public/private key pair proves that an
authentic, uncompromised app on a non-jailbroken device was used to generate this key pair as intended by the app developer.

#### Supreme Attestation Format (Supported Since Version 2.2.0)
Following Apple's attestation format makes it clear that no data, but only hashes are ever encoded and signed.
Hence, it allows for a lot of flexibility when it comes to the data to be hashed.
The new _Supreme_ attestation format exploits this and does not only pass the hash over a challenge to the AppAttest
service, but instead constructs a structured (JSON) client data object, inspired by WebAuthn and passes tha hash of this data to DCAppAttest. This means that:

1. A `ClientData` object is created based on the challenge and the public key to attest.
2. The ClientData is serialized to JSON, and the `ByteArray`-representation of this JSON string are hashed using SHA-256:
* `val clientDataJSON = Json.encodeToString(clientData).encodeToByteArray()`
* `val clientDataHash = Digest.SHA256.digest(clientDataJSON).toNSData()`
3. This hash is then passed to DCAppAttest.
If your mobile clients are using the Supreme KMP crypto provider, this is procedure already implemented, and you don't have to worry about it.
4. The `IosHomebrewAttestation` provided by Signum's _Indispensable_ module lets you access both the raw bytes of this client data
as well as the original `ClientData` object, so you can easily verify both the hash of this data and its contents.

For this whole routine to work, clients need to create a Secure-Enclave-protected key pair before calling `DCAppattest` and construct the structured
client data, containing the public part of this key pair and the server challenge.
The client data format is defined in the _Signum's
[Indispensable](https://a-sit-plus.github.io/signum/dokka/indispensable/at.asitplus.signum.indispensable/-ios-homebrew-attestation/-client-data/index.html)_
module, as is the [IosHomebrewAttestation](https://a-sit-plus.github.io/signum/dokka/indispensable/at.asitplus.signum.indispensable/-ios-homebrew-attestation/index.html) containing it.

This library abstracts away all the nitty-gritty details of this verification process and provides a unified API
which works with both Android and iOS.
which works with both Android and iOS. (The [AndroidKeyStoreAttestation](https://a-sit-plus.github.io/signum/dokka/indispensable/at.asitplus.signum.indispensable/-android-keystore-attestation/index.html) contains simply the certificate chain attached to an attested key.)
The test resources contain examples of [Android](https://github.com/a-sit-plus/warden/tree/main/warden/src/test/resources/aksattest.json) and [iOS](https://github.com/a-sit-plus/warden/tree/main/warden/src/test/resources/ios-appattest.json) attestation proofs.

## Usage
Written in Kotlin, plays nicely with Java (cf. `@JvmOverloads`), published at maven central.
Expand Down Expand Up @@ -188,12 +211,18 @@ The sample also contains Android and iOS clients.
* The general workflow this library caters to assumes a back-end service, sending an attestation challenge to the mobile app. This challenge needs to be kept for future reference
* The app is assumed to generate a key pair with attestation (passing the received challenge to the platform's respective crypto APIs)
* The app responds with a platform-dependent attestation proof, the public key just created, and the challenge.

**DEPRECETED, but still supported**
* On Android, this proof is simply the certificate chain associated with the newly created key pair, which obtainable through the Android KeyStore API.
* The certificate chain needs to be encoded into a list of byte arrays.
* The first (index `0`) certificate is assumed to be the leaf, while tha last is assumed to be a certificate signed by the Google hardware attestation root key.
* On iOS, the list of byte arrays must contain exactly two entries:
* Index `0` contains an attestation object
* Index `1` contains an assertion over the to-be-attested public key (either ANSI X9.63 encoded or DER encoded)

**END DEPRECATION. The structure of the platform-specific proofs can be found [here](https://a-sit-plus.github.io/signum/dokka/indispensable/at.asitplus.signum.indispensable/-ios-homebrew-attestation/index.html).**


* On the back-end, a single call to `verifyKeyAttestation()` is sufficient to remotely verify
whether the key is indeed stored in HW (and whether the app can be trusted). This call requires the challenge from step 1.

Expand All @@ -202,11 +231,10 @@ Various advanced, platform-specific variants of this `verifyKeyAttestation()` ca
However, only `verifyKeyAttestation()` works for both Android and iOS and returns a [KeyAttestation](https://github.com/a-sit-plus/warden/blob/main/warden/src/main/kotlin/AttestationService.kt#L293) object:

```kotlin
fun <T : PublicKey> verifyKeyAttestation(
attestationProof: List<ByteArray>,
expectedChallenge: ByteArray,
keyToBeAttested: T
): KeyAttestation<T>
fun verifyKeyAttestation(
attestationProof: Attestation,
challenge: ByteArray)
: KeyAttestation<PublicKey>
```
The returned `KeyAttestation` object contains the attested key on success, or an error on failure.

Expand All @@ -220,8 +248,8 @@ On Android, this is simply the certificate chain from the attestation certificat
[Google hardware attestation root certificates](https://developer.android.com/training/articles/security-key-attestation#root_certificate).
on iOS this must contain the [AppAttest attestation statement](https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server#3576643)
at index `0` and an [assertion](https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server#3576644)
at index `1`, which, is verified for integrity and to match `keyToBeAttested`.
The signature counter in the attestation must be `0` and the signature counter in the assertion must be `1`.
at index `1`, which, is verified for integrity and to match `keyToBeAttested` **if the deprecated ios Attestation is used**.
The signature counter in the attestation must be `0` (and the signature counter in the assertion must be `1` **if the deprecated ios Attestation is used**).

Passing a public key created in the same app on an iDevice's secure hardware as `clientData` to create an assertion effectively
emulates Android's key attestation: Attesting such a secondary key through an assertion proves that
Expand All @@ -237,7 +265,6 @@ The key can be passed in either encoding to the secure enclave when creating an
External contributions are greatly appreciated!
Just be sure to observe the contribution guidelines (see [CONTRIBUTING.md](CONTRIBUTING.md)).


---
<p align="center">
This project has received funding from the European Union’s Horizon 2020 research and innovation
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
plugins { id("at.asitplus.gradle.conventions") version "2.0.20+20240904" }
plugins { id("at.asitplus.gradle.conventions") version "2.0.20+20240920" }

group = "at.asitplus"
3 changes: 0 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[versions]
serialization = "1.7.1"
kmmresult = "1.6.1"
2 changes: 1 addition & 1 deletion warden/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sourceSets.test {
dependencies {
api("at.asitplus:warden-roboto:$androidAttestationVersion")
api(datetime())
implementation("at.asitplus.signum:indispensable:3.6.0")
implementation("at.asitplus.signum:indispensable:3.9.0")
implementation("ch.veehait.devicecheck:devicecheck-appattest:0.9.6")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.14.2")
implementation("net.swiftzer.semver:semver:1.2.0")
Expand Down
25 changes: 23 additions & 2 deletions warden/src/main/kotlin/AttestationService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import at.asitplus.attestation.AttestationException
import at.asitplus.attestation.IOSAttestationConfiguration.AppData
import at.asitplus.attestation.android.*
import at.asitplus.attestation.android.exceptions.AttestationValueException
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.fromJcaPublicKey
import at.asitplus.signum.indispensable.*
import ch.veehait.devicecheck.appattest.assertion.Assertion
import ch.veehait.devicecheck.appattest.attestation.ValidatedAttestation
import com.google.android.attestation.AttestationApplicationId
Expand Down Expand Up @@ -247,6 +246,8 @@ abstract class AttestationService {
): AttestationResult


abstract fun verifyKeyAttestation(attestationProof: Attestation, challenge: ByteArray): KeyAttestation<PublicKey>

/**
* Verifies key attestation for both Android and Apple devices.
*
Expand All @@ -273,6 +274,11 @@ abstract class AttestationService {
*
* @return [KeyAttestation] containing the attested public key on success or null in case of failure (see [KeyAttestation])
*/
@Deprecated(
"This uses the legacy attestation format, which is not future-proof, makes too few guarantees wrt. encoding, " +
"guesses the platform based on the number of elements in the attestation proof, etc.",
ReplaceWith("AttestationService.verifyAttestation(attestationProof, challenge)")
)
fun <T : PublicKey> verifyKeyAttestation(
attestationProof: List<ByteArray>,
expectedChallenge: ByteArray,
Expand Down Expand Up @@ -610,6 +616,21 @@ object NoopAttestationService : AttestationService() {
if (attestationProof.size > 2) AttestationResult.Android.NOOP(attestationProof)
else AttestationResult.IOS.NOOP(clientData)

override fun verifyKeyAttestation(attestationProof: Attestation, challenge: ByteArray): KeyAttestation<PublicKey> =
when (attestationProof) {
is IosHomebrewAttestation -> KeyAttestation(
attestationProof.parsedClientData.publicKey.getJcaPublicKey().getOrThrow(),
AttestationResult.IOS.NOOP(attestationProof.parsedClientData.publicKey.encodeToDer())
)

is AndroidKeystoreAttestation -> KeyAttestation(
attestationProof.certificateChain.first().publicKey.getJcaPublicKey().getOrThrow(),
AttestationResult.Android.NOOP(attestationProof.certificateChain.map { it.encodeToDer() })
)

else -> KeyAttestation(null, AttestationResult.Error("Unsupported attestation proof type"))
}

override val ios: IOS
get() = object : IOS {
override fun verifyAppAttestation(attestationObject: ByteArray, challenge: ByteArray) =
Expand Down
5 changes: 1 addition & 4 deletions warden/src/main/kotlin/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ data class AttestationObject(
internal fun PublicKey.transcodeToAllFormats() = CryptoPublicKey.fromJcaPublicKey(this).getOrThrow().let {
listOf(
it.iosEncoded,
(it as CryptoPublicKey.EC).copy(
it.publicPoint,
preferCompressedRepresentation = !it.preferCompressedRepresentation
).iosEncoded,
(it as CryptoPublicKey.EC).toAnsiX963Encoded(useCompressed = !it.preferCompressedRepresentation),
it.encodeToDer()
)
}
Expand Down
55 changes: 55 additions & 0 deletions warden/src/main/kotlin/Warden.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package at.asitplus.attestation
import at.asitplus.attestation.android.*
import at.asitplus.attestation.android.exceptions.AttestationValueException
import at.asitplus.attestation.android.exceptions.CertificateInvalidException
import at.asitplus.signum.indispensable.*
import ch.veehait.devicecheck.appattest.AppleAppAttest
import ch.veehait.devicecheck.appattest.assertion.Assertion
import ch.veehait.devicecheck.appattest.assertion.AssertionChallengeValidator
Expand Down Expand Up @@ -186,6 +187,60 @@ class Warden(
}
}

override fun verifyKeyAttestation(
attestationProof: Attestation,
challenge: ByteArray
): KeyAttestation<PublicKey> =
when (attestationProof) {
is SelfAttestation, is IosLegacyHomebrewAttestation -> KeyAttestation<PublicKey>(
null,
AttestationResult.Error("${attestationProof::class.simpleName} is unsupported")
)

is IosHomebrewAttestation -> {
if (IosHomebrewAttestation.ClientData(
attestationProof.parsedClientData.publicKey,
challenge
) != attestationProof.parsedClientData
)
KeyAttestation(
null, AttestationResult.Error(
"Challenge mismatch",
AttestationException.Content.iOS(cause = IosAttestationException(reason = IosAttestationException.Reason.CHALLENGE))
)
)
else
verifyAttestationApple(
attestationProof.attestation,
attestationProof.clientDataJSON,
assertionData = null,
counter = 0L
).let {
when (it) {
is AttestationResult.IOS -> KeyAttestation(
attestationProof.parsedClientData.publicKey.getJcaPublicKey().getOrThrow(), it
)
is AttestationResult.Error -> KeyAttestation(null, it)
is AttestationResult.Android -> KeyAttestation(null, AttestationResult.Error("This must never happen!"))
}
}
}

is AndroidKeystoreAttestation -> verifyAttestationAndroid(
attestationProof.certificateChain.map { it.encodeToDer() },
challenge
).let {
when (it) {
is AttestationResult.Android -> KeyAttestation(
attestationProof.certificateChain.first().publicKey.getJcaPublicKey().getOrThrow(), it
)
is AttestationResult.Error -> KeyAttestation(null, it)
is AttestationResult.IOS -> KeyAttestation(null, AttestationResult.Error("This must never happen!"))
}
}

}

/**
* Verifies [Android Key Attestation](https://developer.android.com/training/articles/security-key-attestation) based
* the provided certificate chain (the leaf ist the attestation certificate, the root must be one of the
Expand Down
Loading

0 comments on commit 269d5c1

Please sign in to comment.