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. 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 or any other HTTP framework of your choice.
- On the back-end, add the
verifierdependency: - On mobile clients, add the
clientdependency:
High-Level Attestation Flow
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.
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 CMP client app. To get going, the following steps are required:
- Decide on HTTPS endpoints to issue challenges and verify attestation statements, and record the Apps identifiers and signer digests (Android) / team ID (iOS)
- back-end:
- Configure a
Makotoinstance based on your policy and app identifiers. - Create an
AttestationVerifierbased on the configuredMakotoinstance, your CA certificate, and signing keys - Wire HTTPS endpoints to the
AttestationVerifierand start an HTTP server
- Configure a
- Mobile App
- Wire the verifier to the HTTPS endpoints to an
AttestationClient - Call into the Endpoints
- Store the received certificate chain after a successful attestation
- Wire the verifier to the HTTPS endpoints to an
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",
)
)
)
- 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.
- 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 proofs 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_A11, //DEFAULT
verificationSecondsOffset = 0, //DEFAULT
/*(12)!*/disableHardwareAttestation = false,
enableSoftwareAttestation = false, //DEFAULT
/*(13)!*/enableNougatAttestation = false, //DEFAULT
/*(14)!*/attestationStatementValiditySeconds = null, // DEFAULT; no validity time checks!
/*(15)!*/httpProxy = "https://192.168.178.74:8000",
requireRemoteKeyProvisioning = false //DEFAULT
),
iosAttestationConfiguration = IosAttestationConfiguration(
applications = listOf(
/*(16)!*/IosAttestationConfiguration.AppData(
teamIdentifier = "9CYHJNG644",
bundleIdentifier = "at.asitplus.attestation-client",
/*(17)!*/iosVersionOverride = OsVersions("16.0", "20A10"),
/*(18)!*/sandbox = true, //defaults to false
/*(19)!*/trustedRootOverrides = myCustomRoots
)
),
/* Same as 17.0 ↘↘ */
/*(20)!*/iosVersion = OsVersions("17", "21A36"), //defaults to null (= no version check)
/*(21)!*/attestationStatementValiditySeconds = 600, //DEFAULT
/*(22)!*/trustedRoots = APPLE_DEFAULT_TRUSTED_ROOTS //DEFAULT
),
clock = Clock.System, //DEFAULT
/*(23)!*/verificationTimeOffset = 5.minutes, //OPTIONAL, defaults shown
)
- The basic application for the masses
- A second, experimental high-security app
- Different package name from the first app
- Enforce minimum version, Android 16, an up-do-date security patches
- Allow for more leeway
- Only remote key provisioning is considered trustworthy for this app
- Only the RKP trust anchor is considered trustworthy
- We want our app to have a dedicated HSM
- 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.
- This is hardly used in practice and shows the default
- This is rather optimistic, but the majority of devices running Android 13 should not screw this up.
- Usually, you will always want hardware attestation, so you'd need to explicitly disable it
- This is for devices launched with Android 7.0 who have not received an upgraded KeyMaster. Such devices are only capable of key attestation.
Their app attestation claims are only software-attested! - 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 mess this up! - Required if you run Warden behind a proxy to fetch revocation information from Google servers!
- A single iOS app for evaluation purposes.
20A10is a build number. For details see this explanation by David Shayer.- Uses the test stage
- Custom trusted root is set, to enable generating iOS attestation proofs in software for evaluation purposes.
- This could already be a production value, in preparation for the real iOS app
- This is simply Apple's recommendation plus five minutes offset
- Explicitly set production trusted roots as default
- Account for clock drift!
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.
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, 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 load but not for larger production deployments. In such scenarios, roll your own (backed by Redis, for example)!
- 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
makoto = makoto,
/*(1)!*/attestationProofOID = serviceSpecificOID, //override default
/*(2)!*/includeGenericDeviceName = false, //true 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
)
- We want Warden Supreme to convey the attestation proof inside the CSR using a custom OID.
- We don't care about device IDs
- 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. - 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
- We want extra long nonces! (Default: 64 bytes. Max: 128 bytes)
- Checking and invalidating challenges is handled by a Redis-backed cache (not shown here, roll your own!)
Instead of passing a Makoto instance, it is also possible to directly use bare configuration parameters directly, as if configuring Makoto, to cut out the middle-man in code.
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)
- We're using JSON to transmit the challenge and the final response.
- Endpoint to serve challenges to clients
- It does nothing but issuing challenges
- The full URL to post the attestation proof to
- Endpoint expecting CSRs containing attestation proofs
- Read the raw CSR from the HTTP body
- Here, inside the
verifyAttestationlambda, we already have a verified attestation according to the configuredmakotoinstance. - Signing a
TbsCertificateautomatically creates an X.509 certificate - The contents of your leaf certificate are up to you! What follows is just an example.
itis the CSR. Remember: The key from the CSR is already attested here!- Build the full certificate chain
- 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 are 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()
}
}
}
- Create an
AttestationClientfrom a Ktor client. - Perform the fully integrated attestation flow iff key constraints are defined in the challenge consisting of the following steps:
- Fetches the challenge from
ENDPOINT_CHALLENGE - Automatically creates a key for
ALIASand an accompanying attestation proof. Beware: if a key for this alias exists, this will fail! - Creates and signs a CSR, feeding the challenge and attestation proof into it.
- Sends it to the endpoint encodes into the received challenge.
- Fetches the challenge from
- If everything worked out, store the received certificate chain using whatever means you decide on
- The kind of error tells you what went wrong. An
AttestationResponse.Failuremay also contain a string explaining further details.
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.
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 customizing the explanations sent to clients.
Debugging
Head over to the dedicated debugging page to learn how to debug attestation issues!
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 backend, 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):
val result = verifier.verifyAttestation(
/*(1)!*/csr = csr,
/*(2)!*/onPreAttestationError = {
when (this) {
is PreAttestationError.AttestationStatementExtraction -> TODO()
is PreAttestationError.ChallengeExtraction -> TODO()
is PreAttestationError.ChallengeVerification -> TODO()
is PreAttestationError.OperationalError -> TODO()
}
/*(3)!*/null
},
/*(4)!*/onAttestationError = { debugStatement ->
val attestationException = cause
val reason = explanation
/*(5)!*/logger.log(Level.WARNING,"Attestation failed due to $reason. "
+ debugStatement.serializeCompact(), attestationException)
/*(6)!*/null
},
/*(7)!*/onAttestationSuccess = { attestedKey ->
when (this) {
is AttestationResult.Android.Verified -> TODO()
is AttestationResult.IOS.Verified -> TODO()
}
}
)/*(8)!*/{ csr ->
when (this) {
is AttestationResult.Android.Verified -> TODO()
is AttestationResult.IOS.Verified -> TODO()
}
TODO("Refer to minimum example for certificate issuance")
}
- This is simply the CSR from the client, as in the minimal example
onPreAttestationErroris 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.- At the end of
onPreAttestationError, it is possible to return a custom error explanation to the client (can be null). onAttestationErroris called if the attestation statement fails to verify. This includes an invalid bootloader lock state, wrong package identifier, etc.- 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.
- Again, a custom error message can be sent to the client
onAttestationSuccessis called right before anAttestationResponse.Successis 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.- This is the certificate signing lambda, also having a fully verified attestation result as receiver.
In contrast to
onAttestationSuccessit 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 as 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!