Skip to content

Warden Supreme Integration

A-SIT Plus Official GitHub license Kotlin Java Maven Central

Warden Supreme is a fully integrated key and app attestation suite consisting of:

  1. A mobile client library (iOS and Android) to generate attestation statements
  2. A unified server-side key and app attestation verification library
  3. Agnostic communication logic for process flows and the 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. Warden Supreme currently supports HTTP as its communication protocol and relies on Ktor on mobile clients. The back-end, however, can also use Spring or any other HTTP framework of your choice.

  • On the back-end, add the verifier dependency:
    implementation("at.asitplus.warden:supreme-verifier:$version")
    
  • On mobile clients, add the client dependency:
    implementation("at.asitplus.warden:supreme-client:$version")
    

High-Level Attestation Flow

An attestation flow works as follows, in accordance with Figure 1:

  1. The client fetches a challenge from the back-end.
  2. The client feeds the challenge into hardware-backed key generation to create an attestation statement.
  3. The client sends the attestation proof (CSR) back to the back-end.
    • Wire-format-wise, this is a CSR with a custom attribute carrying the attestation statement payload
    • CSRs were chosen because their canonical encoding is precisely specified and because they inherently come with a proof of possession of the private key
    • CSRs 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
  4. 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.
    • If the attestation does not verify, the back-end records the reason for this failure.
  5. The back-end responds either with the full certificate chain (success) or a detailed error reason (failure).
Warden Supreme attestation flow
Figure 1: Remotely establishing trust in mobile clients

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.

Warden Supreme Step-by-Step Guide

Note

The Warden Supreme verifier does not ship with a crypto provider! Still, this example assumes Signum Supreme for brevity.

Warden Supreme integrates server-side and client-side logic into a lean interface.

This section illustrates a complete end-to-end setup assuming a Ktor server on the back-end and a KMP client app. To get going, the following steps are required:

  • Decide on HTTPS endpoints to issue challenges and verify attestation statements, and record the app identifiers and signer digests (Android) / team ID (iOS).
  • Back-end:
    1. Configure a Makoto instance based on your policy and app identifiers.
    2. Create an AttestationVerifier based on the configured Makoto instance, your CA certificate, and signing keys.
    3. Wire HTTPS endpoints to the AttestationVerifier and start an HTTP server.
  • Mobile app:
    1. Wire the verifier to the HTTPS endpoints in an AttestationClient.
    2. Call the endpoints.
    3. Store the received certificate chain after a successful attestation.

Migration Info

Warden Supreme 0.9.99 revamped trust anchor management and thus changed configuration parameters.

Attestation Policy Configuration

Since Android and iOS attestation require different configuration parameters, distinct configuration classes exist. The following snippet shows an MWE that also accounts for five minutes of clock drift:

val makoto = Makoto(
    androidAttestationConfiguration = AndroidAttestationConfiguration(
     /*(1)!*/AndroidAttestationConfiguration.AppData(
            packageName = "at.asitplus.attestation_client",
            signerFingerprints = listOf("34 b9 76 2c 4d 6c 90 d4 84 31 94 0c 57 bd e7 31 42 58 b2 64 20 ec".parseHex())
        )
    ),
    iosAttestationConfiguration = IosAttestationConfiguration(
     /*(2)!*/IosAttestationConfiguration.AppData(
            teamIdentifier = "9CYHJNG644",
            bundleIdentifier = "at.asitplus.attestation-client",
        )
    )
)
  1. At least package identifier and a single signer digest need to be configured for an Android application to be attested.
    • The combination of package identifier and signature digest fully identify an Android application and make it possible to attest its authenticity.
    • For production apps distributed through the Google Play Store, this is the digest of a Google cloud signing certificate.
  2. An iOS application is uniquely identified by a bundle identifier and a team ID.
    This combination makes it possible to attest its authenticity.

With great power comes great responsibility!

The above really is a minimum working example! Many more configuration properties exist, and it is recommended to explicitly set all those that are relevant to your specific scenario, as the value of every single one should very much be the result of careful consideration. In the end, a strongly informed decision about every property is required to reflect the intended audience and the required security properties.

Warden Supreme, by definition, cannot take these decisions from you!

The full details on the configuration can be found in the API documentation and a comprehensive example can be expanded below. Be sure to read up on Clock drift issues before tweaking properties!

Comprehensive example of Makoto config options

The below config illustrates configuring two different Android apps: a regular one for the masses and a second one with much tighter security constraints. This makes no sense when Warden Supreme is integrated into a back-end. If, however, a dedicated attestation service is deployed that is then used to issue certificates for apps used by different services, this can be legitimate. A single iOS-app is configured for test purposes only. In this example, the iOS app has not yet launched and is purely simulated. To still be able to test the attestation code path for iOS, custom trusted roots are set and all iOS attestation statements sent to the back-end are created in software, purely for evaluation purposes.

Be sure to check the annotations!

val makoto = Makoto(
    androidAttestationConfiguration = AndroidAttestationConfiguration(
        applications = listOf(
         /*(1)!*/AndroidAttestationConfiguration.AppData(
                packageName = "at.asitplus.attestation_client",
                signerFingerprints = listOf("34 b9 76 2c 4d 6c 90 d4 84 31 94 0c 57 bd e7 31 42 58 b2 64 20 ec".parseHex()),
            ),
         /*(2)!*/AndroidAttestationConfiguration.AppData(
             /*(3)!*/packageName = "at.asitplus.attestation_client-hardened",
                signerFingerprints = listOf("34 b9 76 2c 4d 6c 90 d4 84 31 94 0c 57 bd e7 31 42 58 b2 64 20 ec".parseHex()),
                appVersion = 2,
             /*(4)!*/androidVersionOverride = 160000,
                patchLevelOverride = PatchLevel(year = 2025, month = 9,
                 /*(5)!*/maxFuturePatchLevelMonths = 2
                ),
             /*(6)!*/requireRemoteKeyProvisioningOverride = true,
             /*(7)!*/trustedRootOverrides = setOf(GOOGLE_RKP_EC_ROOT),
             /*(8)!*/requireStrongBoxOverride = true,
            )
        ),
     /*(9)!*/androidVersion = 130000, patchLevel = PatchLevel(2023, 12), requireStrongBox = false,
        allowBootloaderUnlock = false, //DEFAULT
     /*(10)!*/requireRollbackResistance = false, //DEFAULT
     /*(11)!*/ignoreLeafValidity = false, // defaults to true
        hardwareTrustedRoots = GOOGLE_DEFAULT_HARDWARE_TRUST_ANCHORS, //DEFAULT
        softwareTrustedRoots = GOOGLE_SOFTWARE_TRUST_ANCHORS_UNTIL_A12, //DEFAULT
        verificationSecondsOffset = 0, //DEFAULT
     /*(12)!*/disableHardwareAttestation = false,
        enableSoftwareAttestation = false, //DEFAULT
     /*(13)!*/attestationStatementValiditySeconds = null, // DEFAULT; no validity time checks!
     /*(14)!*/revocation = listOf(
            AndroidRevocationList.GoogleDefaultLoaderConfig.withHttpProxy("https://192.168.178.74:8000")
        ),
        requireRemoteKeyProvisioning = false //DEFAULT

    ),
    iosAttestationConfiguration = IosAttestationConfiguration(
        applications = listOf(
         /*(15)!*/IosAttestationConfiguration.AppData(
                teamIdentifier = "9CYHJNG644",
                bundleIdentifier = "at.asitplus.attestation-client",
             /*(16)!*/iosVersionOverride = OsVersions("16.0", "20A10"),
             /*(17)!*/sandbox = true, //defaults to false
             /*(18)!*/trustedRootOverrides = myCustomRoots
            )
        ),
                /* Same as 17.0 β†˜β†˜ */
     /*(19)!*/iosVersion = OsVersions("17", "21A36"), //defaults to null (= no version check)
     /*(20)!*/attestationStatementValiditySeconds = 600, //DEFAULT
     /*(21)!*/trustedRoots = APPLE_DEFAULT_TRUSTED_ROOTS //DEFAULT
    ),
    clock = Clock.System, //DEFAULT
 /*(22)!*/verificationTimeOffset = 5.minutes, //OPTIONAL, defaults shown
)
  1. The basic application for the masses
  2. A second, experimental high-security app
  3. Different package name from the first app
  4. Enforce minimum version, Android 16, and up-to-date security patches
  5. Allow for more leeway
  6. Only remote key provisioning is considered trustworthy for this app
  7. Only the RKP trust anchor is considered trustworthy
  8. We want our app to have a dedicated HSM
  9. By default, Android 13 with a somewhat recent patch level will be required without enforcing the most recent security patches or StrongBox to reach a wider audience. This concerns the first app, since the second one overrides these values.
  10. This is rarely used in practice and shows the default
  11. This is rather optimistic, but the majority of devices running Android 13 should handle this correctly.
  12. Usually, you will want hardware attestation, so you'd need to explicitly disable it
  13. Warden Supreme does not need to enforce this because cryptographic nonces are used to ensure freshness.
    It is not recommended to set this value because many OEMs get this wrong.
  14. Required if you run Warden behind a proxy to fetch revocation information from Google servers.
  15. A single iOS app for evaluation purposes.
  16. 20A10 is a build number. For details see this explanation by David Shayer.
  17. Uses the test stage
  18. Custom trusted root is set, to enable generating iOS attestation statements in software for evaluation purposes.
  19. This could already be a production value, in preparation for the real iOS app
  20. This is simply Apple's recommendation plus five minutes offset
  21. Explicitly set production trusted roots as default
  22. Account for clock drift!

Note that revocation configuration has been revamped after 0.9.9999 (see below)!

Starting with Warden Supreme 1.0.0, it is possible to configure attestation only for iOS or only for Android by simply omitting either the androidAttestationConfiguration or the iosAttestationConfiguration, respectively.
In such cases, trying to verify an attestation statement for the not-configured platform will always return an error. The shorthand AttestationVerifier constructor that directly accepts androidAttestationConfiguration and iosAttestationConfiguration properties instead of a pre-configured Makoto instance does not support such omissions.

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 needs to be explicitly enabled. 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.

Flexible Android Revocation Configuration

Warden Supreme 1.0.0 and later completely revamp revocation handling. Instead of hardcoding a check against the official Google revocation list, it is now possible to configure an arbitrary number of revocation list loaders. Configuring an empty list completely disables revocation checks. Warden Supreme ships with three loaders by default:

  1. HttpLoader
  2. FileLoader
  3. InMemoryLoader

The first two handle caching by simply re-serving a previously loaded list until it expires. The format conforms to the revocation list schema specified by Google with the addition of date, expires, and lastModified fields. This allows for encoding freshness information directly into the revocation list, which is relevant when serving from the file system, instead of an HTTP server, where HTTP headers are used to encode this info.
The in-memory loader, on the other hand, will only ever serve a single, static pre-configured revocation list.

Attestation Verifier Setup

First, an AttestationVerifier instance needs to be created based on a Makoto instance:

Important Nonce Info

Under the hood, the attestation verifier needs a source to generate attestation challenges, track them, invalidate them, and match them against incoming attestation requests. Warden Supreme provides a secure nonce generation service and uses an in-memory challenge cache by default, which is fine for small to medium loads but not for larger production deployments. In such scenarios, roll your own (backed by Redis, for example).

val verifier = AttestationVerifier(makoto)/*(1)!*/
  1. Yes, it really is that simple 99% of the time! Sane defaults are set, including instructions (KeyConstraints) for the client to create a hardware-backed P-256 key.
Comprehensive list of Verifier options
val verifier = AttestationVerifier(
    makoto = makoto,
 /*(1)!*/attestationProofOID = serviceSpecificOID, //override default
 /*(2)!*/genericDeviceNameOID = null, //WardenDefaults.OIDs.DEVICE_NAME by default
 /*(3)!*/defaultKeyConstraints = KeyConstraints(
        algorithmParameters = AlgorithmParameters.EC(
            curve = ECCurve.SECP_256_R_1,
            digests = setOf(ECCurve.SECP_256_R_1.nativeDigest),
            allowKeyAgreement = false //DEFAULT
        ),
 /*(4)!*/keyProtection = KeyProtection(
            timeout = 30.seconds,
            deviceLock = false,
            biometry = true,
            allowNewBiometricFactors = false,
        )
    ),
    nonceValidity = 5.minutes, //DEFAULT
    nonceGenerator = suspend { CryptoRand.nextBytes(ByteArray(/*(5)!*/128)) },
 /*(6)!*/challengeValidator = redisBacked
)
  1. We want Warden Supreme to convey the attestation statement payload inside the CSR using a custom OID.
  2. We don't care about device names in this example.
  3. We explicitly specify the key we want to have created on the client.
    The values shown here correspond to the defaults, as this is supported by Android and iOS.
  4. We require user authentication to use the private key:
    • Protected by biometric auth
    • Usable for 30 seconds without reauthentication
    • Enrolling new biometric factors will invalidate the key
  5. We want extra long nonces (default: 64 bytes; max: 128 bytes).
  6. Checking and invalidating challenges is handled by a Redis-backed cache (not shown here; roll your own).

Instead of passing parameters programmatically, it is also possible to externalise configuration (see Externalising Configuration). As such, an AttestationVerifier can also be created by passing a SupremeConfiguration which contains iOS and Android attestation policies, as well as everything needed on top (object identifiers, etc.):

val verifierFromConfig = AttestationVerifier(
    configuration,
 /*(1)!*/WardenDefaults.nonceGenerator
) {/*(2)!*/clock, offset -> InMemoryChallengeCache(clock, offset) }
  1. Default, secure nonce generator.
  2. Default in-memory challenge validator.

Tip

Warden Supreme does not check whether a device has biometrics enrolled. If you choose to bind a to-be-attested key to biometric auth, you need to check device capa­bili­ties beforehand.
→ AuthCheckKit provides a unified multiplatform API for that.

Handling Requests

This example assumes Ktor. Since this is an example environment, TLS is omitted for brevity.

val server = embeddedServer(Netty, port = 8080) {
   /*(1)!*/install(ContentNegotiation) { json() }

    routing {
     /*(2)!*/get(PATH_CHALLENGE) {
           call.respond(
              /*(3)!*/verifier.issueChallenge(/*(4)!*/"$publicEndpoint/$PATH_ATTEST")
            )
        }
     /*(5)!*/post(PATH_ATTEST) {
         /*(6)!*/val decodedCSR = Pkcs10CertificationRequest.decodeFromDer(call.receive<ByteArray>())
            val result = verifier.verifyAttestation(decodedCSR) {
             /*(7)!*/val leafCertificate = signer.sign(
                 /*(8)!*/TbsCertificate(
                      /*(9)!*/serialNumber = Random.nextBytes(32),
                      /*(10)!*/publicKey = it.tbsCsr.publicKey,
                         signatureAlgorithm = signer.signatureAlgorithm.toX509SignatureAlgorithm().getOrThrow(),
                         validFrom = Asn1Time(Clock.System.now()),
                         validUntil = Asn1Time(Clock.System.now() + 10.days),
                         issuerName = issuerName,
                         subjectName = subjectName,
                    )
                ).getOrThrow()
             /*(11)!*/listOf(leafCertificate, caCert)
            }
         /*(12)!*/call.respond(result)
        }
    }
}.start(wait = false)
  1. We're using JSON to transmit the challenge and the final response.
  2. Endpoint to serve challenges to clients
  3. It does nothing but issuing challenges
  4. The full URL to post the attestation proof to
  5. Endpoint expecting CSRs containing attestation statement payloads
  6. Read the raw CSR from the HTTP body
  7. Here, inside the verifyAttestation lambda, we already have a verified attestation according to the configured makoto instance.
  8. Signing a TbsCertificate automatically creates an X.509 certificate
  9. The contents of your leaf certificate are up to you! What follows is just an example.
  10. it is the CSR. Remember: The key from the CSR is already attested here!
  11. Build the full certificate chain
  12. Finally, respond with the result:
    • On success, the certificate chain produced above will be returned.
    • On failure, an explanation about what failed will be returned.

Client Integration

Key Management

Trying to create a key for an existing alias will cause an error! Key management is your responsibility!

On the client, Warden Supreme is even easier to integrate. In contrast to the verifier, it is tailored around Ktor for its KMP goodness. Doing so allows for obtaining a certificate chain for an attested key in literally three short lines of code, if the challenge already specifies key constraints:

/*(1)!*/val client = AttestationClient(ktorClient)

/*(2)!*/when (val result = client.performAttestationFlow(ALIAS, Url(ENDPOINT_CHALLENGE))) {
       is AttestationResponse.Success ->/*(3)!*/myCertStore.store(result.certificateChain) //<-- You're golden!

       is AttestationResponse.Failure -> {
        /*(4)!*/when(result.kind) {
               AttestationResponse.Failure.Type.TRUST -> TODO()
               AttestationResponse.Failure.Type.TIME -> TODO()
               AttestationResponse.Failure.Type.CONTENT -> TODO()
               AttestationResponse.Failure.Type.INTERNAL -> TODO()
           }
       }
   }
  1. Create an AttestationClient from a Ktor client.
  2. Perform the fully integrated attestation flow iff key constraints are defined in the challenge consisting of the following steps:
    1. Fetches the challenge from ENDPOINT_CHALLENGE
    2. Automatically creates a key for ALIAS and an accompanying attestation statement payload. Beware: if a key for this alias exists, this will fail!
    3. Creates and signs a CSR, feeding the challenge and attestation statement payload into it.
    4. Sends it to the endpoint encoded in the received challenge.
  3. If everything worked out, store the received certificate chain using whatever storage approach you choose
  4. The kind of error tells you what went wrong. An AttestationResponse.Failure may also contain a string explaining further details.

This really is it! If you've made it this far, you have successfully issued certificates to mobile clients that fulfil your policy. The AttestationClient doesn't even come with any configuration options.

Beyond the Basics

The step-by-step guide above illustrates the intended ways of using Warden Supreme, bolting down as many moving parts as possible. Reality, though, is a complex beast and sometimes a little more control is needed. In particular, logging is often not only desirable, but essential. Hence, the verifier allows for hooking into every possible outcome of an attestation verification. This also allows for customising the explanations sent to clients.

Error Handling and Debugging

Head over to the dedicated debugging page to learn how to debug attestation issues. For a comprehensive guide on how to handle and interpret Warden Supreme's attestation errors, see the error handling page.

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 verifier provides four callbacks to analyse challenge validation, attestation errors, and success (without side effects):

val result = verifier.verifyAttestation(
 /*(1)!*/csr = csr,
 /*(2)!*/onChallengeValidated = { csr ->
        val customPayload = additionalPayload
        logger.log(Level.FINE,
            "Challenge validated (payload=$customPayload) based on nonce ${csr.tbsCsr.nonce} from CSR")
    },
 /*(3)!*/onPreAttestationError = {
        when (this) {
            is PreAttestationError.AttestationStatementExtraction -> TODO()
            is PreAttestationError.ChallengeExtraction -> TODO()
            is PreAttestationError.ChallengeVerification -> TODO()
            is PreAttestationError.OperationalError -> TODO()
        }
     /*(4)!*/null
    },
 /*(5)!*/onAttestationError = { debugStatement ->
        val attestationException = cause
        val reason = explanation
     /*(6)!*/logger.log(Level.WARNING,"Attestation failed due to $reason. "
            + debugStatement.serializeCompact(), attestationException)
     /*(7)!*/null
    },
 /*(8)!*/onAttestationSuccess = { attestedKey ->
        when (this) {
            is AttestationResult.Android.Verified -> TODO()
            is AttestationResult.IOS.Verified -> TODO()
        }

    }
)/*(9)!*/{ csr ->
    when (this) {
  1. This is simply the CSR from the client, as in the minimal example
  2. onChallengeValidated is called after the CSR’s challenge binding was validated. It has the validated challenge as receiver and the CSR as parameter.
  3. onPreAttestationError is called in case of operational/internal errors, or if the attestation statement cannot be extracted from a CSR. Different side-effect-free handling strategies can be employed based on error type.
  4. At the end of onPreAttestationError, it is possible to return a custom error explanation to the client (can be null).
  5. onAttestationError is called if the attestation statement fails to verify. This includes an invalid bootloader lock state, wrong package identifier, etc. See the dedicated error handling guide for more details!
  6. This logs a debug statement that can be used to replicate and debug the attestation process. Beware of privacy implications! See debugging and replaying diagnostics.
  7. Again, a custom error message can be sent to the client
  8. onAttestationSuccess is called right before an AttestationResponse.Success is returned. It has a verified attestation statement as its receiver and the associated public key as parameter. This can be useful for statistical analyses, for example.
  9. This is the certificate signing lambda, also having a fully verified attestation result as receiver. In contrast to onAttestationSuccess, it is not side-effect-free, but is expected to return a certificate chain, whose leaf certifies the attested key. As such, it receives the fully verified CSR as a parameter.

The step-by-step guide above will cover most use cases perfectly well. While extensive configurations were also included alongside the basic ones, Warden Supreme is, in fact, more flexible:

  • Instead of always using the defaults, it is possible to specify challenge properties manually for each challenge issued
  • Key constraints need not be specified. In that case, it is up to the client to create a key that is desired by the back-end and sign a CSR manually.
    (This is still very smooth, as can be seen in the API docs.)
  • By default, a device identifier is always encoded into the CSR; this can be toggled.

For more details, refer to the API docs on the verifier and on the client!