Attestation Generator
Besides verifying Android key attestations, Warden Supreme can also produce them. The generator creates attestation statements, the certificate chains carrying them, and the corresponding private keys. It is available as a Kotlin library and as a command-line tool. Typical uses include automated attestation tests, reproducing device quirks, and inspecting the format without first writing a verifier integration.
Statements are built from the same AttestationKeyDescription and AuthorizationList types consumed by the parser.
This also permits generating malformed or outright nonsensical input. Real devices have already demonstrated why that
is a useful feature.
Command-Line Tool
Test fixtures do not require Kotlin code. The stand-alone generator takes one JSON configuration and writes a certificate chain and attested private key for every statement, together with the root certificate:
To build statements from Kotlin instead, add the generator as a test dependency:
Issuing an Attestation
An issuer holds the root and the attestation CA chain. Every call to issue creates a fresh attestation key and a fresh
attested leaf key, mirroring the key hierarchy produced by a device:
val issuer = androidAttestationIssuer {
/*(1)!*/factoryProvisioned(AttestationKeyDescription.SecurityLevel.TRUSTED_ENVIRONMENT)
}
val attestation = issuer.issue {
/*(2)!*/nonce = "server-challenge".encodeToByteArray()
hardwareEnforced = AuthorizationList(
purpose = setOf(AuthorizationList.KeyPurpose.SIGN),
algorithm = AuthorizationList.Algorithm.EC,
ecCurve = AuthorizationList.ECCurve.P_256,
noAuthRequired = AuthorizationList.NoAuthRequired,
origin = AuthorizationList.Origin.GENERATED,
)
}
/*(3)!*/val chain = attestation.certificateChain
/*(4)!*/val attestedKey = attestation.leafSigner
- The provisioning method determines the chain shape.
factoryProvisioned()places one factory CA below the root;rkp()usesDroid CA2andDroid CA3. The verifier derives the security level from this structure, so getting it wrong changes the meaning of the chain. See Automated Attestation Tests. - The attestation challenge. Unspecified fields retain the defaults of an otherwise empty KeyMint 4.0 statement.
- Certificate chains are returned leaf first and root last. Pass the chain to
verify()and configure its root as a trust anchor. - The attested private key is returned as well, ready to sign a CSR or another payload covered by the test.
A Complete Device Statement
Use the parser's own AuthorizationList constructor to fill in a statement. It covers the entire schema and keeps the
generator from growing a second, inevitably slightly different model:
val attestation = issuer.issue {
/*(1)!*/attestationVersion = 400
keyMintVersion = 400
securityLevel = AttestationKeyDescription.SecurityLevel.TRUSTED_ENVIRONMENT
nonce = "server-challenge".encodeToByteArray()
/*(2)!*/softwareEnforced = AuthorizationList(
creationDateTime = AuthorizationList.CreationDateTime(createdAt),
attestationApplicationId = AuthorizationList.AttestationApplicationId(
packageInfos = setOf(
AuthorizationList.AttestationPackageInfo("at.asitplus.attestation_client", version = 1u)
),
signatureDigests = setOf(ByteArray(32) { 0x11 }),
),
)
/*(3)!*/hardwareEnforced = AuthorizationList(
purpose = setOf(AuthorizationList.KeyPurpose.SIGN),
algorithm = AuthorizationList.Algorithm.EC,
keySize = AuthorizationList.KeySize(BitLength(256u)),
ecCurve = AuthorizationList.ECCurve.P_256,
noAuthRequired = AuthorizationList.NoAuthRequired,
origin = AuthorizationList.Origin.GENERATED,
/*(4)!*/rootOfTrust = AuthorizationList.RootOfTrust(
verifiedBootKeyDigest = ByteArray(32) { 0x22 },
deviceLocked = true,
verifiedBootState = AuthorizationList.RootOfTrust.VerifiedBootState.Verified,
verifiedBootHash = ByteArray(32) { 0x33 },
),
osVersion = AuthorizationList.OsVersion(14u, 0u, 0u),
osPatchLevel = AuthorizationList.OsPatchLevel(2026u, Month.AUGUST),
)
}
securityLevelsets both the attestation and KeyMint security levels; either can still be overridden separately.- Creation time and application identity belong to the software-enforced list.
- Key parameters and properties vouched for by the TEE belong to the hardware-enforced list.
- The root of trust records verified boot state, the boot-key digest, and whether the bootloader is locked. These are usually the interesting bits when testing policy.
Remote Key Provisioning
val issuer = androidAttestationIssuer {
/*(1)!*/rkp(AttestationKeyDescription.SecurityLevel.STRONGBOX)
}
val attestation = issuer.issue {
securityLevel = AttestationKeyDescription.SecurityLevel.STRONGBOX
nonce = "server-challenge".encodeToByteArray()
}
/*(2)!*/val chain = attestation.certificateChain
- Remote key provisioning follows Google's CA names and records the security level in the attestation certificate's
organisation (
O=TEEorO=StrongBox). - The resulting chain has five certificates:
root → Droid CA2 → Droid CA3 → attestation → leaf.
One Trust Anchor, Many Attestations
An issuer is a small test PKI. Keep it for the lifetime of the suite, register its root once, and issue all test attestations below it:
/*(1)!*/val issuer = androidAttestationIssuer {
factoryProvisioned()
/*(2)!*/issuedAt = Instant.parse("2026-01-15T09:30:00Z")
validity = 90.days
}
/*(3)!*/val trustAnchor = issuer.rootCertificate
val attestations = List(3) { index ->
issuer.issue { nonce = "challenge-$index".encodeToByteArray() }
}
- To reuse a root, assign
root = RootSpec(certificatePem, privateKeyPkcs8Pem). Otherwise, the generator creates one. - Explicit issuance times and lifetimes let tests move a chain to whichever unfortunate date is required.
- Register the root with the test configuration as
TrustedRoot.PublicKeyorTrustedRoot.Certificate.
Negative Test Vectors
Negative tests rarely call for well-behaved input. mangle replaces any property with raw ASN.1, allowing the generator
to produce structures that a parser must handle safely and a verifier must reject:
val attestation = issuer.issue {
hardwareEnforced = AuthorizationList(algorithm = AuthorizationList.Algorithm.EC)
/*(1)!*/.mangle(AuthorizationList.KeySize, "a30402020080")
}
- Pass the complete, explicitly tagged property as DER. This example encodes
keySize [3]with an unexpected nestedINTEGER 128. Invalid encodings are fair game too.
Command-Line Use
The DSL can export an issuer and its statements to the JSON understood by the command-line tool. A fixture developed in Kotlin can therefore move into CI unchanged:
val issuer = androidAttestationIssuer { factoryProvisioned() }
/*(1)!*/val json = issuer.configuration(
attestations = listOf(
/*(2)!*/at.asitplus.attestation.generator.attestationSpec {
nonce = "server-challenge".encodeToByteArray()
}
),
outputDirectory = "build/attestations",
).toJson()
configuration()returns a serialisable description of the issuer.- A configuration may contain any number of statements; each gets its own certificate chain.
- Generated root material is included in the configuration. Re-running the command therefore keeps the same trust anchor instead of quietly inventing a new PKI.
Configurations are plain JSON. Authorization-list properties contain the complete DER encoding of each property, which also accommodates invalid values. Warden Supreme's test suite generates and verifies all four examples on every build:
Minimal Configuration
{
"issuer": {
"provisioning": "FACTORY",
"securityLevel": "TEE",
"root": null,
"issuedAt": "2026-01-15T09:30:00Z",
"validity": "PT8760H"
},
"attestations": [
{
"keyDescription": {
"attestationVersion": 400,
"attestationSecurityLevel": "TEE",
"keyMintVersion": 400,
"keyMintSecurityLevel": "TEE",
"attestationChallenge": "",
"uniqueId": "",
"softwareEnforced": [],
"hardwareEnforced": []
},
"createdAt": "2026-01-15T09:35:00Z",
"leafCanSignCertificates": false
}
],
"outputDirectory": "attestations"
}
You can download this example here.
What a TEE-Backed Device Attests to
A factory-provisioned TEE chain containing application identity, key parameters, root of trust, OS version, and patch level.
{
"issuer": {
"provisioning": "FACTORY",
"securityLevel": "TEE",
"root": null,
"issuedAt": "2026-01-15T09:30:00Z",
"validity": "PT8760H"
},
"attestations": [
{
"keyDescription": {
"attestationVersion": 400,
"attestationSecurityLevel": "TEE",
"keyMintVersion": 400,
"keyMintSecurityLevel": "TEE",
"attestationChallenge": "7365727665722d6368616c6c656e6765",
"uniqueId": "",
"softwareEnforced": [
"bf853d080206019bc1021da0",
"bf85454f044d304b31253023041e61742e61736974706c75732e6174746573746174696f6e5f636c69656e74020101312204201111111111111111111111111111111111111111111111111111111111111111"
],
"hardwareEnforced": [
"a1053103020102",
"a203020103",
"a30402020100",
"aa03020101",
"bf8377020500",
"bf853e03020100",
"bf85404c304a042022222222222222222222222222222222222222222222222222222222222222220101ff0a010004203333333333333333333333333333333333333333333333333333333333333333",
"bf85410502030222e0",
"bf8542050203031770"
]
},
"createdAt": "2026-01-15T09:35:00Z",
"leafCanSignCertificates": false
}
],
"outputDirectory": "attestations/tee"
}
You can download this example here.
StrongBox, Provisioned Remotely
A remotely provisioned chain with a device-unique attestation and module hash, valid for half an hour.
{
"issuer": {
"provisioning": "RKP",
"securityLevel": "STRONGBOX",
"root": null,
"issuedAt": "2026-01-15T09:30:00Z",
"validity": "PT30M"
},
"attestations": [
{
"keyDescription": {
"attestationVersion": 400,
"attestationSecurityLevel": "STRONGBOX",
"keyMintVersion": 400,
"keyMintSecurityLevel": "STRONGBOX",
"attestationChallenge": "7365727665722d6368616c6c656e6765",
"uniqueId": "",
"softwareEnforced": [],
"hardwareEnforced": [
"a1053103020102",
"a203020103",
"aa03020101",
"bf8550020500",
"bf85542204204444444444444444444444444444444444444444444444444444444444444444"
]
},
"createdAt": "2026-01-15T09:35:00Z",
"leafCanSignCertificates": false
}
],
"outputDirectory": "attestations/strongbox-rkp"
}
You can download this example here.
Two Statements That Must Be Rejected
One statement replaces keySize with valid but unexpected ASN.1. The other reports failed verified boot and carries
a property outside the schema.
{
"issuer": {
"provisioning": "FACTORY",
"securityLevel": "TEE",
"root": null,
"issuedAt": "2026-01-15T09:30:00Z",
"validity": "PT8760H"
},
"attestations": [
{
"keyDescription": {
"attestationVersion": 400,
"attestationSecurityLevel": "TEE",
"keyMintVersion": 400,
"keyMintSecurityLevel": "TEE",
"attestationChallenge": "7365727665722d6368616c6c656e6765",
"uniqueId": "",
"softwareEnforced": [],
"hardwareEnforced": [
"a203020103",
"a30402020080"
]
},
"createdAt": "2026-01-15T09:35:00Z",
"leafCanSignCertificates": false
},
{
"keyDescription": {
"attestationVersion": 400,
"attestationSecurityLevel": "TEE",
"keyMintVersion": 400,
"keyMintSecurityLevel": "TEE",
"attestationChallenge": "7365727665722d6368616c6c656e6765",
"uniqueId": "",
"softwareEnforced": [],
"hardwareEnforced": [
"a203020103",
"bf85404c304a042022222222222222222222222222222222222222222222222222222222222222220101000a010204203333333333333333333333333333333333333333333333333333333333333333",
"bfce0f03020101"
]
},
"createdAt": "2026-01-15T09:35:00Z",
"leafCanSignCertificates": false
}
],
"outputDirectory": "attestations/negative"
}
You can download this example here.
Run the generator with any of these files to write the root certificate and one certificate chain and private key per
statement to outputDirectory:
Test Material Only
Generated chains lead to a root created by the generator or supplied in its configuration. They have no connection to a Google root and validate only where this test root is explicitly trusted. Keep it out of production configuration.