diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..48a3e3c
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id("com.android.application")
+ kotlin("android")
+}
+
+android {
+ namespace = "com.android.attestation.app" // UPDATED
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.android.attestation.app" // UPDATED
+ minSdk = 33 // MATCHING MANIFEST
+ targetSdk = 33 // MATCHING MANIFEST
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false // Consider enabling for production releases
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions { jvmTarget = "1.8" }
+}
+
+dependencies {
+ implementation(project(":")) // Depends on the root library project
+
+ implementation("androidx.appcompat:appcompat:1.7.0")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.activity:activity:1.9.0")
+
+ // Use the Android version of Guava
+ implementation("com.google.guava:guava:33.2.1-android")
+ implementation("com.google.protobuf:protobuf-javalite:4.28.3")
+
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7cbb3f5
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/android/attestation/app/AttestationUiManager.java b/app/src/main/java/com/android/attestation/app/AttestationUiManager.java
new file mode 100644
index 0000000..29d2be9
--- /dev/null
+++ b/app/src/main/java/com/android/attestation/app/AttestationUiManager.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2025 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.
+ */
+package com.android.attestation.app;
+
+import android.app.Activity;
+import android.content.res.ColorStateList;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
+import com.google.android.material.card.MaterialCardView;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.io.BaseEncoding;
+import com.google.protobuf.ByteString;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.Locale;
+
+/**
+ * Utility class for updating the UI of the Attestation Verifier app.
+ *
+ *
This class provides methods for updating the UI based on the verification result, such as
+ * updating the summary card, bootloader banner, and parameter details.
+ */
+public final class AttestationUiManager {
+ private static final String TAG = "AttestationUiManager";
+ private final Activity activity;
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ public static final int EIGHT_DIGIT_PATCH_LEVEL = 8;
+ public static final int SIX_DIGIT_PATCH_LEVEL = 6;
+
+ public AttestationUiManager(Activity activity) {
+ this.activity = activity;
+ }
+
+ /** Enum representing the different parameter names displayed in the UI. */
+ public enum AttestationParameter {
+ VERIFICATION_RESULT(R.string.label_verification_result),
+ ATTESTATION_CHALLENGE(R.string.label_attestation_challenge),
+ ATTESTATION_SECURITY_LEVEL(R.string.label_attestation_security_level),
+ KEYMASTER_VERSION(R.string.label_keymaster_version),
+ KEYMASTER_SECURITY_LEVEL(R.string.label_keymaster_security_level),
+ CREATION_DATE(R.string.label_creation_date),
+ APPLICATION_ID(R.string.label_id_device),
+ ATTESTATION_PURPOSES(R.string.label_attestation_purposes),
+ ATTESTATION_ALGORITHMS(R.string.label_attestation_algorithms),
+ KEY_SIZE(R.string.label_key_size),
+ DIGESTS(R.string.label_digests),
+ EC_CURVE(R.string.label_ec_curve),
+ ATTESTATION_ORIGIN(R.string.label_origin),
+ ROOT_OF_TRUST(R.string.label_root_of_trust),
+ ROT_STATE(R.string.label_rot_state),
+ ROT_LOCKED(R.string.label_rot_locked),
+ NO_AUTH_REQUIRED(R.string.label_no_auth_required),
+ ACTIVE_DATE(R.string.label_active_date),
+ ORIGINATION_EXPIRE_DATE(R.string.label_origination_expire_date),
+ USAGE_EXPIRE_DATE(R.string.label_usage_expire_date),
+ USER_AUTH_TYPE(R.string.label_user_auth_type),
+ AUTH_TIMEOUT(R.string.label_auth_timeout),
+ TRUSTED_USER_PRESENCE_REQUIRED(R.string.label_presence_required),
+ UNLOCKED_DEVICE_REQUIRED(R.string.label_unlocked_required),
+ ATTESTATION_OS_VERSION(R.string.label_os_version),
+ ATTESTATION_OS_PATCH(R.string.label_os_patch),
+ VENDOR_PATCH_LEVEL(R.string.label_vendor_patch),
+ BOOT_PATCH_LEVEL(R.string.label_boot_patch),
+ ATTESTATION_ID_BRAND(R.string.label_id_brand),
+ ATTESTATION_ID_MODEL(R.string.label_id_model),
+ ATTESTATION_ID_SERIAL(R.string.label_id_serial),
+ ATTESTATION_ID_IMEI(R.string.label_id_imei),
+ RSA_PUBLIC_EXPONENT(R.string.label_rsa_exponent),
+ ATTESTATION_MODULE_HASH(R.string.label_module_hash),
+ ERROR(R.string.label_error);
+
+ private final int labelResId;
+
+ AttestationParameter(@StringRes int labelResId) {
+ this.labelResId = labelResId;
+ }
+
+ @StringRes
+ public int getLabelResId() {
+ return labelResId;
+ }
+ }
+
+ /**
+ * Initializes the Activity's Action Bar with the application icon and title.
+ *
+ *
This method should be called during {@code Activity.onCreate}, immediately after {@code
+ * setContentView}. It is safe to call this method multiple times; subsequent calls will simply
+ * re-apply the icon and display configurations to the existing Action Bar.
+ */
+ public void setupActionBar() {
+ if (activity instanceof AppCompatActivity appCompatActivity) {
+ ActionBar actionBar = appCompatActivity.getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.app_name);
+ actionBar.setDisplayShowHomeEnabled(true);
+ actionBar.setIcon(activity.getApplicationInfo().icon);
+ }
+ }
+ }
+
+ public void updateSummaryStatus(boolean isVerified) {
+ handler.post(
+ () -> {
+ View summaryCard = activity.findViewById(R.id.summary_card);
+ if (summaryCard == null) {
+ return;
+ }
+
+ summaryCard.setVisibility(View.VISIBLE);
+ TextView summaryTitle = activity.findViewById(R.id.summary_title);
+ TextView summarySubtitle = activity.findViewById(R.id.summary_subtitle);
+
+ if (isVerified) {
+ summaryTitle.setText(R.string.summary_title_verified);
+ summaryTitle.setTextColor(ContextCompat.getColor(activity, R.color.brand_success));
+ summaryTitle.setCompoundDrawablesWithIntrinsicBounds(
+ 0, R.drawable.ic_check_circle, 0, 0);
+ summaryTitle.setCompoundDrawableTintList(
+ ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_success)));
+
+ summarySubtitle.setText(R.string.summary_subtitle_verified);
+ } else {
+ summaryTitle.setText(R.string.summary_title_unverified);
+ summaryTitle.setTextColor(ContextCompat.getColor(activity, R.color.brand_grey));
+ summaryTitle.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ic_warning, 0, 0);
+ summaryTitle.setCompoundDrawableTintList(
+ ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_grey)));
+
+ summarySubtitle.setText(R.string.summary_subtitle_unverified);
+ }
+ });
+ }
+
+ public String getParameterDescription(
+ AttestationParameter parameter, EnforcementType enforcement) {
+ String description = "";
+ String example = "";
+ switch (parameter) {
+ case VERIFICATION_RESULT -> {
+ description = activity.getString(R.string.desc_verification_result);
+ example = activity.getString(R.string.ex_verification_result);
+ }
+ case ATTESTATION_CHALLENGE -> {
+ description = activity.getString(R.string.desc_attestation_challenge);
+ example = activity.getString(R.string.ex_attestation_challenge);
+ }
+ case ATTESTATION_SECURITY_LEVEL -> {
+ description = activity.getString(R.string.desc_attestation_security_level);
+ example = activity.getString(R.string.ex_attestation_security_level);
+ }
+ case KEYMASTER_VERSION -> {
+ description = activity.getString(R.string.desc_keymaster_version);
+ example = activity.getString(R.string.ex_keymaster_version);
+ }
+ case KEYMASTER_SECURITY_LEVEL -> {
+ description = activity.getString(R.string.desc_keymaster_security_level);
+ example = activity.getString(R.string.ex_keymaster_security_level);
+ }
+ case CREATION_DATE -> {
+ description = activity.getString(R.string.desc_creation_date);
+ example = activity.getString(R.string.ex_creation_date);
+ }
+ case APPLICATION_ID -> {
+ description = activity.getString(R.string.desc_application_id);
+ example = activity.getString(R.string.ex_application_id);
+ }
+ case ATTESTATION_PURPOSES -> {
+ description = activity.getString(R.string.desc_attestation_purposes);
+ example = activity.getString(R.string.ex_attestation_purposes);
+ }
+ case ATTESTATION_ALGORITHMS -> {
+ description = activity.getString(R.string.desc_attestation_algorithms);
+ example = activity.getString(R.string.ex_attestation_algorithms);
+ }
+ case KEY_SIZE -> {
+ description = activity.getString(R.string.desc_key_size);
+ example = activity.getString(R.string.ex_key_size);
+ }
+ case DIGESTS -> {
+ description = activity.getString(R.string.desc_digests);
+ example = activity.getString(R.string.ex_digests);
+ }
+ case EC_CURVE -> {
+ description = activity.getString(R.string.desc_ec_curve);
+ example = activity.getString(R.string.ex_ec_curve);
+ }
+ case ATTESTATION_ORIGIN -> {
+ description = activity.getString(R.string.desc_attestation_origin);
+ example = activity.getString(R.string.ex_attestation_origin);
+ }
+ case ROOT_OF_TRUST -> {
+ description = activity.getString(R.string.desc_root_of_trust);
+ example = activity.getString(R.string.ex_root_of_trust);
+ }
+ case ROT_STATE -> {
+ description = activity.getString(R.string.desc_rot_state);
+ example = activity.getString(R.string.ex_rot_state);
+ }
+ case ROT_LOCKED -> {
+ description = activity.getString(R.string.desc_rot_locked);
+ example = activity.getString(R.string.ex_rot_locked);
+ }
+ case NO_AUTH_REQUIRED -> {
+ description = activity.getString(R.string.desc_no_auth_required);
+ example = activity.getString(R.string.ex_no_auth_required);
+ }
+ case ACTIVE_DATE -> description = activity.getString(R.string.desc_active_date);
+ case ORIGINATION_EXPIRE_DATE ->
+ description = activity.getString(R.string.desc_origination_expire_date);
+ case USAGE_EXPIRE_DATE -> description = activity.getString(R.string.desc_usage_expire_date);
+ case USER_AUTH_TYPE -> {
+ description = activity.getString(R.string.desc_user_auth_type);
+ example = activity.getString(R.string.ex_user_auth_type);
+ }
+ case AUTH_TIMEOUT -> description = activity.getString(R.string.desc_auth_timeout);
+ case TRUSTED_USER_PRESENCE_REQUIRED ->
+ description = activity.getString(R.string.desc_trusted_user_presence_required);
+ case UNLOCKED_DEVICE_REQUIRED ->
+ description = activity.getString(R.string.desc_unlocked_device_required);
+ case ATTESTATION_OS_VERSION -> {
+ description = activity.getString(R.string.desc_attestation_os_version);
+ example = activity.getString(R.string.ex_attestation_os_version);
+ }
+ case ATTESTATION_OS_PATCH -> {
+ description = activity.getString(R.string.desc_attestation_os_patch);
+ example = activity.getString(R.string.ex_attestation_os_patch);
+ }
+ case VENDOR_PATCH_LEVEL -> {
+ description = activity.getString(R.string.desc_vendor_patch_level);
+ example = activity.getString(R.string.ex_vendor_patch_level);
+ }
+ case BOOT_PATCH_LEVEL -> {
+ description = activity.getString(R.string.desc_boot_patch_level);
+ example = activity.getString(R.string.ex_boot_patch_level);
+ }
+ case ATTESTATION_ID_BRAND -> {
+ description = activity.getString(R.string.desc_attestation_id_brand);
+ example = activity.getString(R.string.ex_attestation_id_brand);
+ }
+ case ATTESTATION_ID_MODEL -> {
+ description = activity.getString(R.string.desc_attestation_id_model);
+ example = activity.getString(R.string.ex_attestation_id_model);
+ }
+ case ATTESTATION_ID_SERIAL ->
+ description = activity.getString(R.string.desc_attestation_id_serial);
+ case ATTESTATION_ID_IMEI ->
+ description = activity.getString(R.string.desc_attestation_id_imei);
+ case RSA_PUBLIC_EXPONENT -> {
+ description = activity.getString(R.string.desc_rsa_public_exponent);
+ example = activity.getString(R.string.ex_rsa_public_exponent);
+ }
+ case ATTESTATION_MODULE_HASH ->
+ description = activity.getString(R.string.desc_attestation_module_hash);
+ default -> description = activity.getString(R.string.desc_default);
+ }
+
+ StringBuilder popupText = new StringBuilder(description);
+
+ if (example != null && !example.isEmpty()) {
+ popupText
+ .append("\n\n")
+ .append(activity.getString(R.string.label_example_prefix))
+ .append(" ")
+ .append(example);
+ }
+
+ if (enforcement != null && enforcement != EnforcementType.NONE) {
+ String footer =
+ switch (enforcement) {
+ case TEE -> activity.getString(R.string.enforced_by_tee);
+ case SOFTWARE -> activity.getString(R.string.enforced_by_sw);
+ case HARDWARE -> activity.getString(R.string.enforced_by_hw);
+ default -> "";
+ };
+ popupText.append("\n\n").append(footer);
+ }
+
+ return popupText.toString();
+ }
+
+ public void showAboutDialog() {
+ new MaterialAlertDialogBuilder(activity)
+ .setTitle(R.string.about_key_attestation_title)
+ .setMessage(activity.getString(R.string.about_key_attestation_dialog_message))
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+
+ public void showParameterDetailDialog(String title, String message) {
+ new MaterialAlertDialogBuilder(activity)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+
+ public void updateBootloaderBanner(boolean isLocked) {
+ handler.post(
+ () -> {
+ MaterialCardView banner = activity.findViewById(R.id.bootloader_banner);
+ TextView statusText = activity.findViewById(R.id.bootloader_text);
+ ImageView icon = activity.findViewById(R.id.bootloader_icon);
+
+ if (banner == null) {
+ return;
+ }
+ banner.setVisibility(View.VISIBLE);
+
+ if (isLocked) {
+ statusText.setText(R.string.bootloader_locked);
+ banner.setCardBackgroundColor(
+ ContextCompat.getColor(activity, R.color.brand_blue_light));
+ statusText.setTextColor(ContextCompat.getColor(activity, R.color.brand_blue_dark));
+ icon.setImageResource(android.R.drawable.ic_lock_lock);
+ icon.setImageTintList(
+ ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_blue_dark)));
+ } else {
+ statusText.setText(R.string.bootloader_unlocked);
+ banner.setCardBackgroundColor(
+ ContextCompat.getColor(activity, R.color.brand_warning_light));
+ statusText.setTextColor(ContextCompat.getColor(activity, R.color.brand_brown_dark));
+ icon.setImageResource(android.R.drawable.ic_lock_idle_lock);
+ icon.setImageTintList(
+ ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.brand_brown_dark)));
+ }
+ });
+ }
+
+ public String formatDate(BigInteger timestamp) {
+ if (timestamp == null || timestamp.equals(BigInteger.ZERO)) {
+ return activity.getString(R.string.not_applicable);
+ }
+ try {
+ Instant instant = Instant.ofEpochMilli(timestamp.longValue());
+
+ DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("MMMM dd, yyyy hh:mm a").withZone(ZoneId.systemDefault());
+
+ return formatter.format(instant);
+ } catch (RuntimeException e) {
+ return String.valueOf(timestamp);
+ }
+ }
+
+ public String formatPatchLevel(Object value) {
+ if (value == null) {
+ return activity.getString(R.string.not_applicable);
+ }
+ String patchStr = String.valueOf(value);
+
+ try {
+ if (patchStr.length() == EIGHT_DIGIT_PATCH_LEVEL) {
+ LocalDate date = LocalDate.parse(patchStr, DateTimeFormatter.ofPattern("yyyyMMdd"));
+ return date.format(
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.getDefault()));
+
+ } else if (patchStr.length() == SIX_DIGIT_PATCH_LEVEL) {
+ int year = Integer.parseInt(patchStr.substring(0, 4));
+ int month = Integer.parseInt(patchStr.substring(4, 6));
+ YearMonth yearMonth = YearMonth.of(year, month);
+ return yearMonth.format(DateTimeFormatter.ofPattern("MMMM yyyy", Locale.getDefault()));
+ }
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Failed to parse patch level: " + patchStr);
+ }
+ return patchStr;
+ }
+
+ public String formatChallenge(ByteString challenge) {
+ if (challenge == null || challenge.isEmpty()) {
+ return activity.getString(R.string.not_applicable);
+ }
+
+ byte[] bytes = challenge.toByteArray();
+
+ if (isPrintableUtf8(bytes)) {
+ return String.format("\"%s\" (string representation)", challenge.toStringUtf8());
+ } else {
+ String hex = BaseEncoding.base16().lowerCase().encode(bytes);
+ return String.format("\"%s\" (hex representation)", hex);
+ }
+ }
+
+ private boolean isPrintableUtf8(byte[] bytes) {
+ try {
+ String str = new String(bytes, StandardCharsets.UTF_8);
+ return str.chars().allMatch(c -> c >= 32 && c < 127);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ public String formatAppId(Object appId) {
+ if (appId == null) {
+ return activity.getString(R.string.not_applicable);
+ }
+ return String.valueOf(appId).trim();
+ }
+
+ /**
+ * The enforcement type of the parameter, which is either TEE, SOFTWARE, HARDWARE, or NONE.
+ *
+ *
The enforcement type is used to determine the color of the parameter and the enforcement
+ * footer.
+ */
+ public enum EnforcementType {
+ TEE("TEE"),
+ SOFTWARE("SW"),
+ HARDWARE("HW"),
+ NONE("");
+
+ private final String label;
+
+ EnforcementType(String label) {
+ this.label = label;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/attestation/app/AttestationUtils.java b/app/src/main/java/com/android/attestation/app/AttestationUtils.java
new file mode 100644
index 0000000..e0ebeac
--- /dev/null
+++ b/app/src/main/java/com/android/attestation/app/AttestationUtils.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2025 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.
+ */
+package com.android.attestation.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.security.KeyStoreException;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+/**
+ * Utility class providing helper methods to verify Android device states, build configurations, and
+ * user profile environments.
+ */
+public final class AttestationUtils {
+
+ public static void showAlertDialog(
+ Activity activity, String title, String message, String buttonText) {
+ new MaterialAlertDialogBuilder(activity)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(buttonText, null)
+ .setCancelable(false)
+ .show();
+ }
+
+ /**
+ * Returns {@code true} if the network is likely available, or if its status cannot be determined.
+ *
+ *
This method fails open: it returns {@code false} only when it's definitively known that the
+ * network is unavailable. If there's insufficient information or an exception occurs during the
+ * check, it defaults to returning {@code true}.
+ */
+ public static boolean isNetworkAvailable(Context context) {
+ try {
+ ConnectivityManager cm =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ if (cm == null) {
+ return true;
+ }
+
+ Network activeNetwork = cm.getActiveNetwork();
+ if (activeNetwork == null) {
+ return false;
+ }
+
+ NetworkCapabilities capabilities = cm.getNetworkCapabilities(activeNetwork);
+ return capabilities == null
+ || capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+ } catch (RuntimeException e) {
+ return true;
+ }
+ }
+
+ public static boolean isCannotAttestIdsError(Exception e) {
+ int errorCode = getKeystoreErrorCode(e);
+
+ return errorCode == KeyStoreException.ERROR_ID_ATTESTATION_FAILURE;
+ }
+
+ public static boolean isNetworkError(Exception e) {
+ int errorCode = getKeystoreErrorCode(e);
+
+ return errorCode == KeyStoreException.RETRY_WHEN_CONNECTIVITY_AVAILABLE
+ || e instanceof UnknownHostException
+ || e instanceof SocketTimeoutException
+ || e instanceof ConnectException;
+ }
+
+ private static int getKeystoreErrorCode(Exception e) {
+ Throwable throwable = e instanceof KeyStoreException ? e : e.getCause();
+ if (throwable instanceof KeyStoreException kse) {
+ return kse.getNumericErrorCode();
+ }
+ return Integer.MAX_VALUE;
+ }
+
+ private AttestationUtils() {}
+}
diff --git a/app/src/main/java/com/android/attestation/app/MainActivity.java b/app/src/main/java/com/android/attestation/app/MainActivity.java
new file mode 100644
index 0000000..1e83ff2
--- /dev/null
+++ b/app/src/main/java/com/android/attestation/app/MainActivity.java
@@ -0,0 +1,619 @@
+/*
+ * Copyright 2025 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.
+ */
+package com.android.attestation.app;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.res.AssetFileDescriptor;
+import android.content.res.ColorStateList;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.core.content.ContextCompat;
+import com.android.keyattestation.verifier.AuthorizationList;
+import com.android.keyattestation.verifier.GoogleTrustAnchors;
+import com.android.keyattestation.verifier.KeyDescription;
+import com.android.keyattestation.verifier.ProvisioningInfoMap;
+import com.android.keyattestation.verifier.RootOfTrust;
+import com.android.keyattestation.verifier.VerificationResult;
+import com.android.keyattestation.verifier.Verifier;
+import com.android.keyattestation.verifier.X509CertificateExtKt;
+import com.android.keyattestation.verifier.challengecheckers.ChallengeMatcher;
+import com.android.keyattestation.verifier.provider.KeyAttestationCertPath;
+import com.google.android.libraries.security.content.SafeContentResolver;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.switchmaterial.SwitchMaterial;
+import com.google.common.io.BaseEncoding;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** Main activity for the Attestation Verifier app. */
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MainActivity";
+ private static final String KEY_ALIAS = "attestation_key";
+ private Button attestButton;
+ private Button exportButton;
+ private AttestationUiManager uiManager;
+
+ private LinearLayout attestationContainer;
+ private LinearLayout keymasterContainer;
+ private LinearLayout authListContainer;
+
+ private View attestationCard;
+ private View keymasterCard;
+ private View authListCard;
+
+ private boolean showAllDetails = false;
+ private boolean attestDeviceProps = true;
+ private boolean useStrongbox = false;
+
+ private ExecutorService executorService;
+ private Handler handler;
+ private List certChain;
+ private ActivityResultLauncher exportCertChainLauncher;
+
+ private int currentTaskId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ uiManager = new AttestationUiManager(this);
+ uiManager.setupActionBar();
+
+ attestButton = findViewById(R.id.attest_button);
+ exportButton = findViewById(R.id.export_button);
+
+ attestationContainer = findViewById(R.id.attestation_container);
+ keymasterContainer = findViewById(R.id.keymaster_container);
+ authListContainer = findViewById(R.id.auth_list_container);
+
+ attestationCard = findViewById(R.id.attestation_card);
+ keymasterCard = findViewById(R.id.keymaster_card);
+ authListCard = findViewById(R.id.auth_list_card);
+ SwitchMaterial idToggle = findViewById(R.id.toggle_id_attestation);
+ idToggle.setChecked(attestDeviceProps);
+
+ idToggle.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> {
+ attestDeviceProps = isChecked;
+ resetAndAttest();
+ });
+
+ executorService = Executors.newSingleThreadExecutor();
+ handler = new Handler(Looper.getMainLooper());
+
+ attestButton.setOnClickListener(unused -> resetAndAttest());
+
+ exportCertChainLauncher =
+ registerForActivityResult(
+ new ActivityResultContracts.CreateDocument("application/x-pem-file"),
+ this::exportCertChain);
+
+ exportButton.setOnClickListener(unused -> showExportInstructions());
+ }
+
+ private void resetAndAttest() {
+ currentTaskId++;
+ final int taskId = currentTaskId;
+
+ attestationContainer.removeAllViews();
+ keymasterContainer.removeAllViews();
+ authListContainer.removeAllViews();
+
+ attestationCard.setVisibility(View.GONE);
+ keymasterCard.setVisibility(View.GONE);
+ authListCard.setVisibility(View.GONE);
+
+ exportButton.setEnabled(false);
+ certChain = null;
+
+ View summaryCard = findViewById(R.id.summary_card);
+ if (summaryCard != null) {
+ summaryCard.setVisibility(View.GONE);
+ }
+
+ executorService.execute(
+ () -> {
+ if (!runPreChecks()) {
+ return;
+ }
+ try {
+ generateAndVerifyAttestation(taskId);
+ } catch (Exception e) {
+ Log.e(TAG, "Attestation failed", e);
+ if (taskId == currentTaskId) {
+ handler.post(
+ () ->
+ addAttestationRow(
+ attestationContainer,
+ AttestationUiManager.AttestationParameter.ERROR,
+ e.getMessage(),
+ null));
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.menu_show_all).setChecked(showAllDetails);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.menu_use_strongbox) {
+ item.setChecked(!item.isChecked());
+ this.useStrongbox = item.isChecked();
+ return true;
+ } else if (id == R.id.menu_show_all) {
+ item.setChecked(!item.isChecked());
+ showAllDetails = item.isChecked();
+ resetAndAttest();
+ return true;
+ } else if (id == R.id.menu_save_to_file) {
+ showExportInstructions();
+ return true;
+ } else if (id == R.id.menu_about) {
+ uiManager.showAboutDialog();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void exportCertChain(Uri uri) {
+ try (AssetFileDescriptor fd = SafeContentResolver.openAssetFileDescriptor(this, uri, "wt");
+ OutputStream out = fd.createOutputStream()) {
+ for (X509Certificate cert : certChain) {
+ out.write("-----BEGIN CERTIFICATE-----\n".getBytes(UTF_8));
+ out.write(
+ Base64.getEncoder()
+ .encodeToString(cert.getEncoded())
+ .replaceAll("(.{64})", "$1\n")
+ .getBytes(UTF_8));
+ out.write("\n-----END CERTIFICATE-----\n".getBytes(UTF_8));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Export failed", e);
+ }
+ }
+
+ private void addAttestationRow(
+ ViewGroup container,
+ AttestationUiManager.AttestationParameter parameter,
+ Object value,
+ AttestationUiManager.EnforcementType enforcement) {
+ String name = getString(parameter.getLabelResId());
+ if (value == null || String.valueOf(value).trim().isEmpty()) {
+ return;
+ }
+
+ handler.post(
+ () -> {
+ View itemView = getLayoutInflater().inflate(R.layout.attestation_item, container, false);
+ TextView titleView = itemView.findViewById(R.id.item_title);
+ TextView summaryView = itemView.findViewById(R.id.item_summary);
+ TextView enforcementView = itemView.findViewById(R.id.item_enforcement);
+
+ titleView.setText(name);
+ summaryView.setText(String.valueOf(value));
+ AttestationUiManager.EnforcementType type =
+ (enforcement != null) ? enforcement : AttestationUiManager.EnforcementType.NONE;
+ enforcementView.setText(type.getLabel());
+
+ int colorRes =
+ switch (type) {
+ case TEE, HARDWARE -> R.color.brand_success;
+ case SOFTWARE -> R.color.brand_warning;
+ case NONE -> 0;
+ };
+
+ if (colorRes != 0) {
+ enforcementView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_shield, 0);
+ enforcementView.setCompoundDrawableTintList(
+ ColorStateList.valueOf(ContextCompat.getColor(this, colorRes)));
+ } else {
+ enforcementView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+
+ itemView.setOnClickListener(
+ v ->
+ uiManager.showParameterDetailDialog(
+ name, uiManager.getParameterDescription(parameter, enforcement)));
+
+ container.addView(itemView);
+ if (container.getParent() instanceof View) {
+ ((View) container.getParent()).setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ private String mapPurposeToString(BigInteger p) {
+ return switch (p.intValue()) {
+ case 1 -> getString(R.string.purpose_sign);
+ case 2 -> getString(R.string.purpose_verify);
+ case 3 -> getString(R.string.purpose_encrypt);
+ case 4 -> getString(R.string.purpose_decrypt);
+ case 10 -> getString(R.string.purpose_attest);
+ default -> getString(R.string.purpose_unknown, p.toString());
+ };
+ }
+
+ private String mapAuthTypeToString(int val) {
+ return switch (val) {
+ case 1 -> getString(R.string.auth_type_password);
+ case 2 -> getString(R.string.auth_type_biometric);
+ default -> getString(R.string.auth_type_none);
+ };
+ }
+
+ private void showExportInstructions() {
+ handler.post(
+ () -> {
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.export_certificate_chain_title))
+ .setMessage(getString(R.string.export_certificate_chain_dialog_message))
+ .setPositiveButton(
+ "Continue",
+ (dialog, which) -> exportCertChainLauncher.launch("attestation_certs.pem"))
+ .setNegativeButton("Cancel", null)
+ .show();
+ });
+ }
+
+ private boolean runPreChecks() {
+
+ if (!AttestationUtils.isNetworkAvailable(this)) {
+ handler.post(
+ () ->
+ AttestationUtils.showAlertDialog(
+ this,
+ getString(R.string.network_error_alert_dialog_title),
+ getString(R.string.network_error_alert_dialog_msg),
+ getString(R.string.ok)));
+ return false;
+ }
+ return true;
+ }
+
+ private void handleAttestationError(Exception e) {
+ if (AttestationUtils.isCannotAttestIdsError(e)) {
+ handler.post(
+ () -> {
+ String message = getString(R.string.cannot_attest_ids_alert_dialog_msg);
+ if (attestDeviceProps) {
+ message += getString(R.string.cannot_attest_ids_tip);
+ }
+ AttestationUtils.showAlertDialog(
+ this,
+ getString(R.string.cannot_attest_ids_alert_dialog_title),
+ message,
+ getString(R.string.ok));
+ });
+ } else if (AttestationUtils.isNetworkError(e)) {
+ handler.post(
+ () ->
+ AttestationUtils.showAlertDialog(
+ this,
+ getString(R.string.network_error_alert_dialog_title),
+ getString(R.string.network_error_alert_dialog_msg),
+ getString(R.string.ok)));
+ } else {
+ handler.post(
+ () ->
+ AttestationUtils.showAlertDialog(
+ this,
+ getString(R.string.unexpected_error_alert_dialog_title),
+ getString(R.string.unexpected_error_alert_dialog_msg),
+ getString(R.string.ok)));
+ }
+ }
+
+ private void generateAndVerifyAttestation(int taskId)
+ throws GeneralSecurityException, IOException {
+
+ // Generate a new key pair in the Android Keystore.
+ KeyPairGenerator keyPairGenerator =
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
+ // The attestation challenge is a random nonce that is used to prevent replay attacks.
+ byte[] challenge = UUID.randomUUID().toString().getBytes(UTF_8);
+ KeyGenParameterSpec spec =
+ new KeyGenParameterSpec.Builder(
+ KEY_ALIAS, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
+ .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
+ .setDigests(KeyProperties.DIGEST_SHA256)
+ .setAttestationChallenge(challenge)
+ .setDevicePropertiesAttestationIncluded(attestDeviceProps)
+ .setIsStrongBoxBacked(useStrongbox)
+ .build();
+
+ try {
+ keyPairGenerator.initialize(spec);
+ keyPairGenerator.generateKeyPair();
+ } catch (Exception e) {
+ handleAttestationError(e);
+ return;
+ }
+
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ Certificate[] certs = keyStore.getCertificateChain(KEY_ALIAS);
+ this.certChain =
+ Arrays.stream(certs).map(c -> (X509Certificate) c).collect(Collectors.toList());
+
+ KeyAttestationCertPath certPath = new KeyAttestationCertPath(this.certChain);
+ KeyDescription ext = X509CertificateExtKt.keyDescription(certPath.leafCert());
+ var unused = ProvisioningInfoMap.parseFrom(certPath.attestationCert());
+
+ Verifier verifier =
+ new Verifier(GoogleTrustAnchors.INSTANCE, Collections::emptySet, Instant::now);
+ VerificationResult result = verifier.verify(certChain, new ChallengeMatcher(challenge));
+
+ handler.post(
+ () -> {
+ if (taskId != currentTaskId) {
+ return;
+ }
+ exportButton.setEnabled(true);
+
+ boolean isVerified = (result instanceof VerificationResult.Success);
+ uiManager.updateSummaryStatus(isVerified);
+ String resultName = isVerified ? "Success" : result.getClass().getSimpleName();
+
+ addAttestationRow(
+ attestationContainer,
+ AttestationUiManager.AttestationParameter.VERIFICATION_RESULT,
+ resultName,
+ isVerified
+ ? AttestationUiManager.EnforcementType.TEE
+ : AttestationUiManager.EnforcementType.SOFTWARE);
+ addAttestationRow(
+ attestationContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_CHALLENGE,
+ uiManager.formatChallenge(ext.getAttestationChallenge()),
+ AttestationUiManager.EnforcementType.NONE);
+ addAttestationRow(
+ attestationContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_SECURITY_LEVEL,
+ ext.getAttestationSecurityLevel().name(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ keymasterContainer,
+ AttestationUiManager.AttestationParameter.KEYMASTER_VERSION,
+ ext.getKeyMintVersion(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ keymasterContainer,
+ AttestationUiManager.AttestationParameter.KEYMASTER_SECURITY_LEVEL,
+ ext.getKeyMintSecurityLevel().name(),
+ AttestationUiManager.EnforcementType.TEE);
+ AuthorizationList sw = ext.getSoftwareEnforced();
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.CREATION_DATE,
+ uiManager.formatDate(sw.getCreationDateTime()),
+ AttestationUiManager.EnforcementType.SOFTWARE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.APPLICATION_ID,
+ uiManager.formatAppId(sw.getAttestationApplicationId()),
+ AttestationUiManager.EnforcementType.SOFTWARE);
+
+ AuthorizationList hw = ext.getHardwareEnforced();
+ if (hw.getPurposes() != null) {
+ String purposes =
+ hw.getPurposes().stream()
+ .map(this::mapPurposeToString)
+ .collect(Collectors.joining(", "));
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_PURPOSES,
+ purposes,
+ AttestationUiManager.EnforcementType.TEE);
+ }
+
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_ALGORITHMS,
+ hw.getAlgorithms(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_OS_VERSION,
+ hw.getOsVersion(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_OS_PATCH,
+ uiManager.formatPatchLevel(hw.getOsPatchLevel()),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_ORIGIN,
+ hw.getOrigin(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_ID_BRAND,
+ hw.getAttestationIdBrand(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.APPLICATION_ID,
+ hw.getAttestationIdDevice(),
+ AttestationUiManager.EnforcementType.TEE);
+
+ if (hw.getRootOfTrust() != null) {
+ RootOfTrust rot = hw.getRootOfTrust();
+ uiManager.updateBootloaderBanner(rot.getDeviceLocked());
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ROT_STATE,
+ rot.getVerifiedBootState().name(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ROT_LOCKED,
+ rot.getDeviceLocked(),
+ AttestationUiManager.EnforcementType.HARDWARE);
+ }
+
+ if (showAllDetails) {
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ACTIVE_DATE,
+ uiManager.formatDate(hw.getActiveDateTime()),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ORIGINATION_EXPIRE_DATE,
+ uiManager.formatDate(hw.getOriginationExpireDateTime()),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.USAGE_EXPIRE_DATE,
+ uiManager.formatDate(hw.getUsageExpireDateTime()),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.NO_AUTH_REQUIRED,
+ hw.getNoAuthRequired(),
+ AttestationUiManager.EnforcementType.TEE);
+ int authTypeVal = (hw.getUserAuthType() != null) ? hw.getUserAuthType().intValue() : 0;
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.USER_AUTH_TYPE,
+ mapAuthTypeToString(authTypeVal),
+ AttestationUiManager.EnforcementType.TEE);
+
+ if (hw.getAuthTimeout() != null) {
+ int timeoutSeconds = hw.getAuthTimeout().intValue();
+ String formattedTimeout =
+ getResources()
+ .getQuantityString(R.plurals.unit_seconds, timeoutSeconds, timeoutSeconds);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.AUTH_TIMEOUT,
+ formattedTimeout,
+ AttestationUiManager.EnforcementType.TEE);
+ }
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.TRUSTED_USER_PRESENCE_REQUIRED,
+ hw.getTrustedUserPresenceRequired(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.UNLOCKED_DEVICE_REQUIRED,
+ hw.getUnlockedDeviceRequired(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.VENDOR_PATCH_LEVEL,
+ uiManager.formatPatchLevel(hw.getVendorPatchLevel()),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.BOOT_PATCH_LEVEL,
+ uiManager.formatPatchLevel(hw.getBootPatchLevel()),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_ID_MODEL,
+ hw.getAttestationIdModel(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_ID_SERIAL,
+ hw.getAttestationIdSerial(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_ID_IMEI,
+ hw.getAttestationIdImei(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.KEY_SIZE,
+ hw.getKeySize(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.DIGESTS,
+ hw.getDigests(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.EC_CURVE,
+ hw.getEcCurve(),
+ AttestationUiManager.EnforcementType.TEE);
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.RSA_PUBLIC_EXPONENT,
+ hw.getRsaPublicExponent() != null ? hw.getRsaPublicExponent().toString() : "",
+ AttestationUiManager.EnforcementType.TEE);
+
+ if (hw.getModuleHash() != null && !hw.getModuleHash().isEmpty()) {
+ String hash =
+ BaseEncoding.base16().lowerCase().encode(hw.getModuleHash().toByteArray());
+ addAttestationRow(
+ authListContainer,
+ AttestationUiManager.AttestationParameter.ATTESTATION_MODULE_HASH,
+ hash,
+ AttestationUiManager.EnforcementType.TEE);
+ }
+ }
+ });
+ }
+}
diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml
new file mode 100644
index 0000000..0ef7e68
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_circle.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_verifier.xml b/app/src/main/res/drawable/ic_launcher_verifier.xml
new file mode 100644
index 0000000..e045a56
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_verifier.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_shield.xml b/app/src/main/res/drawable/ic_shield.xml
new file mode 100644
index 0000000..47bbacd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shield.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml
new file mode 100644
index 0000000..a9c1357
--- /dev/null
+++ b/app/src/main/res/drawable/ic_warning.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..1091eaa
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/attestation_item.xml b/app/src/main/res/layout/attestation_item.xml
new file mode 100644
index 0000000..e1a9ebf
--- /dev/null
+++ b/app/src/main/res/layout/attestation_item.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/menu/main_menu.xml b/app/src/main/res/layout/menu/main_menu.xml
new file mode 100644
index 0000000..62c3e64
--- /dev/null
+++ b/app/src/main/res/layout/menu/main_menu.xml
@@ -0,0 +1,26 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/summary_card.xml b/app/src/main/res/layout/summary_card.xml
new file mode 100644
index 0000000..a72491d
--- /dev/null
+++ b/app/src/main/res/layout/summary_card.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..9666ab1
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,26 @@
+
+ #D0BCFF
+ #381E72
+ #4F378B
+ #EADDFF
+ #CCC2DC
+ #332D41
+ #4A4458
+ #E8DEF8
+ #EFB8C8
+ #492532
+ #633B48
+ #FFD8E4
+ #F2B8B5
+ #601410
+ #8C1D18
+ #F9DEDC
+ #1C1B1F
+ #E6E1E5
+ #1C1B1F
+ #E6E1E5
+ #49454F
+ #CAC4D0
+ #938F99
+ #D0BCFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..b798494
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,37 @@
+
+ #4285F4
+ #D2E3FC
+ #6750A4
+ #80868B
+ #174EA6
+ #3E2723
+ #AECBFA
+ #FDE293
+ #EA4335
+ #FBBC05
+ #34A853
+
+ #6750A4
+ #FFFFFF
+ #EADDFF
+ #21005D
+ #625B71
+ #FFFFFF
+ #E8DEF8
+ #1D192B
+ #7D5260
+ #FFFFFF
+ #FFD8E4
+ #31111D
+ #B3261E
+ #FFFFFF
+ #F9DEDC
+ #410E0B
+ #FFFBFE
+ #1C1B1F
+ #FFFBFE
+ #1C1B1F
+ #E7E0EC
+ #49454F
+ #79747E
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2eadbd1
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,183 @@
+
+ Attestation Verifier
+ Attestation output will be shown here.
+ Attestation failed: %s
+ Attestation generated successfully.
+ Generate and verify attestation
+ Use StrongBox
+ Export certificate chain
+ OK
+ Attest Device IDs (IMEI/Serial)
+ Security enforcement level indicator
+ Verification status icon
+ Attestation
+ Keymaster
+ Authorization List
+ Attest device props
+ Show all
+ Save to file
+ About
+ Bootloader is locked
+ Bootloader is unlocked
+ Export Certificate Chain
+ An unexpected error occurred during attestation.
+ Save the PEM chain to your device. Retrieve it via:\n\nadb pull /sdcard/Download/attestation_certs.pem
+ About Key Attestation
+ Verifies Android Keystore integrity using official Trust Anchors.
+ Unexpected Error
+ Network Error
+ Cannot Attest IDs
+ "The attestation process requires an internet connection.\n\nSteps to fix:\n1. Check your Wi-Fi or Data connection.\n2. Ensure you are connected to the network.\n3. Try attesting again."
+ "The device was unable to attest to hardware identifiers (Device IDs).\n\nReason may include:\n Device not having provisioned identifiers or Device identifiers are not supported .\n"
+ \n\nTip: Key generation failed. If this device does not support ID attestation, try disabling the \'Attest Device IDs\' toggle on the main screen.
+ Device Secured
+ Hardware-backed attestation verified successfully.
+ Security Warning
+ Attestation failed or is software-enforced.
+ The outcome of validating the certificate chain against the official Root of Trust.
+ A \'Success\' indicates the chain is authentic and untampered.
+
+
+ A unique value provided to ensure the attestation is fresh and not a replay of a previous one.
+ It is typically represented as a Base64 string or a timestamp.
+
+ The security level of the environment that generated the attestation.
+ Levels include \'TRUSTED_ENVIRONMENT\' (TEE) or \'STRONG_BOX\'.
+
+ The version of the hardware-backed Keymaster or KeyMint implementation on the device.
+ For example, version 400 represents Keymaster 4.0.
+
+ Specifies where the keys themselves are stored and managed.
+ This is typically \'TRUSTED_ENVIRONMENT\' for TEE-backed keys.
+
+ The date and time when the key was created, specified in milliseconds since the epoch.
+ For example, January 1, 2021 is represented as 1609459200000.
+
+ Identifies the specific Android application and its signature that requested this key.
+ It contains the package name and the SHA-256 hash of the signing certificate.
+
+ The cryptographic operations that the hardware allows this key to perform.
+ Purposes include \'Sign\', \'Verify\', \'Encrypt\', or \'Decrypt\'.
+
+ The cryptographic algorithm used for the key pair.
+ Standard values include \'RSA\' (1) or \'EC\' (3).
+
+ The bit length of the generated key.
+ For example, 2048 for RSA or 256 for Elliptic Curve.
+
+ The hashing algorithms permitted for use with this key.
+ Commonly used digests include \'SHA-256\' or \'SHA-512\'.
+
+ The elliptic curve used for EC key generation.
+ For example, \'P-256\' (secp256r1) is represented as 1.
+
+ The source of the key material.
+ Levels include \'Generated\' (0) for keys created inside the hardware.
+
+ The foundational security state of the device, including bootloader and verified boot info.
+ It confirms if the device bootloader is \'Locked\' or \'Unlocked\'.
+
+ If true, the key can be used without requiring a user to authenticate (PIN/Biometric).
+ A \'true\' value means no biometric prompt is needed to use this key.
+
+ The date before which the key cannot be used.
+
+ The date after which the key can no longer be used for signing.
+
+ The date after which the key can no longer be used for verification.
+
+ The type of authentication required to access the key.
+ Common values include 1 (Password/PIN) or 2 (Fingerprint).
+
+ The duration in seconds that the user\'s authentication remains valid.
+
+ If true, the user must provide physical proof of presence (e.g., button press).
+
+ If true, the key can only be used when the device screen is currently unlocked.
+
+ The version of the Android OS currently running on the device.
+ For example, 14 represents Android 14.
+
+ The month and year associated with the security patch being used within the Keymaster.
+ For example, the August 2018 patch is represented as 201808.
+
+ The security patch level of the vendor-specific partition.
+ It follows the YYYYMMDD format to denote the specific driver update.
+
+ The security patch level of the bootloader image.
+ Represented as YYYYMMDD.
+
+ The device brand name verified by the hardware.
+ For example: \'samsung\'.
+
+ The device model name verified by the hardware.
+ For example: \'Pixel 8 Pro\'.
+ The hardware serial number of the device, if attestation is permitted.
+ The International Mobile Equipment Identity of the primary SIM slot.
+ The public exponent value used in the RSA algorithm math.
+ Usually 65537.
+ A hash representing the state of the security modules loaded in the OS.
+ A technical security parameter defined by the Android Keystore attestation schema.
+ The current state of the device\'s root of trust.
+ This indicates whether the device is \'Locked\' or \'Unlocked\'.
+ If true, the device bootloader is locked, which is required for strong attestation.
+ A \'true\' value means the bootloader is locked and the device is secured.
+ Enforced by TEE
+ Enforced by Software
+ Enforced by Hardware
+ Example:
+ N/A
+ Verification Result
+ Attestation Challenge
+ Attestation Security Level
+ Keymaster Version
+ Keymaster Security Level
+ Creation Date
+ Application ID
+ Attestation Purposes
+ Attestation Algorithms
+ Attestation OS Version
+ Attestation OS Patch
+ Attestation Origin
+ Attestation ID Brand
+ Attestation ID Device
+ Root of Trust (State)
+ Root of Trust
+ Root of Trust (Locked)
+ Active Date
+ Origination Expire Date
+ Usage Expire Date
+ No Auth Required
+ User Auth Type
+ Auth Timeout
+ Trusted User Presence Required
+ Unlocked Device Required
+ Vendor Patch Level
+ Boot Patch Level
+ Attestation ID Model
+ Attestation ID Serial
+ Attestation ID IMEI
+ Key Size
+ Digests
+ EC Curve
+ RSA Public Exponent
+ Attestation Module Hash
+ Sign
+ Verify
+ Encrypt
+ Decrypt
+ Attest
+ Unknown (%1$s)
+ Password/PIN
+ Biometric
+ None
+ Attestation Verified
+ The key and its properties were successfully verified.
+ Verification Failed
+ The certificate chain or challenge could not be verified.
+
+ - 1 second
+ - %1$d seconds
+
+ Error
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..c795395
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,29 @@
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8166462..85d3810 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,3 +17,7 @@
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" }
rootProject.name = "keyattestation"
+
+include(":app")
+
+project(":app").projectDir = file("app")