From d8a3a58c0e8d64ee43a145bd8ac21f3fe53a5c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Pr=C3=BCnster?= Date: Thu, 31 Oct 2024 18:21:42 +0100 Subject: [PATCH] strict temporal validation --- CHANGELOG.md | 13 +++ warden-roboto | 2 +- warden/src/main/kotlin/AttestationService.kt | 13 ++- warden/src/main/kotlin/Throwables.kt | 5 + warden/src/main/kotlin/Warden.kt | 76 ++++++++++++--- .../src/test/kotlin/FeatureDemonstration.kt | 15 ++- warden/src/test/kotlin/TemporalOffsetTest.kt | 31 ++++-- warden/src/test/kotlin/TestCommons.kt | 15 ++- warden/src/test/kotlin/WardenTests.kt | 95 +++++++++++++++++-- 9 files changed, 224 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9246224..b573349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 2.3.0 Behavioural Changes! +- Update to WARDEN-roboto 1.7.0 + - Android attestation statements (for SW, HW, but not Hybrid Nougat Attestation) do now verify attestation creation time! + - Refer to the [WARDEN-roboto changelog](https://github.com/a-sit-plus/warden-roboto/blob/main/CHANGELOG.md#170)! +- Change Android verification offset calculation: + It is now the sum of the toplevel offset and the Android-specific offset +- Change the reason for iOS attestation statement temporal invalidity: + - It is now `AttestationException.Content.iOS(cause = IosAttestationException(…, reason = IosAttestationException.Reason.STATEMENT_TIME))` + - **This reason was newly introduced in this release, making it binary and source incompatible!** + - iOS attestations are now also rejected if their validity starts in the future + - The validity time can now be configured in the same way as for Android, using the `attestationStatementValiditySeconds` property + - Any configured `verificationTimeOffset` is **NOT** automatically compensated for any more. **This means if you have previously used a five minutes offset, you now have to manually increase the `attestationStatementValiditySeconds` to `10 * 60`!** + ## 2.2.0 - Introduce new attestation format diff --git a/warden-roboto b/warden-roboto index 0af7bdc..7223ec4 160000 --- a/warden-roboto +++ b/warden-roboto @@ -1 +1 @@ -Subproject commit 0af7bdcfd993404e4a6a87e07db7db48ac21c729 +Subproject commit 7223ec41bbd32c70431346a8c3d623e1be064cbb diff --git a/warden/src/main/kotlin/AttestationService.kt b/warden/src/main/kotlin/AttestationService.kt index 9d71de0..15ebc82 100644 --- a/warden/src/main/kotlin/AttestationService.kt +++ b/warden/src/main/kotlin/AttestationService.kt @@ -36,11 +36,20 @@ data class IOSAttestationConfiguration @JvmOverloads constructor( */ val iosVersion: OsVersions? = null, - ) { + /** + * The maximum age an attestation statement is considered valid. + */ + val attestationStatementValiditySeconds: Int = 5 * 60 + +) { @JvmOverloads - constructor(singleApp: AppData, iosVersion: OsVersions? = null) : this(listOf(singleApp), iosVersion) + constructor( + singleApp: AppData, + iosVersion: OsVersions? = null, + attestationStatementValiditySeconds: Int = 5 * 60 + ) : this(listOf(singleApp), iosVersion, attestationStatementValiditySeconds) init { if (applications.isEmpty()) diff --git a/warden/src/main/kotlin/Throwables.kt b/warden/src/main/kotlin/Throwables.kt index 234bfd9..d6b4f43 100644 --- a/warden/src/main/kotlin/Throwables.kt +++ b/warden/src/main/kotlin/Throwables.kt @@ -153,6 +153,11 @@ class IosAttestationException(msg: String? = null, cause: Throwable? = null, val */ OS_VERSION, + /** + * Attestation statement creation time in the future + */ + STATEMENT_TIME, + /** * Signature counter in the assertion is too high. This could mean either an implementation error on the client, or a compromised client app. */ diff --git a/warden/src/main/kotlin/Warden.kt b/warden/src/main/kotlin/Warden.kt index 3e3de3f..3a0ec37 100644 --- a/warden/src/main/kotlin/Warden.kt +++ b/warden/src/main/kotlin/Warden.kt @@ -3,7 +3,10 @@ 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 at.asitplus.signum.indispensable.AndroidKeystoreAttestation +import at.asitplus.signum.indispensable.Attestation +import at.asitplus.signum.indispensable.IosHomebrewAttestation +import at.asitplus.signum.indispensable.getJcaPublicKey import ch.veehait.devicecheck.appattest.AppleAppAttest import ch.veehait.devicecheck.appattest.assertion.Assertion import ch.veehait.devicecheck.appattest.assertion.AssertionChallengeValidator @@ -12,7 +15,6 @@ import ch.veehait.devicecheck.appattest.attestation.ValidatedAttestation import ch.veehait.devicecheck.appattest.common.App import ch.veehait.devicecheck.appattest.common.AppleAppAttestEnvironment import ch.veehait.devicecheck.appattest.receipt.ReceiptException -import ch.veehait.devicecheck.appattest.receipt.ReceiptValidator import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.cbor.CBORFactory import com.fasterxml.jackson.module.kotlin.registerKotlinModule @@ -31,6 +33,7 @@ import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.security.interfaces.ECPublicKey import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration import kotlin.time.toKotlinDuration @@ -79,17 +82,45 @@ class Warden( private val log = LoggerFactory.getLogger(this.javaClass) private val androidAttestationCheckers = mutableListOf().apply { - if (!androidAttestationConfiguration.disableHardwareAttestation) add( + + if (verificationTimeOffset.inWholeSeconds > Int.MAX_VALUE) throw AttestationException.Configuration( + Platform.ANDROID, + "Offset too large!", + cause = NumberFormatException() + ) + if (verificationTimeOffset.inWholeSeconds < Int.MIN_VALUE) throw AttestationException.Configuration( + Platform.ANDROID, + "Offset too large!", + cause = NumberFormatException() + ) + + val androidOffset = + (verificationTimeOffset + androidAttestationConfiguration.verificationSecondsOffset.seconds).inWholeSeconds + if (androidOffset > Int.MAX_VALUE) throw AttestationException.Configuration( + Platform.ANDROID, + "Calculated Android offset too large!", + cause = NumberFormatException() + ) + if (androidOffset < Int.MIN_VALUE) throw AttestationException.Configuration( + Platform.ANDROID, + "Calculated Android offset too large!", + cause = NumberFormatException() + ) + + val correctlyOffsetAndroidConfig = + androidAttestationConfiguration.copy(verificationSecondsOffset = androidOffset.toInt()) + + if (!correctlyOffsetAndroidConfig.disableHardwareAttestation) add( HardwareAttestationChecker( - androidAttestationConfiguration + correctlyOffsetAndroidConfig ) { expected, actual -> expected contentEquals actual }) - if (androidAttestationConfiguration.enableNougatAttestation) add( + if (correctlyOffsetAndroidConfig.enableNougatAttestation) add( NougatHybridAttestationChecker( - androidAttestationConfiguration + correctlyOffsetAndroidConfig ) { expected, actual -> expected contentEquals actual }) - if (androidAttestationConfiguration.enableSoftwareAttestation) add( + if (correctlyOffsetAndroidConfig.enableSoftwareAttestation) add( SoftwareAttestationChecker( - androidAttestationConfiguration + correctlyOffsetAndroidConfig ) { expected, actual -> expected contentEquals actual }) } @@ -117,8 +148,7 @@ class Warden( clock = appAttestClock, receiptValidator = app.createReceiptValidator( clock = appAttestClock, - maxAge = (verificationTimeOffset.absoluteValue * 2).toJavaDuration() - .plus(ReceiptValidator.APPLE_RECOMMENDED_MAX_AGE) + maxAge = iosAttestationConfiguration.attestationStatementValiditySeconds.seconds.toJavaDuration() ) ) } @@ -215,8 +245,12 @@ class Warden( 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 AttestationResult.Android -> KeyAttestation( + null, + AttestationResult.Error("This must never happen!") + ) } } } @@ -229,6 +263,7 @@ class Warden( 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!")) } @@ -267,7 +302,7 @@ class Warden( runCatching { it.verifyAttestation( certificates, - (clock.now() + verificationTimeOffset).toJavaDate(), + (clock.now()).toJavaDate(), expectedChallenge ) } @@ -366,6 +401,14 @@ class Warden( results.first { (_, result) -> result.isSuccess }.let { (app, res) -> app to res.getOrNull()!! } + val notBefore = + result.second.receipt.payload.notBefore?.value ?: result.second.receipt.payload.creationTime.value + if (notBefore > appAttestClock.instant()) + throw AttestationException.Content.iOS( + message = "Attestation statement created after ${appAttestClock.instant()}: $notBefore", + cause = IosAttestationException(reason = IosAttestationException.Reason.STATEMENT_TIME) + ) + val iosVersion = iosApps.entries.firstOrNull { (_, appAttest) -> appAttest.app == result.first.app }?.key?.iosVersionOverride ?: iosAttestationConfiguration.iosVersion @@ -527,8 +570,11 @@ class Warden( ex = ex.cause } if (ex.message?.startsWith("Receipt's creation time is after") == true) - AttestationException.Certificate.Time.iOS( - cause = ex + AttestationException.Content.iOS( + cause = IosAttestationException( + cause = ex, + reason = IosAttestationException.Reason.STATEMENT_TIME + ), ) else AttestationException.Content.iOS( cause = IosAttestationException( @@ -538,6 +584,8 @@ class Warden( ) } } + } else if (it is AttestationException) { + it } else AttestationException.Content.iOS( cause = IosAttestationException( cause = it, diff --git a/warden/src/test/kotlin/FeatureDemonstration.kt b/warden/src/test/kotlin/FeatureDemonstration.kt index 0724556..9e73d44 100644 --- a/warden/src/test/kotlin/FeatureDemonstration.kt +++ b/warden/src/test/kotlin/FeatureDemonstration.kt @@ -30,7 +30,9 @@ class FeatureDemonstration : FreeSpec() { requireStrongBox = false, //optional allowBootloaderUnlock = false, //you don't usually want to change this requireRollbackResistance = false, //depends on device, so leave off - ignoreLeafValidity = false //Hello, Samsung! + ignoreLeafValidity = false, //Hello, Samsung! + verificationSecondsOffset = 15 * 60 - 1 * 60 * 60 + 24 * 60 * 60, //iOS and Android statements were created at different times + attestationStatementValiditySeconds = 10*60 //But we were not that exact in the line above ), iosAttestationConfiguration = IOSAttestationConfiguration( applications = listOf( @@ -45,7 +47,7 @@ class FeatureDemonstration : FreeSpec() { buildNumber = "0A0" ) //optional, use SemVer notation and large hex number to ignore build number ), - clock = FixedTimeClock(Instant.parse("2023-04-13T00:00:00Z")), //optional + clock = FixedTimeClock(Instant.parse("2023-04-13T14:03:00Z")), //optional verificationTimeOffset = Duration.ZERO //optional ) @@ -57,7 +59,8 @@ class FeatureDemonstration : FreeSpec() { .apply { shouldBeInstanceOf().apply { attestationCertificate.encoded shouldBe nokiaX10KeyMasterGood.attestationProof.first() - attestationRecord.attestationChallenge().toByteArray() shouldBe nokiaX10KeyMasterGood.challenge + attestationRecord.attestationChallenge() + .toByteArray() shouldBe nokiaX10KeyMasterGood.challenge attestationRecord.attestationSecurityLevel() shouldBeIn listOf( ParsedAttestationRecord.SecurityLevel.TRUSTED_ENVIRONMENT, ParsedAttestationRecord.SecurityLevel.STRONG_BOX @@ -80,7 +83,8 @@ class FeatureDemonstration : FreeSpec() { attestedPublicKey!!.encoded shouldBe nokiaX10KeyMasterGood.publicKey!!.encoded details.shouldBeInstanceOf().apply { attestationCertificate.encoded shouldBe nokiaX10KeyMasterGood.attestationProof.first() - attestationRecord.attestationChallenge().toByteArray() shouldBe nokiaX10KeyMasterGood.challenge + attestationRecord.attestationChallenge() + .toByteArray() shouldBe nokiaX10KeyMasterGood.challenge attestationRecord.attestationSecurityLevel() shouldBeIn listOf( ParsedAttestationRecord.SecurityLevel.TRUSTED_ENVIRONMENT, ParsedAttestationRecord.SecurityLevel.STRONG_BOX @@ -100,7 +104,8 @@ class FeatureDemonstration : FreeSpec() { attestedPublicKey!!.encoded shouldBe nokiaX10KeyMasterGood.publicKey!!.encoded details.shouldBeInstanceOf().apply { attestationCertificate.encoded shouldBe nokiaX10KeyMasterGood.attestationProof.first() - attestationRecord.attestationChallenge().toByteArray() shouldBe nokiaX10KeyMasterGood.challenge + attestationRecord.attestationChallenge() + .toByteArray() shouldBe nokiaX10KeyMasterGood.challenge attestationRecord.attestationSecurityLevel() shouldBeIn listOf( ParsedAttestationRecord.SecurityLevel.TRUSTED_ENVIRONMENT, ParsedAttestationRecord.SecurityLevel.STRONG_BOX diff --git a/warden/src/test/kotlin/TemporalOffsetTest.kt b/warden/src/test/kotlin/TemporalOffsetTest.kt index c734416..1169c3b 100644 --- a/warden/src/test/kotlin/TemporalOffsetTest.kt +++ b/warden/src/test/kotlin/TemporalOffsetTest.kt @@ -3,11 +3,12 @@ package at.asitplus.attestation import at.asitplus.attestation.data.AttestationData import io.kotest.core.spec.style.FreeSpec import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.matchers.types.shouldNotBeInstanceOf -import java.security.interfaces.ECPublicKey -import kotlin.time.Duration import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds class TemporalOffsetTest : FreeSpec() { @@ -33,6 +34,8 @@ class TemporalOffsetTest : FreeSpec() { attestationService( timeSource = FixedTimeClock(it.verificationDate.time), offset = 1.days, + androidAttestationStatementValidity = 1.days + 1.seconds, + iosAttestationStatementValidity = 1.days + 1.seconds, ).verifyAttestation( it.attestationProof, it.challenge, @@ -43,7 +46,7 @@ class TemporalOffsetTest : FreeSpec() { } "Exact Time of Validity - 1D" - { - withData(exactStartOfValidity) { + withData(mapOf("KeyMint 200" to pixel6KeyMint200Good)) { attestationService( timeSource = FixedTimeClock(it.verificationDate.time), offset = (-1).days, @@ -71,12 +74,28 @@ class TemporalOffsetTest : FreeSpec() { ).apply { shouldBeInstanceOf() .cause.shouldBeInstanceOf() - } } + } + "iOS Temporal Offset Strict Fail" - { + withData(nameFn = {it.toIsoString()},1.days, -1.days) { offset -> + attestationService( + timeSource = FixedTimeClock(ios16.verificationDate.time), + offset = offset, + iosAttestationStatementValidity =23.hours, + androidAttestationStatementValidity = 23.hours + ).verifyAttestation( + ios16.attestationProof, + ios16.challenge, + ).apply { + shouldBeInstanceOf() + this.cause.shouldBeInstanceOf() + (this.cause as AttestationException.Content).cause.shouldBeInstanceOf() + ((cause as AttestationException.Content).cause as IosAttestationException).reason shouldBe IosAttestationException.Reason.STATEMENT_TIME + } + } + } } } -} - diff --git a/warden/src/test/kotlin/TestCommons.kt b/warden/src/test/kotlin/TestCommons.kt index 2d91a33..429b4c1 100644 --- a/warden/src/test/kotlin/TestCommons.kt +++ b/warden/src/test/kotlin/TestCommons.kt @@ -7,6 +7,7 @@ import at.asitplus.attestation.data.mimeDecoder import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -68,7 +69,7 @@ val pixel6KeyMint200Good = AttestationData( DqG8At2JHA== """ ), - isoDate = "2023-04-14T14:30:21Z", + isoDate = "2023-04-14T14:30:22Z", //need to round up millis pubKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqs5NcBOKN40tu/5+NLFvGRMRcYF6KRksYoUmiwlKhhzbGaALzE2PerEM5wzNKeC6ESruZJRoBPuHn5D+HfoMkA==" ) val nokiaX10KeyMasterGood = AttestationData( @@ -117,7 +118,7 @@ val nokiaX10KeyMasterGood = AttestationData( wqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUBZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCHex0SdDrx+tWUDqG8At2JHA== """ ), - isoDate = "2023-04-15T00:00:00Z", + isoDate = "2023-04-14T13:14:42Z", pubKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3PdP8200NNz3h4p0bcwrPikiD5+s/qPXN/eHikTd8RnQiutcz4tqAq4NXgcmjLiEcNIOtkPTKi45ETDEoqPpA==" ) @@ -239,7 +240,7 @@ val ios16 = AttestationData( Ek2jN2KWUxblOXK/GhPheUDb27abxuG31ilG00AAAAAB """ ), - isoDate = "2023-04-12T14:02:40Z", + isoDate = "2023-04-13T14:02:40.755Z", // 755 milis past 40, so we round up pubKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEoHa3s8Au2QoovqslpA2X6kczRLSNnSzEvaIgFO03iRylBevYIh0jktyMtCnOhOeqcb4fyO/83QCRMMXGgvK9A==", packageOverride = "at.asitplus.attestation-client", isProductionOverride = true @@ -251,7 +252,7 @@ val ios17 = AttestationData( "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" ), - isoDate = "2024-03-05T07:37:20Z", + isoDate = "2024-03-05T07:39:28Z", pubKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIYCAo7rjNZhzrJ/ByrHqaNjpfwrOY+9igg3KhUr3TWMcJgdhyh8eZlNETyBtX7HWiz7AjVxky2JqzYV+wwpE9w==", isProductionOverride = false, packageOverride = "at.asitplus.attestation.Test" @@ -300,6 +301,8 @@ fun attestationService( eternalLeaves: Boolean = false, androidSW: Boolean = false, androidN: Boolean = false, + androidAttestationStatementValidity: Duration= 10.minutes, + iosAttestationStatementValidity: Duration= 10.minutes, ) = Warden( AndroidAttestationConfiguration( @@ -318,6 +321,7 @@ fun attestationService( ignoreLeafValidity = eternalLeaves, enableSoftwareAttestation = androidSW, enableNougatAttestation = androidN, + attestationStatementValiditySeconds = androidAttestationStatementValidity.inWholeSeconds.toInt() ), IOSAttestationConfiguration( applications = listOf( @@ -327,7 +331,8 @@ fun attestationService( sandbox = iosSandbox ) ), - iosVersion = iosVersion + iosVersion = iosVersion, + attestationStatementValiditySeconds = iosAttestationStatementValidity.inWholeSeconds.toInt() ), timeSource, offset diff --git a/warden/src/test/kotlin/WardenTests.kt b/warden/src/test/kotlin/WardenTests.kt index c4399c8..baf52f2 100644 --- a/warden/src/test/kotlin/WardenTests.kt +++ b/warden/src/test/kotlin/WardenTests.kt @@ -22,6 +22,8 @@ import kotlinx.serialization.json.decodeFromStream import java.security.KeyPairGenerator import java.security.spec.ECGenParameterSpec import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalStdlibApi::class) @@ -110,7 +112,7 @@ class WardenTest : FreeSpec() { "9R0HW4Qprd+PVoFS1oQFrFO9pHFhdXRoZW50aWNhdG9yRGF0YVgljiSVS1qsC3yiRa+Gw3NrIPZ0W9pBspx+KbwXluNyqeVAAAAA" + "AQ==" ), - "2023-09-11T16:02:40Z", + "2023-09-11T13:06:32Z", pubKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFT1XwEeF8NftY84GfnqTFBoxHNkdG7wZHcOkLKwT4W6333Jqmga1XkKySq/ApnslBPNZE1Os363SAv8X85ZIrQ==" ) @@ -127,13 +129,17 @@ class WardenTest : FreeSpec() { "at.asitplus.oegv-demo-app", sandbox = true ) - ), FixedTimeClock(2023u, 9u, 11u) + ), FixedTimeClock(iosIDA.verificationDate.toInstant().toKotlinInstant()) ).apply { verifyKeyAttestation( iosIDA.attestationProof, iosIDA.challenge, iosIDA.publicKey!! ).apply { isSuccess.shouldBeTrue() - verifyKeyAttestation( iosIDA.attestationProof, iosIDA.challenge, iosIDA.pubKeyB64!!.decodeBase64ToArray()) shouldBe this + verifyKeyAttestation( + iosIDA.attestationProof, + iosIDA.challenge, + iosIDA.pubKeyB64!!.decodeBase64ToArray() + ) shouldBe this } } } @@ -153,6 +159,7 @@ class WardenTest : FreeSpec() { timeSource = FixedTimeClock( recordedAttestation.verificationDate.toInstant().toKotlinInstant() ), + androidAttestationStatementValidity = 10.hours ).apply { "Generic" { verifyAttestation( @@ -733,7 +740,7 @@ class WardenTest : FreeSpec() { ySn502vQX3xvw== """ ), - isoDate = "2023-04-18T00:00:00Z", + isoDate = "2023-04-17T15:10:00Z", pubKeyB64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgQC2Fo5nb6dlnJh2h4tg0vnJmjPN8x2t+tlwbZEjWO6uJWlqu5uTPFkYKzgpxF6HVoOFYWwPFBZgB4ktwU3ysw==" ).apply { @@ -808,7 +815,8 @@ class WardenTest : FreeSpec() { ), disableHardwareAttestation = true, enableSoftwareAttestation = true, - ignoreLeafValidity = true + ignoreLeafValidity = true, + attestationStatementValiditySeconds = 10*60 ), DEFAULT_IOS_ATTESTATION_CFG, clock = clock @@ -833,7 +841,8 @@ class WardenTest : FreeSpec() { disableHardwareAttestation = true, enableNougatAttestation = true, enableSoftwareAttestation = true, - ignoreLeafValidity = true + ignoreLeafValidity = true, + attestationStatementValiditySeconds = 10*60 ), DEFAULT_IOS_ATTESTATION_CFG, clock = clock @@ -857,7 +866,8 @@ class WardenTest : FreeSpec() { ), enableNougatAttestation = true, enableSoftwareAttestation = true, - ignoreLeafValidity = true + ignoreLeafValidity = true, + attestationStatementValiditySeconds = 10*60 ), DEFAULT_IOS_ATTESTATION_CFG, clock = clock @@ -1101,7 +1111,10 @@ class WardenTest : FreeSpec() { "at.asitplus.signumtest.iosApp", sandbox = true ) - ), FixedTimeClock(2024u, 10u, 1u) + ), + FixedTimeClock(2024u, 10u, 1u), + verificationTimeOffset = 12.hours + 45.minutes + ).apply { withClue("should pass") { verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply { @@ -1139,6 +1152,72 @@ class WardenTest : FreeSpec() { } } + withClue("Attestation created in the future") { + Warden( + AndroidAttestationConfiguration.Builder( + AndroidAttestationConfiguration.AppData( + "at.asitplus.cryptotest.androidApp", + androidSigDigests + ) + ).build(), + IOSAttestationConfiguration( + IOSAttestationConfiguration.AppData( + "9CYHJNG644", + "at.asitplus.signumtest.iosApp", + sandbox = true + ) + ), + FixedTimeClock(2024u, 10u, 1u), + verificationTimeOffset = 0.hours + 45.minutes + + ).apply { + withClue("should pass") { + verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply { + isSuccess.shouldBeFalse() + } + } + + withClue("challenge fail pass") { + verifyKeyAttestation(it.second, it.first.reversed().hexToByteArray(HexFormat.UpperCase)).apply { + isSuccess.shouldBeFalse() + } + } + } + } + + withClue("Verification Clock too far in the future (i.e. statement too old)") { + Warden( + AndroidAttestationConfiguration.Builder( + AndroidAttestationConfiguration.AppData( + "at.asitplus.cryptotest.androidApp", + androidSigDigests + ) + ).build(), + IOSAttestationConfiguration( + IOSAttestationConfiguration.AppData( + "9CYHJNG644", + "at.asitplus.signumtest.iosApp", + sandbox = true + ) + ), + FixedTimeClock(2024u, 10u, 1u), + verificationTimeOffset = 14.hours + 45.minutes + + ).apply { + withClue("should pass") { + verifyKeyAttestation(it.second, it.first.hexToByteArray(HexFormat.UpperCase)).apply { + isSuccess.shouldBeFalse() + } + } + + withClue("challenge fail pass") { + verifyKeyAttestation(it.second, it.first.reversed().hexToByteArray(HexFormat.UpperCase)).apply { + isSuccess.shouldBeFalse() + } + } + } + } + withClue("Invalid Signature / Team ID") { Warden( AndroidAttestationConfiguration.Builder(