Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
b1bf437
Rename "ExtensionConstraintConfig" to "ConstraintConfig"
sethmoo Mar 26, 2026
87f2a9a
Reformat test certs
suzannajiwani Apr 2, 2026
a1bf5ab
Make ConstraintConfig more generic by parameterizing the constrained …
sethmoo Apr 14, 2026
cd45eb4
Wrap CharacterCodingException.
carmenyh Apr 20, 2026
31fbda4
Ignore expiry checks for factory certificate chains
sethmoo Apr 20, 2026
f39ec0d
Add example authlist with all extensions parsed
suzannajiwani Apr 21, 2026
8ebb290
Add `deviceLocked` to `VerificationResult.Success`
May 5, 2026
0683504
Fix README code examples + Extension.kt error messages
suzannajiwani May 6, 2026
a83ff03
Add support for ML-DSA.
carmenyh May 13, 2026
fcf416d
Test Verifier against all testdata
suzannajiwani Jun 1, 2026
6a80b36
Update subject extended test cert
suzannajiwani Jun 5, 2026
d6209c0
Fix TrustAnchor validation.
suzannajiwani Jun 8, 2026
7f99055
Fix factory provisioned cert detection to catch "really old" certs
sethmoo Jun 11, 2026
4057431
Generate roots valid from long ago to far in the future.
carmenyh Jun 12, 2026
1666512
Add missing tags
sethmoo Jul 6, 2026
3d9b718
Add p256+sha384 test cert
suzannajiwani Jul 7, 2026
dac71f9
Add support to pass in hardcoded root/key.
carmenyh Jul 9, 2026
18abe6c
Log the signing algorithms of the certificate chain.
carmenyh Jul 13, 2026
27dcd9f
Add constraint enforcing SecurityLevel matching
sethmoo Jul 15, 2026
d9264f6
Add reference to revocation list to example.
carmenyh Jul 20, 2026
2751293
Support software root for testing.
carmenyh Jul 20, 2026
bf3ccbf
Support software root for testing.
sethmoo Jul 20, 2026
47f970d
Support software root for testing.
carmenyh Jul 21, 2026
4d1362e
Remove RequiresApi annotations as we no longer need them
sethmoo Aug 19, 2026
a69e6a6
Add workflow to build Maven repo
carmenyh Aug 19, 2026
2e3561b
Move function-local revocation status types up a level
sethmoo Aug 25, 2026
2b0f4f3
Add bounds checks to AttestationApplicationId
sethmoo Aug 26, 2026
2dd8708
Bump default maxPackages to 32
sethmoo Aug 27, 2026
a7d217a
Including a test cert from before Keymaster 4.0
suzannajiwani Aug 27, 2026
fd8c937
Add ProvisioningMethodConstraint
sethmoo Sep 4, 2026
a48898a
Make SecurityLevel.STRICT constraint even more strict
sethmoo Sep 4, 2026
c046460
Add release document to track release changes.
carmenyh Sep 10, 2026
2faf60e
Add more information about v0.1.
carmenyh Sep 10, 2026
100deda
Fix a bunch of misspellings and bad grammar.
carmenyh Sep 11, 2026
33cca48
Add publishing info to build.gradle.kts.
carmenyh Sep 15, 2026
b8a3b3d
Add DataItem.asLong() extension function and refactor asInt function.
alawngnome Sep 17, 2026
bc8737c
Remove areTagsOrdered from generated json
suzannajiwani Sep 18, 2026
2462d01
Log for malformed booleans
suzannajiwani Sep 23, 2026
d5b0001
Add AttestationApplicationId checking.
carmenyh Sep 24, 2026
0cb2c37
Add AttestationApplicationId checking.
Sep 24, 2026
20e2d11
Update dependencies for github workflows.
carmenyh Sep 24, 2026
411188a
Add AttestationApplicationId checking.
carmenyh Sep 25, 2026
a508aef
Additional malformed extension testing
suzannajiwani Sep 25, 2026
55c3504
Add standalone Gradle Kotlin consumer test for m2repo
carmenyh Sep 28, 2026
3f550f9
Add license to generated POM file.
carmenyh Oct 8, 2026
e1ca91a
Automated Code Change
cushon Oct 12, 2026
621149c
Make types in public method signatures api().
carmenyh Oct 13, 2026
4c25724
Merge upstream android/keyattestation v0.4 into sync.
pendula95 Nov 5, 2026
9bee3f9
Update org.bouncycastle:bcpkix-jdk18on to 1.85
pendula95 Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Gradle Build and Test

on:
Expand All @@ -10,12 +24,12 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: "temurin"
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Build
run: ./gradlew build
- name: Test
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
build-maven-repo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: "temurin"
java-version: 21
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: "Set environment variables"
run: |
echo "ORG_GRADLE_PROJECT_KeyAttestationMavenRepo=$GITHUB_WORKSPACE/keyattestation_m2repo" >> "$GITHUB_ENV"
- name: Build Maven repo
env:
REF_NAME: ${{ github.ref_name }}
run: ./gradlew -PkeyAttestationReleaseVersion="$REF_NAME" publishAllPublicationsToLocalDirRepository
- name: Test Maven repo
env:
REF_NAME: ${{ github.ref_name }}
run: ./gradlew -p testapp -PkeyAttestationReleaseVersion="$REF_NAME" test

- name: Upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: keyattestation_m2repo
path: keyattestation_m2repo
90 changes: 73 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ A Kotlin library for verifying Android key attestation certificate chains.

```kotlin
// Create a verifier with default, Google-rooted trust anchors, revocation
// info, and time source
// info, and time source.
val verifier = Verifier(
GoogleTrustAnchors, // Trust anchors source
{ setOf<String>() }, // Revoked serials source
{ Instant.now() } // Time source
GoogleTrustAnchors, // Trust anchors source
::getGoogleRevocationStatusFromWeb, // Revoked serials source
{ Instant.now() }, // Time source
ConstraintConfig(attestationApplicationId =
AttestationApplicationIdConstraint.ALLOW_UNKNOWN_PACKAGE(expectedAppId)),

)

// Verify an attestation certificate chain
val result = verifier.verify(certificateChain)
val result = verifier.verify(
certificateChain,
challengeChecker,
)

// Handle the verification result
when (result) {
Expand All @@ -29,21 +35,47 @@ when (result) {
is VerificationResult.PathValidationFailure -> // Handle validation failure
is VerificationResult.ChainParsingFailure -> // Handle parsing failure
is VerificationResult.ExtensionParsingFailure -> // Handle extension parsing issues
is VerificationResult.ExtensionConstraintViolation -> // Handle constraint violations
is VerificationResult.ConstraintViolation -> // Handle constraint violations
}
```

If there is additional verification you'd like to perform on the challenge
associated with the attestation certificate chain, pass in a `ChallengeChecker`
when verifying. For example, if you expect the challenge to be equal to
"challenge123", then usage would look like
### Choose a strong attestation challenge {#use-the-challenge}

Correctly generating your attestation challenges can:

* Prevent replay attacks (where attackers use an attestation more than once).
* Set a time-bound on a replay/relay (where attackers use an attestation from
one device on another device) attacks.

**Important:** A challenge alone, unless it is bound to the protocol in some
other way, cannot entirely prevent relay attacks.

#### Include information unique to the request in the challenge

See the
[PIA](https://developer.android.com/google/play/integrity/standard#protect-requests)
documentation for guidance on how to choose a strong challenge by including
information from the request in the challenge. Note that the equivalent of the
attestation challenge is `requestHash` in the PIA context.

#### Set time bounds for attestation validity

It’s important that the attestation not be valid for eternity. The longer an
attestation lives, the more likely it is to be used for a replay or relay
attack.

The easiest way to do this is to include a timestamp signed by your server-side
code in the challenge. When you verify the attestation's challenge, you'll check
the challenge signature and then make sure the timestamp is sufficiently fresh.

#### Example implementations

For example, if you expect the challenge to be equal to "challenge123", then
usage would look like

```kotlin
// Create a ChallengeChecker
val challengeChecker = ChallengeMatcher("challenge123")

// Verify an attestation certificate chain with the checker
val result = verifier.verify(certificateChain, challengeChecker)
val challengeChecker = ChallengeMatcher(ByteString.copyFromUtf8("challenge123"))
```

If there are multiple checks to perform on the challenge, use a
Expand All @@ -57,9 +89,14 @@ with an `InMemoryLruCache` like in this sample:
```kotlin
val cacheSize = 100

// Create a ChainedChallengeChecker with desired ChallengeCheckers
// Create a ChainedChallengeChecker with desired ChallengeCheckers. The
// coroutineScope is used to run each individual checker.
val challengeChecker =
ChainedChallengeChecker.of(ChallengeMatcher("expectedChallenge"), InMemoryLruCache(cacheSize))
ChainedChallengeChecker.of(
coroutineScope,
ChallengeMatcher(ByteString.copyFromUtf8("expectedChallenge")),
InMemoryLruCache(cacheSize),
)

// Verify an attestation certificate chain with the checker
val result = verifier.verify(certificateChain, challengeChecker)
Expand All @@ -71,6 +108,17 @@ against the `InMemoryLruCache` if the challenge doesn't match.
If the implementations in `challengecheckers/` don't fit your needs, simply
extend the `ChallengeChecker` interface.

### Getting the expected Attestation Application Id

It is important to check the attestation application ID when verifying a key
attestation. This assures that you don't accept attestations for keys controlled
by other applications, and can provide some assurance against relay attacks.

The package list should be the names of all applications you expect to verify
against and their minimum accepted version numbers. You can get the signature
digests to put in in `signatures` from the Play Console as the app certificate
digests.

## Building

```bash
Expand All @@ -95,7 +143,15 @@ Android Key Attestation root certificates are documented

## Getting Revoked Serials

The revoked serials may be retrieved from https://android.googleapis.com/attestation/status.
It's important to check the revoked serials list to prevent accepting
fraudulent attestations from known leaked keys. The revoked serials may be
retrieved from https://android.googleapis.com/attestation/status. This list is
updated relatively frequently so it's important to use a fresh copy of the
revocation list.

The above example will make a network call and parse the revoked serials list
every time `verify` is called. For regularly updated binaries, it may make sense
to build the revoked serial list into the binary.

See [here](https://developer.android.com/privacy-and-security/security-key-attestation#certificate_status)
for more information about the format of the data.
Expand Down
18 changes: 18 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Release Notes

## Pre v1 releases

### Before v0.4

Test releases, may have minor issues.

### v0.4

Test release.

It is a working version of the library at a given point in time, before any
BETA/1.0 release.

We plan to export more parameters and accept more checks but verification is
supported and the information for the minimum verification use case is
returned.
91 changes: 64 additions & 27 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
plugins {
id("com.adarshr.test-logger") version "4.0.0"
id("org.jetbrains.kotlin.jvm") version "2.2.0"
id("com.gradleup.shadow") version "9.3.2"
id("com.gradleup.shadow") version "9.3.2"
`java-library`
`maven-publish`
}

repositories {
Expand All @@ -29,14 +31,14 @@ dependencies {
implementation("androidx.annotation:annotation:1.9.1")
implementation("co.nstant.in:cbor:0.9")
implementation("com.google.code.gson:gson:2.11.0")
implementation("com.google.errorprone:error_prone_annotations:2.41.0")
implementation("com.google.protobuf:protobuf-javalite:4.28.3")
api("com.google.errorprone:error_prone_annotations:2.41.0")
api("com.google.protobuf:protobuf-javalite:4.28.3")
implementation("com.google.protobuf:protobuf-kotlin-lite:4.28.3")
implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
api("org.bouncycastle:bcpkix-jdk18on:1.85")
api("org.jetbrains.kotlin:kotlin-stdlib:2.2.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2")
implementation("com.google.guava:guava:33.5.0-jre")
api("com.google.guava:guava:33.5.0-jre")

testImplementation(kotlin("test"))
testImplementation("com.google.testparameterinjector:test-parameter-injector:1.18")
Expand All @@ -46,7 +48,10 @@ dependencies {
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
}

java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
withSourcesJar()
}

tasks {
test {
Expand All @@ -59,20 +64,19 @@ tasks {

val generatedSourcesDir = layout.buildDirectory.dir("generated")

val googleTrustAnchors by
tasks.registering {
val jsonFile = file("roots.json")
val json = jsonFile.readText()
val generatedFile = generatedSourcesDir.get().file("main/kotlin/GoogleTrustAnchors.kt")
val googleTrustAnchors by tasks.registering {
val jsonFile = file("roots.json")
val json = jsonFile.readText()
val generatedFile = generatedSourcesDir.get().file("main/kotlin/GoogleTrustAnchors.kt")

inputs.files(jsonFile)
outputs.file(generatedFile)
inputs.files(jsonFile)
outputs.file(generatedFile)

doLast {
generatedFile
.getAsFile()
.writeText(
"""
doLast {
generatedFile
.getAsFile()
.writeText(
"""
package com.android.keyattestation.verifier

import com.android.keyattestation.verifier.asX509Certificate
Expand All @@ -93,16 +97,49 @@ val googleTrustAnchors by
}
}
"""
)
}
)
}
}

val generateSources by
tasks.registering {
outputs.dir(generatedSourcesDir)
dependsOn(tasks.named("googleTrustAnchors"))
}
val generateSources by tasks.registering {
outputs.dir(generatedSourcesDir)
dependsOn(tasks.named("googleTrustAnchors"))
}

sourceSets { main { kotlin.srcDir(generateSources) } }

tasks.named("compileKotlin").configure { dependsOn("generateSources") }

publishing {
publications {
create<MavenPublication>("keyattestation") {
from(components["java"])
groupId = "com.android.keyattestation"
artifactId = "keyattestation"
version =
(findProperty("keyAttestationReleaseVersion") as? String)?.removePrefix("v")
?: "0.1-SNAPSHOT"
pom {
licenses {
license {
name.set("The Apache Software License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
}
}
}
repositories {
maven {
name = "localDir"
val repoDir =
findProperty("KeyAttestationMavenRepo") as? String
?: rootProject.layout.buildDirectory
.dir("keyattestation_m2repo")
.get()
.asFile
.absolutePath
url = uri(repoDir)
}
}
}
Loading
Loading