Skip to content

Commit

Permalink
Supreme attestation
Browse files Browse the repository at this point in the history
  • Loading branch information
JesusMcCloud committed Oct 1, 2024
1 parent 6cd4a20 commit 755d1f0
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 13 deletions.
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
125 changes: 123 additions & 2 deletions warden/src/test/kotlin/WardenTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import at.asitplus.attestation.android.AndroidAttestationConfiguration
import at.asitplus.attestation.android.PatchLevel
import at.asitplus.attestation.android.exceptions.AttestationValueException
import at.asitplus.attestation.data.AttestationData
import at.asitplus.signum.indispensable.Attestation
import com.google.android.attestation.ParsedAttestationRecord
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.booleans.shouldBeFalse
Expand All @@ -15,6 +17,8 @@ import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.kotest.matchers.types.shouldNotBeInstanceOf
import kotlinx.datetime.toKotlinInstant
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.security.KeyPairGenerator
import java.security.spec.ECGenParameterSpec
import kotlin.time.Duration.Companion.days
Expand Down Expand Up @@ -1072,8 +1076,125 @@ class WardenTest : FreeSpec() {
//TODO jailbroken iphone
}

"And the Samsung" {
//TODO eternal leaves for samsung devices
"And the Fabulous" - {
val ios = "D4BD9EFC2A1AB1E2351143A4E67BB91F" to
Json.decodeFromStream<Attestation>(WardenTest::class.java.classLoader.getResourceAsStream("ios-appattest.json"))
val android = "CAC4307080875C418BEB668E825649DC" to
Json.decodeFromStream<Attestation>(this::class.java.classLoader.getResourceAsStream("aksattest.json"))

withData(ios, android) {
Warden(
AndroidAttestationConfiguration.Builder(
AndroidAttestationConfiguration.AppData(
"at.asitplus.cryptotest.androidApp",
listOf(
"941A4513A3027563D3A6EA48EEE85BA45EB9F69CEEA19EF0EBB17F100BFC8878".hexToByteArray(
HexFormat.UpperCase
)
)
)
).build(),
IOSAttestationConfiguration(
IOSAttestationConfiguration.AppData(
"9CYHJNG644",
"at.asitplus.signumtest.iosApp",
sandbox = true
)
), FixedTimeClock(2024u, 10u, 1u)
).apply {
withClue("should pass") {
verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply {
isSuccess.shouldBeTrue()
}
}

withClue("challenge fail pass") {
verifyKeyAttestation(it.second, it.first.reversed().hexToByteArray(HexFormat.UpperCase)).apply {
isSuccess.shouldBeFalse()
}
}
}

withClue("Invalid App ID") {
Warden(
AndroidAttestationConfiguration.Builder(
AndroidAttestationConfiguration.AppData(
"borked",
listOf(
"941A4513A3027563D3A6EA48EEE85BA45EB9F69CEEA19EF0EBB17F100BFC8878".hexToByteArray(
HexFormat.UpperCase
)
)
)
).build(),
IOSAttestationConfiguration(
IOSAttestationConfiguration.AppData(
"9CYHJNG644",
"borked",
sandbox = true
)
), FixedTimeClock(2024u, 10u, 1u)
).apply {

verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply {
isSuccess.shouldBeFalse()
}
}
}

withClue("Invalid Signature / Team ID") {
Warden(
AndroidAttestationConfiguration.Builder(
AndroidAttestationConfiguration.AppData(
"at.asitplus.cryptotest.androidApp",
listOf(
"491A4513A3027563D3A6EA48EEE85BA45EB9F69CEEA19EF0EBB17F100BFC8878".hexToByteArray(
HexFormat.UpperCase
)
)
)
).build(),
IOSAttestationConfiguration(
IOSAttestationConfiguration.AppData(
"borked1337",
"at.asitplus.signumtest.iosApp",
sandbox = true
)
), FixedTimeClock(2024u, 10u, 1u)
).apply {

verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply {
isSuccess.shouldBeFalse()
}
}
}

withClue("Timewarp") {
Warden(
AndroidAttestationConfiguration.Builder(
AndroidAttestationConfiguration.AppData(
"at.asitplus.cryptotest.androidApp",
listOf(
"941A4513A3027563D3A6EA48EEE85BA45EB9F69CEEA19EF0EBB17F100BFC8878".hexToByteArray(
HexFormat.UpperCase
)
)
)
).build(),
IOSAttestationConfiguration(
IOSAttestationConfiguration.AppData(
"9CYHJNG644",
"at.asitplus.signumtest.iosApp",
sandbox = true
)
), FixedTimeClock(1954u, 10u, 1u)
).apply {
verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply {
isSuccess.shouldBeFalse()
}
}
}
}
}
}
}
Expand Down
Loading

0 comments on commit 755d1f0

Please sign in to comment.