All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arden-roboto.1.7.0.source-code.AndroidAttestationChecker.kt Maven / Gradle / Ivy

The newest version!
package at.asitplus.attestation.android

import at.asitplus.attestation.android.exceptions.AttestationValueException
import at.asitplus.attestation.android.exceptions.CertificateInvalidException
import at.asitplus.attestation.android.exceptions.RevocationException
import com.google.android.attestation.AuthorizationList
import com.google.android.attestation.ParsedAttestationRecord
import com.google.android.attestation.RootOfTrust
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.util.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonObject
import java.io.IOException
import java.io.InputStream
import java.math.BigInteger
import java.security.Principal
import java.security.PublicKey
import java.security.cert.CertificateExpiredException
import java.security.cert.CertificateNotYetValidException
import java.security.cert.X509Certificate
import java.time.Duration
import java.time.Instant
import java.time.YearMonth
import java.util.*
import kotlin.jvm.optionals.getOrNull

abstract class AndroidAttestationChecker(
    protected val attestationConfiguration: AndroidAttestationConfiguration,
    private val verifyChallenge: (expected: ByteArray, actual: ByteArray) -> Boolean
) {

    private val revocationListClient = HttpClient(CIO) { setup(attestationConfiguration.httpProxy) }

    @Throws(CertificateInvalidException::class, RevocationException::class)
    private fun List.verifyCertificateChain(verificationDate: Date) {

        runCatching { verifyRootCertificate(verificationDate) }
            .onFailure {
                throw if (it is CertificateInvalidException) it else CertificateInvalidException(
                    "could not verify root certificate (valid from: ${last().notBefore} to ${last().notAfter}), verification date: $verificationDate",
                    cause = it,
                    if ((it is CertificateExpiredException) || (it is CertificateNotYetValidException)) CertificateInvalidException.Reason.TIME else CertificateInvalidException.Reason.TRUST
                )
            }
        val revocationStatusList = runCatching { RevocationList.fromGoogleServer(client = revocationListClient) }
            .getOrElse {
                throw RevocationException(
                    "could not download revocation information",
                    it,
                    RevocationException.Reason.LIST_UNAVAILABLE
                )
            }
        let {
            if (attestationConfiguration.ignoreLeafValidity) mapIndexed { i, cert ->
                if (i == 0) EternalX509Certificate(cert) else cert
            } else it
        }.reversed().zipWithNext { parent, certificate ->
            verifyCertificatePair(certificate, parent, verificationDate, revocationStatusList)
        }
    }

    @Throws(RevocationException::class, CertificateInvalidException::class)
    private fun verifyCertificatePair(
        certificate: X509Certificate,
        parent: X509Certificate,
        verificationDate: Date,
        statusList: RevocationList
    ) {
        runCatching {
            certificate.checkValidity(verificationDate)
            certificate.verify(parent.publicKey)
        }.onFailure {
            throw CertificateInvalidException(
                it.message ?: "Certificate invalid",
                it,
                if ((it is CertificateExpiredException) || (it is CertificateNotYetValidException)) CertificateInvalidException.Reason.TIME else CertificateInvalidException.Reason.TRUST
            )
        }
        runCatching {
            statusList.isRevoked(certificate.serialNumber)
        }.onSuccess {
            if (it)
                throw RevocationException("Certificate revoked", reason = RevocationException.Reason.REVOKED)
        }.onFailure {
            throw RevocationException(
                "Could not init revocation list",
                it,
                RevocationException.Reason.LIST_UNAVAILABLE
            )
        }
    }

    private fun List.verifyRootCertificate(verificationDate: Date) {
        val root = last()
        root.checkValidity(verificationDate)
        val matchingTrustAnchor = trustAnchors
            .firstOrNull { root.publicKey.encoded.contentEquals(it.encoded) }
            ?: throw CertificateInvalidException(
                "No matching root certificate",
                reason = CertificateInvalidException.Reason.TRUST
            )
        root.verify(matchingTrustAnchor)
    }

    protected abstract val trustAnchors: Collection

    protected open fun ParsedAttestationRecord.verifyAttestationTime(verificationDate: Instant) {
        val createdAt =
            teeEnforced().creationDateTime().getOrNull() ?: softwareEnforced().creationDateTime().getOrNull()
        if (createdAt == null) throw AttestationValueException(
            "Attestation statement creation time missing",
            reason = AttestationValueException.Reason.TIME
        )
        var checkTime = verificationDate.plusSeconds(attestationConfiguration.verificationSecondsOffset.toLong())
        val difference = Duration.between(createdAt, checkTime)
        if (difference.isNegative) throw AttestationValueException(
            "Attestation statement creation time too far in the future: $createdAt, check time: $checkTime",
            reason = AttestationValueException.Reason.TIME
        )

        if (difference > Duration.ofSeconds(attestationConfiguration.attestationStatementValiditySeconds.toLong())) throw AttestationValueException(
            "Attestation statement creation time too far in the past: $createdAt, check time: $checkTime, attestation statement validity in seconds: ${attestationConfiguration.attestationStatementValiditySeconds}",
            reason = AttestationValueException.Reason.TIME
        )


    }

    @Throws(AttestationValueException::class)
    private fun ParsedAttestationRecord.verifyApplication(application: AndroidAttestationConfiguration.AppData) {
        runCatching {
            if (softwareEnforced().attestationApplicationId().get().packageInfos().first()
                    .packageName() != application.packageName
            ) {
                throw AttestationValueException(
                    "Invalid Application Package",
                    reason = AttestationValueException.Reason.PACKAGE_NAME
                )
            }
            application.appVersion?.let { configuredVersion ->
                if (softwareEnforced().attestationApplicationId().get().packageInfos().first()
                        .version() < configuredVersion
                ) {
                    throw AttestationValueException(
                        "Application Version not supported",
                        reason = AttestationValueException.Reason.APP_VERSION
                    )
                }
            }

            if (!softwareEnforced().attestationApplicationId().get().signatureDigests().any { fromAttestation ->
                    application.signatureDigests.any { it.contentEquals(fromAttestation.toByteArray()) }
                }) {
                throw AttestationValueException(
                    "Invalid Application Signature Digest",
                    reason = AttestationValueException.Reason.APP_SIGNER_DIGEST
                )
            }
        }.onFailure {
            throw when (it) {
                is AttestationValueException -> it
                else -> AttestationValueException(
                    "Could not verify Client Application",
                    it,
                    reason = AttestationValueException.Reason.APP_UNEXPECTED
                )
            }
        }
    }


    @Throws(AttestationValueException::class)
    protected abstract fun ParsedAttestationRecord.verifyAndroidVersion(
        versionOverride: Int? = null,
        osPatchLevel: PatchLevel?
    )

    protected fun AuthorizationList.verifyAndroidVersion(versionOverride: Int?, patchLevel: PatchLevel?) {
        runCatching {

            (versionOverride ?: attestationConfiguration.androidVersion)?.let {
                if ((osVersion().get()) < it) throw AttestationValueException(
                    "Android version not supported",
                    reason = AttestationValueException.Reason.OS_VERSION
                )
            }

            (patchLevel ?: attestationConfiguration.patchLevel)?.let {
                if ((osPatchLevel().get()).isBefore(YearMonth.of(it.year, it.month))) throw AttestationValueException(
                    "Patch level not supported",
                    reason = AttestationValueException.Reason.OS_VERSION
                )
            }
        }.onFailure {
            throw when (it) {
                is AttestationValueException -> it
                else -> AttestationValueException(
                    "Could not verify Android Version",
                    it,
                    AttestationValueException.Reason.OS_VERSION
                )
            }
        }
    }


    @Throws(AttestationValueException::class)
    protected abstract fun ParsedAttestationRecord.verifyBootStateAndSystemImage()

    @Throws(AttestationValueException::class)
    protected fun AuthorizationList.verifySystemLocked() {
        if (attestationConfiguration.allowBootloaderUnlock) return

        if (rootOfTrust() == null) throw AttestationValueException(
            "Root of Trust not present",
            reason = AttestationValueException.Reason.SYSTEM_INTEGRITY
        )

        if (!rootOfTrust().get().deviceLocked()) throw AttestationValueException(
            "Bootloader not locked",
            reason = AttestationValueException.Reason.SYSTEM_INTEGRITY
        )

        if ((rootOfTrust().get().verifiedBootState()
                ?: RootOfTrust.VerifiedBootState.FAILED) != RootOfTrust.VerifiedBootState.VERIFIED
        ) throw AttestationValueException(
            "System image not verified",
            reason = AttestationValueException.Reason.SYSTEM_INTEGRITY
        )
    }

    @Throws(AttestationValueException::class)
    protected abstract fun ParsedAttestationRecord.verifyRollbackResistance()

    @Throws(AttestationValueException::class)
    protected fun AuthorizationList.verifyRollbackResistance() {
        if (attestationConfiguration.requireRollbackResistance)
            if (!rollbackResistance()) throw AttestationValueException(
                "No rollback resistance",
                reason = AttestationValueException.Reason.ROLLBACK_RESISTANCE
            )
    }

    /**
     * Verifies Android Key attestation Implements in accordance with https://developer.android.com/training/articles/security-key-attestation.
     * Checks are performed according to the properties set in the [attestationConfiguration].
     *
     * @See [AndroidAttestationConfiguration] for details on what is and is not checked.
     *
     * @return [ParsedAttestationRecord] on success
     * @throws AttestationValueException if a property fails to verify according to the current configuration
     * @throws RevocationException if a certificate has been revoked
     * @throws CertificateInvalidException if certificates fail to verify
     *
     */
    @Throws(AttestationValueException::class, CertificateInvalidException::class, RevocationException::class)
    open fun verifyAttestation(
        certificates: List,
        verificationDate: Date = Date(),
        expectedChallenge: ByteArray
    ): ParsedAttestationRecord {
        val calendar = Calendar.getInstance()
        calendar.time = verificationDate
        calendar.add(Calendar.SECOND, attestationConfiguration.verificationSecondsOffset)

        certificates.verifyCertificateChain(calendar.time)

        val parsedAttestationRecord = ParsedAttestationRecord.createParsedAttestationRecord(certificates)
        if (!verifyChallenge(
                expectedChallenge,
                parsedAttestationRecord.attestationChallenge().toByteArray()
            )
        ) throw AttestationValueException(
            "verification of attestation challenge failed",
            reason = AttestationValueException.Reason.CHALLENGE
        )
        parsedAttestationRecord.verifyAttestationTime(verificationDate.toInstant())
        parsedAttestationRecord.verifySecurityLevel()
        parsedAttestationRecord.verifyBootStateAndSystemImage()
        parsedAttestationRecord.verifyRollbackResistance()

        val attestedApp = attestationConfiguration.applications.associateWith { app ->
            runCatching { parsedAttestationRecord.verifyApplication(app) }
        }.let {
            it.entries.firstOrNull { (_, result) -> result.isSuccess } ?: it.values.first().exceptionOrNull()!!
                .let { throw it }
        }.key
        parsedAttestationRecord.verifyAndroidVersion(attestedApp.androidVersionOverride, attestedApp.patchLevelOverride)
        return parsedAttestationRecord
    }

    @Throws(AttestationValueException::class)
    protected abstract fun ParsedAttestationRecord.verifySecurityLevel()

    /**
     * taken and adapted from [com.google.android.attestation.CertificateRevocationStatus] to separate downloading and checking
     */
    class RevocationList(json: JsonObject) {
        private val entries by lazy { json["entries"]?.jsonObject ?: throw IOException() }
        fun isRevoked(
            serialNumber: BigInteger
        ): Boolean {
            val serialNumberNormalised = serialNumber.toString(16).lowercase(Locale.getDefault())
            return entries[serialNumberNormalised] != null //any entry is a red flag!
        }

        companion object {
            @JvmStatic
            private val client by lazy { HttpClient(CIO) { setup(null) } }

            @OptIn(ExperimentalSerializationApi::class)
            @JvmStatic
            fun from(source: InputStream) = RevocationList(json.decodeFromStream(source))

            @Throws(Throwable::class)
            @JvmStatic
            @JvmOverloads
            fun fromGoogleServer(client: HttpClient = this.client) =
                runBlocking {
                    RevocationList(client.get("https://android.googleapis.com/attestation/status").body())
                }
        }
    }
}


class EternalX509Certificate(private val delegate: X509Certificate) : X509Certificate() {
    override fun toString() = delegate.toString()

    override fun getEncoded(): ByteArray = delegate.encoded

    override fun verify(key: PublicKey?) = delegate.verify(key)

    override fun verify(key: PublicKey?, sigProvider: String?) = delegate.verify(key, sigProvider)

    override fun getPublicKey(): PublicKey = delegate.publicKey

    override fun hasUnsupportedCriticalExtension(): Boolean = delegate.hasUnsupportedCriticalExtension()

    override fun getCriticalExtensionOIDs(): MutableSet = delegate.criticalExtensionOIDs

    override fun getNonCriticalExtensionOIDs(): MutableSet = delegate.nonCriticalExtensionOIDs

    override fun getExtensionValue(oid: String?): ByteArray = delegate.getExtensionValue(oid)

    override fun checkValidity() {
        /*NOOP*/
    }

    override fun checkValidity(date: Date?) {
        /*NOOP*/
    }

    override fun getVersion(): Int = delegate.version

    override fun getSerialNumber(): BigInteger = delegate.serialNumber

    override fun getIssuerDN(): Principal = delegate.issuerDN

    override fun getSubjectDN(): Principal = delegate.subjectDN

    override fun getNotBefore(): Date = delegate.notBefore

    override fun getNotAfter(): Date = delegate.notAfter

    override fun getTBSCertificate(): ByteArray = delegate.tbsCertificate

    override fun getSignature(): ByteArray = delegate.signature

    override fun getSigAlgName(): String = delegate.sigAlgName

    override fun getSigAlgOID(): String = delegate.sigAlgOID

    override fun getSigAlgParams(): ByteArray = delegate.sigAlgParams

    override fun getIssuerUniqueID(): BooleanArray = delegate.issuerUniqueID

    override fun getSubjectUniqueID(): BooleanArray = delegate.subjectUniqueID

    override fun getKeyUsage(): BooleanArray = delegate.keyUsage

    override fun getBasicConstraints(): Int = delegate.basicConstraints

}

internal val json = Json { ignoreUnknownKeys = true }

fun HttpClientConfig<*>.setup(proxyUrl: String?) =
    apply {
        install(HttpCache)
        install(ContentNegotiation) { json(json) }
        engine { proxyUrl?.let { proxy = ProxyBuilder.http(it) } }
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy