Warden Supreme Integration
Warden Supreme is a fully integrated key and app attestation suite consisting of:
- Mobile (iOS and Android) client library to generate attestation statements
 - Unified server-side key and app attestation verification library
 - Agnostic communication logic, taking care of process flows and wire format
 
Bugs Ahead!
Several devices and OS versions in the field come with bugs and quirks! Warden Supreme's docs hub discusses them here. Be sure to read up on them, before integrating attestation into your services!
Put differently, Warden Supreme is the evolution of the battle-tested WARDEN server-side key and app attestation library, augmented by Signum's Supreme KMP crypto provider for a consistent UX across Android and iOS. The original server-side-only key and app attestation library is still available and actively maintained, as it is one of the pillars supporting Warden Supreme. It now lives on as Warden makoto and continues to be published to Maven Central.
Using Warden Supreme in your Projects
Warden Supreme targets Android and iOS clients and JVM-based back-ends.
- On the back-end, add the 
verifierdependency: - On mobile clients, add the 
clientdependency: 
Warden Supreme currently only supports HTTP as its communication protocol and relies on Ktor on mobile clients. The back-end, however, can also use Spring, for example. An attestation flow works as follows, in accordance with Figure 1:
- The client fetches a challenge from the back-end.
 - The client feeds the challenge into hardware-backed key generation to create an attestation statement.
 - The client sends the attestation statement back to the back-end.
- Wire-format-wise this is a CSR, with a custom attribute carrying the actual attestation proof
 - CSRs were chosen as their canonical encoding is precisely specified and because they inherently come with a proof of possession of the private key
 - CSRs freely allow defining arbitrary extensions and attributes, which is a perfect fit for Warden Supreme's usage scenario
 - Finally, the PKIX context is the natural habitat of a CSR
 
 - The back-end verifies the attestation statement against a predefined policy.
- If the attestation is considered valid, the back-end issues a certificate for the attested key, thus vouching for the integrity of the client.
 - In case the attestation does not verify, the back-end records the reason for this failure.
 
 - The back-end responds either with the full certificate chain (success) or a detailed error reason (failure).
 
    As shown in Figure 1, the back-end needs to be configured before being able to assert the trustworthiness of a client. While the actual API is unified for Android and iOS (both for verifying attestation statements and on the mobile clients creating attestation statements), configuration needs to deal with each platform separately.
Back-End Configuration
Since Android and iOS attestation require different configuration parameters, distinct configuration classes exist. The following snippet lists all configuration values:
val warden = Warden(
    androidAttestationConfiguration = AndroidAttestationConfiguration(
        applications = listOf(   // REQUIRED: add applications to be attested
            AndroidAttestationConfiguration.AppData(
                packageName = "at.asitplus.attestation_client",
                signatureDigests = listOf("NLl2LE1skNSEMZQMV73nMUJYsmQg7=".encodeToByteArray()),
                appVersion = 5
            ),
            AndroidAttestationConfiguration.AppData( // dedicated app for the latest Android version
                packageName = "at.asitplus.attestation_client-tiramisu",
                signatureDigests = listOf("NLl2LE1skNSEMZQMV73nMUJYsmQg7=".encodeToByteArray()),
                appVersion = 2, // with a different versioning scheme
                androidVersionOverride = 130000, // so we need to override this
                patchLevelOverride = PatchLevel(2023, 6, maxFuturePatchLevelMonths = 2), // also override patch level and
                                                                     // consider patch levels from 2 months in the future
                                                                     // as valid 
                                                                     // maxFuturePatchLevelMonths defaults to 1
                                                                     // null means any future patch level is OK
                trustAnchorOverrides = setOf(extraTrustedRootPubKey),// require a custom root as the trust anchor
                                                                     // for the attestation certificate chain
                requireRemoteProvisioningOverride = true // require a remotely-provisioned attestation
                                                         // certificate for extra security
            )
        ),
        androidVersion = 110000,                  // OPTIONAL, null by default
        patchLevel = PatchLevel(2022, 12),        // OPTIONAL, null by default; maxFuturePatchLevelMonths defaults to 1
        requireStrongBox = false,                 // OPTIONAL, defaults to false
        allowBootloaderUnlock = false,            // OPTIONAL, defaults to false
        requireRollbackResistance = false,        // OPTIONAL, defaults to false
        ignoreLeafValidity = false,               // OPTIONAL, defaults to false
        hardwareAttestationTrustAnchors = linkedSetOf(*DEFAULT_HARDWARE_TRUST_ANCHORS), // OPTIONAL, defaults shown here
        softwareAttestationTrustAnchors = linkedSetOf(*DEFAULT_SOFTWARE_TRUST_ANCHORS), // OPTIONAL, defaults shown here
        verificationSecondsOffset = -300,         // OPTIONAL, defaults to 0
        disableHardwareAttestation = false,       // OPTIONAL, defaults to false; set true to disable HW attestation
        enableNougatAttestation = false,          // OPTIONAL, defaults to false; set true to enable hybrid attestation
        enableSoftwareAttestation = false,        // OPTIONAL, defaults to false; set true to enable SW attestation
        attestationStatementValiditySeconds = 300,// OPTIONAL, defaults to 300s
        httpProxy = null,                         //OPTIONAL HTTP proxy url, such as http://proxy.domain:12345, defaults to null for no proxy
        requireRemoteKeyProvisioning = false      //OPTIONAL, whether to require a remotely provisioned attestation certificate
    ),
    iosAttestationConfiguration = IosAttestationConfiguration(
        applications = listOf(
            IosAttestationConfiguration.AppData(
                teamIdentifier = "9CYHJNG644",
                bundleIdentifier = "at.asitplus.attestation-client",
                iosVersionOverride = "16.0",     // OPTIONAL, null by default
                sandbox = false                  // OPTIONAL, defaults to false
            )
        ),
        iosVersion = 14,                                              // OPTIONAL, null by default
        attestationStatementValiditySeconds = 300                     // OPTIONAL, defaults to 300s
    ),
    clock = FixedTimeClock(Instant.parse("2023-04-13T00:00:00Z")),   // OPTIONAL, system clock by default
    verificationTimeOffset = Duration.ZERO                           // OPTIONAL, defaults to zero
)
The (nullable) properties like patch level, iOS version, or Android app version essentially allow for excluding outdated devices. Defining custom logic to verify the attestation challenge for Android is unsupported by design, considering iOS constraints and inconsistencies between platforms resulting from such a customisation. More details on the configuration can be found in the API documentation.
A Note on Android Attestation
This library allows combining different flavours of Android attestation, ranging from full hardware attestation
to (rather useless in practice) software-only attestation, which can be useful for testing using an Android emulator.
Hardware attestation is enabled by default, while hybrid and software-only attestation need to be explicitly enabled
through enableNougatAttestation and enableSoftwareAttestation, respectively. Doing so will chain the corresponding
AndroidAttestationCheckers from the strictest (hardware) to the least strict (software-only).
Naturally, hardware attestation can also be disabled by setting disableHardwareAttestation = true, although there is probably
no real use case for such a configuration except for testing.
Example Usage
A verifier expects the following parameters to be configured:
- Either:
- a preconfigured 
Wardeninstance, or - pass all Warden configuration properties directly
 
 - a preconfigured 
 - an OID (globally unique, usually UUID-based) of the CSR attribute to carry the attestation statement (see also Data Model)
 - a lambda specifying how the validity of challenges is verified (and how used challenges are invalidated)
 
Naturally, clients and back-end need to agree on these parameters. Hence, it makes sense to set them inside a common module that is shared by back-end and clients. This leads to the following shared constants:
val ENDPOINT_CHALLENGE = "/api/v1/challenge"
val ENDPOINT_ATTEST = "/api/v1/attest"
val PROOF_OID = ObjectIdentifier(Uuid.parse("c893b702-28f6-4c50-8578-d1d7a1580729"))
Back-End Setup
In addition to the parameters described above, the back-end also needs a source to generate attestation challenges, track them, and match them against incoming attestation requests. As Warden Supreme's verifier component aims to integrate with any service, it simply expects a lambda that matches an incoming attestation statement against the expected nonce. Session management is out of scope, as it is provided by frameworks such as Ktor or Spring. In the end, a verifier instance is created as follows:
val attestationValidator = AttestationValidator(
    warden, /* the configured instance as per Back-End Configuration section */
    attestationProofOID = PROOF_OID
) {
    /*
    Your nonce validation logic here:
    Usually, you'll want to check
      * whether the challenge you got is one you issued before
      * and whether it is still fresh enough
    and then remove it from whatever challenge-cache you are using.
    Since you receive the challenge in the logic attached to the HTTP endpoint accepting attestation
    statements, you'll be matching it against the active session there anyway.
    */
}
As mentioned, it is also possible to pass the configuration parameters directly.
Handling Requests
 ENDPOINT_CHALLENGE is expected to return an AttestationChallenge
The encoding of the challenge is not fixed, but can be serialized using kotlinx.serialization
AttestationChallenge, contains the following properties:
1. nonce: ByteArray: The actual challenge value; usually a cryptographic nonce, based on true randomness
2. validity: Duration: This is used to communicate the validity duration of a challenge to the client
3. timeZone: TimeZone: Optional information about the TimeZone set on the backend
4. postEndpoint: String: This property conveys the endpoint to post the attestation statement to. This will typically be <service url>/$ENDPOINT_ATTEST.
5. timeOffset: Duration: This property is used to inform the client about the maximum tolerated time offset for temporal validations.
A fresh challenge is prepared as follows:
val challenge= attestationValidator.issueChallenge(
    TODO("Your nonce obtaining code here"),
    TODO("Your Challenge validity duration here"), //optional
    timeZone = TimeZone.currentSystemDefault(), //optional
    ENDPOINT_ATTEST,
    timeOffset = TODO("Communicate your server-side clock offset here"), //optional
)
 ENDPOINT_ATTEST expects a CSR created by the Supreme Client, after it obtained a challenge from ENDPOINT_CHALLENGE
Hence, the back-end is expected to decode the received body into a CSR, verify the contained attestation statement and (if everything checks out) issue a binding certificate and respond with the full certificate chain. When using Ktor, this typically works as follows:
post(ENDPOINT_ATTEST) {
    val src = call.receive<ByteArray>()
    val resp =
        attestationValidator.verifyKeyAttestation(Pkcs10CertificationRequest.decodeFromDer(src)) { csr, _ ->
        /* certificateSigner is assumed to be a `Signer` instance configured to use the CA key for signing */
        certificateSigner.sign(
            TbsCertificate(
                serialNumber = YOUR_SERIAL_HERE,
                publicKey = csr.publicKey, // use the CSR's subject public key
                signatureAlgorithm = certificateSigner.signatureAlgorithm.toX509SignatureAlgorithm().getOrThrow(),
                validFrom = Asn1Time(Clock.System.now()),
                validUntil = Asn1Time(Clock.System.now() + CERT_VALIDITY),
                issuerName = YOUR_ISSUER_NAME_HERE,
                subjectName = YOUR_SUBJECT_NAME_HERE,
            )
        ).map { listOf(it) }
    }
    call.respondText(Json.encodeToString(resp), contentType = ContentType.Application.Json)
}
Client Integration
On the client, Warden Supreme is even easier to integrate, assuming you are using Signum's Supreme KMP crypto provider:
// Create a Supreme attestation client
val client = AttestationClient(HttpClient())
// Fetch a challenge
val challenge = client.getChallenge(Url(ENDPOINT_CHALLENGE)).getOrThrow()
//TODO: handle errors, such as clock offset, gracefully
// Init the signer with a freshly created key and produce an attestation statement
val signer = PlatformSigningProvider.createSigningKey(alias) {
    ec {}
    hardware {
        attestation {
            this.challenge = challenge.nonce
        }
    }
}.getOrThrow()
// Create and sign a CSR
val csr = signer.createCsr(challenge).getOrThrow()
// Have the back-end attest it
val result = client.attest(csr, challenge.postEndpoint)
// Get the certificate chain containing the binding certificate as leaf
val certificateChain = when (result) {
    is AttestationResponse.Failure -> TODO("handle errors gracefully")
    is AttestationResponse.Success -> result.certificateChain
}
Tip
Warden Supreme does not check whether a device has biometrics enrolled. So if you choose to bind a to-be-attested key
to biometric auth, you need to check device capabilities beforehand!
→ AuthCheckKit
provides a unified multiplatform API for that.
Reacting to Attestation Results
The Supreme attestation verifier only returns an enum indicating the reason for an error, with the option to attach a custom explanatory string. This is by design, as it is generally undesirable to expose the internals of a back-end to clients.
On the back-end, however, attestation issues typically need to be analysed. Hence, the Supreme attestation validator provides three callbacks to analyse attestation errors and success (without side effects).
onPreAttestationErroris called in case of operational/internal errors, or if the attestation statement cannot be extracted from a CSR, for example.onAttestationErroris called if the attestation statement fails to verify. This includes an invalid bootloader lock state, wrong package identifier, etc.onAttestationSuccessis called right before anAttestationResponse.Successis returned with the fully parsed and verified attestation statement and the associated public key. This can be useful for statistical analyses, for example.