feat(device): 设备状态查询增加 Challenge-Response 签名认证

- Android 端先获取挑战值,用 TEE 私钥签名后查询设备状态
- 服务端新增挑战值接口,校验时效、防重放并验签后才返回状态
This commit is contained in:
TongTongStudio
2026-08-23 14:01:20 +08:00
parent f0d4d4193f
commit 417c4989f7
7 changed files with 211 additions and 12 deletions

View File

@@ -12,6 +12,8 @@ import com.secure.demo.network.ApiClient;
import com.secure.demo.network.DeviceApi;
import com.secure.demo.network.model.ApiResponse;
import com.secure.demo.network.model.BindRequest;
import com.secure.demo.network.model.ChallengeResponse;
import com.secure.demo.network.model.DeviceStatusRequest;
import com.secure.demo.network.model.DeviceStatusResponse;
import com.secure.demo.network.model.DownloadDecryptResponse;
import com.secure.demo.network.model.EncryptedDek;
@@ -215,9 +217,25 @@ public class MainViewModel extends BaseViewModel {
*
* 后端接口 GET /api/device/status?sn=xxx设备端无登录
*/
/**
* 查询云端设备状态Challenge-Response 设备签名认证)。
*
* 流程:
* 1. GET /api/device/challenge 获取挑战值
* 2. 用 TEE 私钥对 challenge 签名
* 3. POST /api/device/status 带上 {sn, challenge, signature},验签通过后才返回状态
*/
private void queryDeviceStatus() {
Disposable d = ApiClient.deviceApi().deviceStatus(sn)
if (!requireCrypto()) return;
Disposable d = ApiClient.deviceApi().deviceStatusChallenge()
.map(ApiResponse::getData)
.flatMap(ch -> {
String challenge = ch.getChallenge();
// 用 TEE 私钥对挑战值签名(与上传照片同一签名算法)
String signature = crypto.signMetadata(challenge);
DeviceStatusRequest body = new DeviceStatusRequest(sn, challenge, signature);
return ApiClient.deviceApi().deviceStatus(body).map(ApiResponse::getData);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@@ -249,9 +267,9 @@ public class MainViewModel extends BaseViewModel {
ui.setBusy(false);
},
e -> {
// 查询失败(后端未启动):设备本地已就绪,回退到手动注册流程
// 查询失败(后端未启动 / 认证失败 / 网络异常):设备本地已就绪,回退到手动注册流程
Log.w(TAG, "查询云端状态失败,回退到手动注册: " + e.getMessage());
ui.appendLog("⚠️ 查询云端状态失败(后端未启动或网络异常),回退到手动流程");
ui.appendLog("⚠️ 查询云端状态失败(后端未启动 / 设备认证未通过),回退到手动流程");
ui.appendLog("设备就绪,可开始操作:先点击「注册设备」");
ui.setCurrentStep(1);
refreshStatus();

View File

@@ -2,7 +2,9 @@ package com.secure.demo.network;
import com.secure.demo.network.model.ApiResponse;
import com.secure.demo.network.model.BindRequest;
import com.secure.demo.network.model.ChallengeResponse;
import com.secure.demo.network.model.DownloadDecryptResponse;
import com.secure.demo.network.model.DeviceStatusRequest;
import com.secure.demo.network.model.DeviceStatusResponse;
import com.secure.demo.network.model.HealthResponse;
import com.secure.demo.network.model.MessageResponse;
@@ -34,9 +36,13 @@ import retrofit2.http.Query;
*/
public interface DeviceApi {
/** 0. 查询设备注册/绑定状态设备端无登录App 启动时复用已有状态 */
@GET("api/device/status")
Single<ApiResponse<DeviceStatusResponse>> deviceStatus(@Query("sn") String sn);
/** 0a. 获取设备状态查询挑战值Challenge-Response 第一步 */
@GET("api/device/challenge")
Single<ApiResponse<ChallengeResponse>> deviceStatusChallenge();
/** 0b. 查询设备注册/绑定状态(设备端无登录,需设备签名认证) */
@POST("api/device/status")
Single<ApiResponse<DeviceStatusResponse>> deviceStatus(@Body DeviceStatusRequest body);
/** 1. 设备注册SN + 公钥,设备端无登录) */
@POST("api/device/register")

View File

@@ -0,0 +1,21 @@
package com.secure.demo.network.model;
import com.google.gson.annotations.SerializedName;
/**
* 设备状态查询挑战值响应。
*
* 对应后端 GET /api/device/challenge 的 data 字段:
* { challenge }
*
* challenge 格式:<timestampMillis>:<nonce>,由设备端使用 TEE 私钥签名后,
* 回传给 POST /api/device/status 完成 Challenge-Response 认证。
*/
public class ChallengeResponse {
@SerializedName("challenge")
private String challenge;
public String getChallenge() { return challenge; }
public void setChallenge(String challenge) { this.challenge = challenge; }
}

View File

@@ -0,0 +1,34 @@
package com.secure.demo.network.model;
import com.google.gson.annotations.SerializedName;
/**
* 设备状态查询(带设备签名认证)请求体。
*
* 对应后端 POST /api/device/status 的 body
* { sn, challenge, signature }
*
* - challenge由 GET /api/device/challenge 下发,格式 <timestampMillis>:<nonce>
* - signature设备使用 TEE 私钥对 challenge 的签名Base64
*/
public class DeviceStatusRequest {
@SerializedName("sn")
private String sn;
@SerializedName("challenge")
private String challenge;
@SerializedName("signature")
private String signature;
public DeviceStatusRequest(String sn, String challenge, String signature) {
this.sn = sn;
this.challenge = challenge;
this.signature = signature;
}
public String getSn() { return sn; }
public String getChallenge() { return challenge; }
public String getSignature() { return signature; }
}