Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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 @@ -34,12 +40,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";
Expand All @@ -49,18 +93,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