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) { @@ -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"; @@ -49,10 +91,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,7 +111,24 @@ private static JSONObject getStatus() { } } + 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 { 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