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 = setOf("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 = setOf("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 = setOf("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)!*/customProperties = mapOf("an app flag" to "is present"),
            )
        ),
     /*(10)!*/androidVersion = 130000, patchLevel = PatchLevel(2023, 12), requireStrongBox = false,
        allowBootloaderUnlock = false, //DEFAULT
     /*(11)!*/verifiedBootKeys = linkedSetOf(
            VerifiedBootKey.OEM,
            VerifiedBootKey.Digest(
                "00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff".parseHex()
            )
        ),
     /*(12)!*/requireRollbackResistance = false, //DEFAULT
     /*(13)!*/ignoreLeafValidity = false, // defaults to true
        hardwareTrustedRoots = GOOGLE_DEFAULT_HARDWARE_TRUST_ANCHORS, //DEFAULT
        softwareTrustedRoots = GOOGLE_SOFTWARE_TRUST_ANCHORS_UNTIL_A12, //DEFAULT
        verificationSecondsOffset = 0, //DEFAULT
     /*(14)!*/disableHardwareAttestation = false,
        enableSoftwareAttestation = false, //DEFAULT
     /*(15)!*/attestationStatementValiditySeconds = null, // DEFAULT; no validity time checks!
     /*(16)!*/revocation = listOf(
            AndroidRevocationList.GoogleDefaultLoaderConfig.withHttpProxy("https://192.168.178.74:8000")
        ),
        requireRemoteKeyProvisioning = false, //DEFAULT
     /*(17)!*/enforceFactoryProvisionedChainValidity = true, //DEFAULT
     /*(18)!*/customProperties = mapOf("an Android flag" to "is present") //DEFAULT
    ),

    iosAttestationConfiguration = IosAttestationConfiguration(
        applications = listOf(
         /*(19)!*/IosAttestationConfiguration.AppData(
                teamIdentifier = "9CYHJNG644",
                bundleIdentifier = "at.asitplus.attestation-client",
             /*(20)!*/iosVersionOverride = OsVersions("16.0", "20A10"),
             /*(21)!*/sandbox = true, //defaults to false
             /*(22)!*/trustedRootOverrides = myCustomRoots,
             /*(23)!*/ customProperties = mapOf("and iOS flag" to "is present"),
            )
        ),
                /* Same as 17.0 ↘↘ */
     /*(24)!*/iosVersion = OsVersions("17", "21A36"), //defaults to null (= no version check)
     /*(25)!*/attestationStatementValiditySeconds = 600, //DEFAULT
     /*(26)!*/trustedRoots = APPLE_DEFAULT_TRUSTED_ROOTS, //DEFAULT
     /*(27)!*/customProperties = mapOf("a global iOS flag" to "is present"), //DEFAULT
    ),
    clock = Clock.System, //DEFAULT
 /*(28)!*/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. We want to mark the hardened app. We an use a Map<String,String> to attach arbitrary properties to the attestation configuration.
  10. 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.
  11. Allow OEM-managed VERIFIED boot and one pinned SELF_SIGNED verified boot key for locked devices only. Omit OEM if you want to trust only explicitly pinned custom ROM keys. This has no effect once unlocked bootloaders are allowed.
  12. This is rarely used in practice and shows the default
  13. This is rather optimistic, but the majority of devices running Android 13 should handle this correctly.
  14. Usually, you will want hardware attestation, so you'd need to explicitly disable it
  15. 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.
  16. Required if you run Warden behind a proxy to fetch revocation information from Google servers.
  17. Factory-provisioned Android attestation chains are checked for timely certificate validity by default. Only disable this for old devices with expired intermediate certificates after accepting the resulting risk.
  18. We want to attach a Map<String, String> with a single entry. Can be extended arbitrarily.
  19. A single iOS app for evaluation purposes.
  20. 20A10 is a build number. For details see this explanation by David Shayer.
  21. Uses the test stage
  22. Custom trusted root is set, to enable generating iOS attestation statements in software for evaluation purposes.
  23. We want to mark the iOS app. We an use a Map<String,String> to attach arbitrary properties to the attestation configuration.
  24. This could already be a production value, in preparation for the real iOS app
  25. This is simply Apple's recommendation plus five minutes offset
  26. Explicitly set production trusted roots as default
  27. We want to attach a Map<String, String> with a single entry. Can be extended arbitrarily.
  28. Account for clock drift!

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

Pinned SELF_SIGNED Android boot keys

If you need to trust a known-good custom Android build, configure verifiedBootKeys. The default [OEM] accepts vendor-managed VERIFIED boot, [OEM, "<hex>"] accepts either vendor-managed VERIFIED boot or an explicitly whitelisted SELF_SIGNED key, and ["<hex>"] accepts only explicitly whitelisted SELF_SIGNED keys. Keep allowBootloaderUnlock = false, otherwise bootloader-lock, verified boot state, and verified boot key checks are skipped entirely.

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.

Accepting Expired Non-RKP Certificate Chains

As of May 2026, the intermediate certificates and roots of some old non-RKP devices — that is, devices no longer receiving security updates — have expired. Since these devices no longer receive updates, Warden Supreme considers them insecure by default, which aligns with Warden Supreme properly checking timely certificate validity by default. Note that for some devices, there is no way to update these certificate chains to fix the validity issue, while for others, the OEMs simply don't care. Still, Warden Supreme keeps you in control: if you must support these devices, you can completely disable timely certificate validity checks for non-RKP devices only. To do so, set enforceFactoryProvisionedChainValidity = false in the AndroidAttestationConfiguration. See the comprehensive example of Makoto config options for details.

Trusting GrapheneOS

A practical example of pinning SELF_SIGNED verified boot keys is trusting GrapheneOS, as shown below.
To obtain current GrapheneOS verified boot key hashes, use GrapheneOS's attestation.json and verify it against the detached signature at attestation.json.sig.

val grapheneOsVerifiedBootKeys = setOf(
    "d8f879d10419eddc9fcda6280718be763f6bf12299e1f72df3ea8ad8a8eb7f80",
    "55a2d44103e56d5ec65496399c417987ba77730e6488fc60ba058d09fc3caee3",
    "141d7fc32af7958a416f2661b37cf6f27bfb376fb5ce616aeaa27a82c7a04f74",
    "4e8ee8f717754052198ca6d2d3aaa232e2461b4293c0d6f297e519cc778de093",
    "3f7415ea26f5df5b14ea6d153256071a7a1af9ce7b0970b7311cc463c7ea02c7",
    "0508de44ee00bfb49ece32c418af1896391abde0f05b64f41bc9a2dfb589445b",
    "af4d2c6e62be0fec54f0271b9776ff061dd8392d9f51cf6ab1551d346679e24c",
    "55d3c2323db91bb91f20d38d015e85112d038f6b6b5738fe352c1a80dba57023",
    "f729cab861da1b83fdfab402fc9480758f2ae78ee0b61c1f2137dd1ab7076e86",
    "9e6a8f3e0d761a780179f93acd5721ba1ab7c8c537c7761073c0a754b0e932de",
    "096b8bd6d44527a24ac1564b308839f67e78202185cbff9cfdcb10e63250bc5e",
    "896db2d09d84e1d6bb747002b8a114950b946e5825772a9d48ba7eb01d118c1c",
    "cd7479653aa88208f9f03034810ef9b7b0af8a9d41e2000e458ac403a2acb233",
    "ee0c9dfef6f55a878538b0dbf7e78e3bc3f1a13c8c44839b095fe26dd5fe2842",
    "94df136e6c6aa08dc26580af46f36419b5f9baf46039db076f5295b91aaff230",
    "508d75dea10c5cbc3e7632260fc0b59f6055a8a49dd84e693b6d8899edbb01e4",
    "bc1c0dd95664604382bb888412026422742eb333071ea0b2d19036217d49182f",
    "3efe5392be3ac38afb894d13de639e521675e62571a8a9b3ef9fc8c44fd17fa1",
    "08c860350a9600692d10c8512f7b8e80707757468e8fbfeea2a870c0a83d6031",
    "439b76524d94c40652ce1bf0d8243773c634d2f99ba3160d8d02aa5e29ff925c",
    "f0a890375d1405e62ebfd87e8d3f475f948ef031bbf9ddd516d5f600a23677e8"
).map { VerifiedBootKey.Digest(it.parseHex()) }.toSet() /*(1)!*/

val grapheneOsConfig = SupremeConfiguration(
    AndroidAttestationConfiguration(
        AndroidAttestationConfiguration.AppData(
            packageName = androidAppPackage,
            signerFingerprints = setOf(appSignerFingerprint)
        ),
        verifiedBootKeys = grapheneOsVerifiedBootKeys
                         + VerifiedBootKey.OEM /*(2)!*/
        ,
        allowBootloaderUnlock = false /*(3)!*/
    )
)
  1. Order is irrelevant, and Warden Supreme makes this explicit by forcing a Set of verified boot key hashes
  2. Also keep OEM so stock/vendor Android trusted, while pinning all GrapheneOS verified boot keys.
  3. Keep allowBootloaderUnlock = false (which is the default). Otherwise, bootloader-lock, verified boot state, and verified boot key checks are skipped entirely.

The concrete digests pinned in the example above were retrieved on 2026-06-15. Do fetch and verify them yourself, if you want to trust GrapheneOS

Attaching Custom Configuration Properties

Warden Supreme defines a canonical configuration format with custom loaders for Spring Boot and Hoplite (see Externalising Configuration). This effectively removes the need for any custom configuration parsing. At the same time, it might be useful to attach arbitrary configuration properties to an application's attestation configuration, or even globally to the configuration subtree dedicated to attestation configuration.

To provide both - canonical configuration parsing, and the ability to extend the configuration - it is possible to attach mappings of string keys to string values under customProperties at the following layers:

  • AndroidAttestationConfiguration
  • AndroidAttestationConfiguration.AppData
  • IosAttestationConfiguration
  • IosAttestationConfiguration.AppData

Using a map of string keys to string values is the pragmatic choice: It has well-defined parsing behaviour, can be arbitrarily extended, and both keys and values are flexible enough to carry anything. If complex structures are to be attached, they should be serialised to a string and deserialised before use.

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 a bounded in-memory challenge cache by default. The default cache allows up to 100_000 unexpired in-flight challenges per verifier instance. Once full, it prunes expired entries and then throws InMemoryChallengeCache.ChallengeCacheFullException instead of evicting active challenges. Challenge nonces are sensitive replay-protection material. Treat them as bearer values for their short lifetime: do not log them, do not expose them across sessions or callers, serve them only over protected transport, and bind/rate-limit callers in your HTTP layer when your service needs that context. Map that exception at your HTTP layer to 429 Too Many Requests and, if useful, a Retry-After header. The cache deliberately does not implement backoff; caller-aware rate limiting needs IP, account, tenant, or device context and should live outside Warden Supreme. For horizontally scaled or high-volume deployments, use a distributed TTL-backed ChallengeValidator instead (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). This is the recommended approach when multiple verifier instances issue challenges.

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 bounded in-memory challenge validator.

Additional Verification Policy

Makoto verifies the generic platform and app attestation policy: challenge freshness, attestation statement validity, trust anchors, app identifiers, signer digests, boot state, OS and app version constraints, and similar platform-specific properties. Some back-ends also need service-specific checks on top of that policy, for example tenant binding, account state, risk engine decisions, device inventory rules, or values carried in AttestationChallenge.additionalPayload.

Use additionalVerifications for such checks:

val response = verifier.verifyAttestation(
 /*(1)!*/csr = csr,
 /*(2)!*/additionalVerifications = { receivedCsr, verifiedAttestation ->
        val tenant = /*(3)!*/additionalPayload?.get("tenant")?.toString()
        if (tenant != expectedTenant) {
         /*(4)!*/AttestationResponse.Failure(
                AttestationResponse.Failure.Type.CONTENT,
                "Attestation challenge does not match tenant policy"
            )
        } else {
         /*(5)!*/null
        }
    },
 /*(6)!*/certificateIssuer = { verifiedCsr ->
        issueCertificateChain(verifiedCsr)
    }
)
  1. This is the verified CSR from the client.
  2. additionalVerifications runs after challenge validation, attestation verification, and CSR signature verification. The validated AttestationChallenge is the receiver; the CSR and verified attestation result are parameters.
  3. Challenge payload can carry service context that is not part of generic platform attestation policy.
  4. Return an AttestationResponse.Failure to stop the flow with your own failure kind and explanation.
  5. Return null to continue to certificate issuance.
  6. Certificate issuance only runs if generic attestation and all additional checks succeed.

Do not throw from this lambda; if an exception escapes, Warden Supreme maps it to an INTERNAL failure and does not call the certificate issuer.

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 issue challenges. In production, catch InMemoryChallengeCache.ChallengeCacheFullException here and return 429 Too Many Requests; apply caller-aware rate limiting outside the verifier.
  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.

Android ATTEST_KEY Is Intentionally Unsupported

Warden Supreme currently only supports Android key binding when the leaf certificate is itself the attestation certificate.

Using Android's ATTEST_KEY purpose to create subordinate keys or certificates below an attested key is intentionally unsupported for now. Supporting that safely would make certificate-chain validation more complex, and Warden will only consider it after thorough investigation to ensure length-extension attacks remain prohibited.

If you need that use case, the supported approach is:

  1. create and attest the would-be issuing key through Warden's regular flow
  2. issue a certificate for that key on your CA infrastructure
  3. manually set the appropriate CA-related extensions and key-usage flags so it becomes a legitimate intermediate CA certificate allowed to sign subordinate certificates

In other words: manual issuance work is currently required for attested intermediate CAs.

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.

Lower-level APIs

If you need more control, you can also manually perform individual steps, as shown below

     authPromptCancelText = "Abort",
     /*additional extensions and attributes go here*/
     ).getOrThrow() //handle error

 /*(4)!*/when (val result = client.attest(csr, challenge.attestationEndpointUrl)) {
        is AttestationResponse.Success -> {
         /*(5)!*/myCertStore.store(result.certificateChain) //<-- You're golden!
        }

        is AttestationResponse.Failure -> {
         /*(6)!*/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. Fetch the challenge
  3. Create a local attestation proof (a signed CSR) to be sent to the verifier
  4. Send it to the verifier endpoint contained in the challenge
  5. Store the received certificate chain on success
  6. Handle errors based on what went wrong

In addition, even more low-level access is possible by directly using Signum Supreme:

/*(1)!*/val client = AttestationClient(ktorClient)
/*(2)!*/val serverChallenge = client.getChallenge(Url(ENDPOINT_CHALLENGE)).getOrThrow()

/*(3)!*/val signer = PlatformSigningProvider.createSigningKey(ALIAS) {
       ec {
           curve = ECCurve.SECP_256_R_1
           purposes {
               signing = true
               keyAgreement = true
           }
       }
       hardware {
           backing = REQUIRED
           attestation {
            /*(4)!*/challenge = serverChallenge.nonce
           }
           protection {
               factors {
                   biometry = true
               }
               timeout = 30.seconds
           }
       }
   }.getOrThrow() //handle error

/*(5)!*/val csr = signer.createCsr(serverChallenge,
    /*optional SubjectName, extns, attributes go here*/
    ).getOrThrow()

/*(6)!*/when (val result = client.attest(csr, serverChallenge.attestationEndpointUrl)) {
       is AttestationResponse.Success -> {
        /*(7)!*/myCertStore.store(result.certificateChain) //<-- You're golden!
       }

       is AttestationResponse.Failure -> {
        /*(8)!*/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. Fetch the challenge
  3. Create and configure a signer using Signum Supreme based on your demands
    This means that you can also override any client hints from the received challenge
  4. Don't forget to pass the nonce to enable attestation!
  5. Create the CSR as desired
  6. Send it to the verifier endpoint contained in the challenge
  7. Store the received certificate chain on success
  8. Handle errors based on what went wrong

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!