Skip to content

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:

java -jar attestation-generator.jar config.json

attestation-generator.jar

To build statements from Kotlin instead, add the generator as a test dependency:

testImplementation("at.asitplus.warden:generator:$version")

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
  1. The provisioning method determines the chain shape. factoryProvisioned() places one factory CA below the root; rkp() uses Droid CA2 and Droid CA3. The verifier derives the security level from this structure, so getting it wrong changes the meaning of the chain. See Automated Attestation Tests.
  2. The attestation challenge. Unspecified fields retain the defaults of an otherwise empty KeyMint 4.0 statement.
  3. Certificate chains are returned leaf first and root last. Pass the chain to verify() and configure its root as a trust anchor.
  4. 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),
    )
}
  1. securityLevel sets both the attestation and KeyMint security levels; either can still be overridden separately.
  2. Creation time and application identity belong to the software-enforced list.
  3. Key parameters and properties vouched for by the TEE belong to the hardware-enforced list.
  4. 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
  1. Remote key provisioning follows Google's CA names and records the security level in the attestation certificate's organisation (O=TEE or O=StrongBox).
  2. 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() }
}
  1. To reuse a root, assign root = RootSpec(certificatePem, privateKeyPkcs8Pem). Otherwise, the generator creates one.
  2. Explicit issuance times and lifetimes let tests move a chain to whichever unfortunate date is required.
  3. Register the root with the test configuration as TrustedRoot.PublicKey or TrustedRoot.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")
}
  1. Pass the complete, explicitly tagged property as DER. This example encodes keySize [3] with an unexpected nested INTEGER 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()
  1. configuration() returns a serialisable description of the issuer.
  2. A configuration may contain any number of statements; each gets its own certificate chain.
  3. 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:

java -jar attestation-generator.jar generator-tee-factory.json

attestation-generator.jar

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.