Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<permission
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ class AppApplication : Application() {
HtmlCompat.setContext(this)
installProvider(this)

// Initialize revocation list in background to fetch latest from network
executor.execute {
try {
io.github.vvb2060.keyattestation.attestation.RevocationList.refresh()
} catch (e: Exception) {
android.util.Log.w(TAG, "Failed to initialize revocation list", e)
}
}

if (Sui.init(BuildConfig.APPLICATION_ID)) {
KeyStoreManager.requestPermission();
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.vvb2060.keyattestation.attestation;

import android.os.Build;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -9,14 +10,19 @@
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Locale;

import io.github.vvb2060.keyattestation.AppApplication;
import io.github.vvb2060.keyattestation.R;

public record RevocationList(String status, String reason) {
private static final JSONObject data = getStatus();
private static final String TAG = "RevocationList";
private static JSONObject data = null;
private static Date publishTime = null;

private static String toString(InputStream input) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Expand All @@ -40,6 +46,42 @@ private static JSONObject parseStatus(InputStream inputStream) throws IOExceptio
}
}

private static JSONObject fetchFromNetwork(String statusUrl) {
HttpURLConnection connection = null;
try {
URL url = new URL(statusUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
connection.setRequestProperty("User-Agent", "KeyAttestation");

int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// Extract Last-Modified header for publish time
long lastModified = connection.getLastModified();
if (lastModified != 0) {
publishTime = new Date(lastModified);
Log.i(TAG, "Revocation list Last-Modified: " + publishTime);
}

try (var input = connection.getInputStream()) {
return parseStatus(input);
}
} else {
Log.w(TAG, "Failed to fetch revocation list from network, HTTP " + responseCode);
return null;
}
} catch (Exception e) {
Log.w(TAG, "Failed to fetch revocation list from network", e);
return null;
} finally {
if (connection != null) {
connection.disconnect();
}
}
}

private static JSONObject getStatus() {
var statusUrl = "https://android.googleapis.com/attestation/status";
var resName = "android:string/vendor_required_attestation_revocation_list_url";
Expand All @@ -49,18 +91,44 @@ private static JSONObject getStatus() {
if (id != 0) {
var url = res.getString(id);
if (!statusUrl.equals(url) && url.toLowerCase(Locale.ROOT).startsWith("https")) {
// no network permission, waiting for user report
throw new RuntimeException("unknown status url: " + url);
statusUrl = url;
}
}

// Try to fetch from network first
JSONObject networkData = fetchFromNetwork(statusUrl);
if (networkData != null) {
Log.i(TAG, "Successfully fetched revocation list from network");
return networkData;
}

// Fallback to local resource
Log.i(TAG, "Using local revocation list");
try (var input = res.openRawResource(R.raw.status)) {
return parseStatus(input);
} catch (IOException e) {
throw new RuntimeException("Failed to parse certificate revocation status", e);
}
}

public static Date getPublishTime() {
return publishTime;
}

public static void refresh() {
synchronized (RevocationList.class) {
data = getStatus();
}
}

public static RevocationList get(BigInteger serialNumber) {
if (data == null) {
synchronized (RevocationList.class) {
if (data == null) {
data = getStatus();
}
}
}
String serialNumberString = serialNumber.toString(16).toLowerCase();
JSONObject revocationStatus;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() {
addItem(CommonItemViewHolder.CERT_INFO_CREATOR, certInfo, id++)
}

// Add revocation list information
val publishTime = io.github.vvb2060.keyattestation.attestation.RevocationList.getPublishTime()
val dateStr = if (publishTime != null) {
io.github.vvb2060.keyattestation.attestation.AuthorizationList.formatDate(publishTime)
} else {
null
}
addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData(
R.string.revocation_list_publish_time,
R.string.revocation_list_description,
dateStr), ID_REVOCATION_INFO)

when (baseData) {
is AttestationData -> updateData(baseData)
is RemoteProvisioningData -> updateData(baseData)
Expand Down Expand Up @@ -271,6 +283,7 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() {
private const val ID_CERT_STATUS = 1L
private const val ID_BOOT_STATUS = 2L
private const val ID_CERT_INFO_START = 1000L
private const val ID_REVOCATION_INFO = 1900L
private const val ID_RKP_HOSTNAME = 2000L
private const val ID_DESCRIPTION_START = 3000L
private const val ID_AUTHORIZATION_LIST_START = 4000L
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
<string name="cert_error_expired">已过期:</string>
<string name="provisioning_info_certs_issued">过去 30 天证书颁发数量:</string>
<string name="provisioning_info_manufacturer">制造商:</string>
<string name="revocation_list_publish_time">吊销列表发布时间</string>
<string name="revocation_list_description">吊销列表用于检查证书是否被吊销。这里显示当前使用的吊销列表发布时间。</string>

<string name="error_message_subtitle">详细信息:</string>
<string name="error_unknown">未知错误</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
<string name="cert_error_expired">expired: </string>
<string name="provisioning_info_certs_issued">number of certs issued in last 30 days: </string>
<string name="provisioning_info_manufacturer">manufacturer: </string>
<string name="revocation_list_publish_time">Revocation list publish time</string>
<string name="revocation_list_description">The revocation list is used to check if certificates have been revoked. This shows the publication time of the currently used revocation list.</string>

<string name="error_message_subtitle">Detailed messages:</string>
<string name="error_unknown">Unknown error</string>
Expand Down