From ada0d7fdef693f919780a3db3395783d8f3c0e98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:06:15 +0000 Subject: [PATCH 1/4] Initial plan From 84aa1257cf8cf2c3c85e1665df3fefd6f4d241dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:15:14 +0000 Subject: [PATCH 2/4] Add network support for revocation list and UI display for publish time Co-authored-by: vvb2060 <26996262+vvb2060@users.noreply.github.com> --- app/src/main/AndroidManifest.xml | 2 + .../attestation/RevocationList.java | 71 ++++++++++++++++++- .../keyattestation/home/HomeAdapter.kt | 13 ++++ app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 386349bc..f0a4acd2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + = Build.VERSION_CODES.TIRAMISU) { @@ -34,12 +44,50 @@ private static String toString(InputStream input) throws IOException { private static JSONObject parseStatus(InputStream inputStream) throws IOException { try { var statusListJson = new JSONObject(toString(inputStream)); + // Try to extract the publish time if it exists + try { + if (statusListJson.has("timestamp")) { + long timestamp = statusListJson.getLong("timestamp"); + publishTime = new Date(timestamp); + } + } catch (JSONException e) { + Log.w(TAG, "Failed to parse timestamp from revocation list", e); + } return statusListJson.getJSONObject("entries"); } catch (JSONException e) { throw new IOException(e); } } + 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) { + 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"; @@ -49,10 +97,19 @@ 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) { @@ -60,6 +117,14 @@ private static JSONObject getStatus() { } } + public static Date getPublishTime() { + return publishTime; + } + + public static void refresh() { + data = getStatus(); + } + public static RevocationList get(BigInteger serialNumber) { String serialNumberString = serialNumber.toString(16).toLowerCase(); JSONObject revocationStatus; diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt index e3aea98f..77f66acb 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt @@ -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) @@ -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 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 914a6dcb..4562d37b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -51,6 +51,8 @@ 已过期: 过去 30 天证书颁发数量: 制造商: + 吊销列表发布时间 + 吊销列表用于检查证书是否被吊销。这里显示当前使用的吊销列表发布时间。 详细信息: 未知错误 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5620044a..a0af7673 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,8 @@ expired: number of certs issued in last 30 days: manufacturer: + Revocation list publish time + The revocation list is used to check if certificates have been revoked. This shows the publication time of the currently used revocation list. Detailed messages: Unknown error From e92a2522e8e932fcd2e0aa5e482dcc6608cce1ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:18:52 +0000 Subject: [PATCH 3/4] Improve thread safety for revocation list network fetching Co-authored-by: vvb2060 <26996262+vvb2060@users.noreply.github.com> --- .../vvb2060/keyattestation/AppApplication.kt | 9 +++++++++ .../attestation/RevocationList.java | 19 ++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/AppApplication.kt b/app/src/main/java/io/github/vvb2060/keyattestation/AppApplication.kt index 1abdaf52..33926914 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/AppApplication.kt +++ b/app/src/main/java/io/github/vvb2060/keyattestation/AppApplication.kt @@ -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 { diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java index 43175012..155753b4 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java @@ -21,12 +21,8 @@ public record RevocationList(String status, String reason) { private static final String TAG = "RevocationList"; - private static JSONObject data; - private static Date publishTime; - - static { - data = getStatus(); - } + 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) { @@ -122,10 +118,19 @@ public static Date getPublishTime() { } public static void refresh() { - data = getStatus(); + 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 { From 97bbf02eaa578344a216c9b052b2c675f8609f7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:57:20 +0000 Subject: [PATCH 4/4] Use Last-Modified header for revocation list timestamp Co-authored-by: vvb2060 <26996262+vvb2060@users.noreply.github.com> --- .../attestation/RevocationList.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java index 155753b4..b30967cb 100644 --- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/RevocationList.java @@ -40,15 +40,6 @@ private static String toString(InputStream input) throws IOException { private static JSONObject parseStatus(InputStream inputStream) throws IOException { try { var statusListJson = new JSONObject(toString(inputStream)); - // Try to extract the publish time if it exists - try { - if (statusListJson.has("timestamp")) { - long timestamp = statusListJson.getLong("timestamp"); - publishTime = new Date(timestamp); - } - } catch (JSONException e) { - Log.w(TAG, "Failed to parse timestamp from revocation list", e); - } return statusListJson.getJSONObject("entries"); } catch (JSONException e) { throw new IOException(e); @@ -67,6 +58,13 @@ private static JSONObject fetchFromNetwork(String statusUrl) { 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); }