feat(android): wire zero-touch device enrollment + fix renew/cache (B-track)
- wire the enroll flow into the app UI (EnrollmentScreen + ViewModel + Hilt DI +
host-menu "自动获取证书"), mirroring iOS — Android previously only had manual
.p12 import; the enroll library was built but unreachable.
- renew is now mTLS-only ({csr}-only body, no Authorization header) matching the
/device/:id/renew contract (the enroll bearer is minutes-lived → silent
rotation would have thrown weeks later).
- enroll refreshes the identity-repository cache so a mid-session-enrolled cert
is presented on the next mTLS handshake without a process restart.
gradle :app:assembleDebug + api-client/client-tls-android unit tests + koverVerify
green. On-device QA (keygen/enroll/present) is the operator's step.
This commit is contained in:
@@ -77,11 +77,16 @@ public class DeviceEnrollmentClient(
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew against the SAME hardware key: a fresh CSR to `/device/:id/renew` under the enroll
|
||||
* [bearerToken] (silent-rotation seam). Required fields validated client-side before any I/O.
|
||||
* Renew against the SAME hardware key: a fresh CSR to `/device/:id/renew` (silent-rotation seam).
|
||||
*
|
||||
* The renew endpoint is authenticated by the CURRENT device certificate over mTLS — the body is
|
||||
* `{ csr }` ONLY and NO bearer is sent (mirrors iOS, which passes `bearerToken: nil`). [bearerToken]
|
||||
* is therefore OPTIONAL and defaults to absent; the `Authorization` header is omitted when it is
|
||||
* null/blank. The seam still accepts a bearer for a hypothetical bearer-authenticated renew, but the
|
||||
* production caller passes none. [deviceId] and [csrDer] are validated client-side before any I/O.
|
||||
*/
|
||||
public suspend fun renew(bearerToken: String, deviceId: String, csrDer: ByteArray): EnrollmentResult {
|
||||
if (bearerToken.isEmpty() || deviceId.isEmpty() || csrDer.isEmpty()) {
|
||||
public suspend fun renew(deviceId: String, csrDer: ByteArray, bearerToken: String? = null): EnrollmentResult {
|
||||
if (deviceId.isEmpty() || csrDer.isEmpty()) {
|
||||
throw DeviceEnrollmentError.InvalidRequest
|
||||
}
|
||||
// Body is `{ csr }` ONLY — the renew endpoint authenticates by the presented mTLS device cert
|
||||
@@ -106,7 +111,9 @@ public class DeviceEnrollmentClient(
|
||||
): HttpRequest {
|
||||
val headers = LinkedHashMap<String, String>()
|
||||
headers[HEADER_CONTENT_TYPE] = CONTENT_TYPE_JSON
|
||||
if (bearer != null) headers[HEADER_AUTHORIZATION] = "$BEARER_PREFIX$bearer"
|
||||
// Omit Authorization entirely when there is no bearer (the mTLS-only renew path) — an empty
|
||||
// string must never emit a bare "Bearer " header.
|
||||
if (!bearer.isNullOrEmpty()) headers[HEADER_AUTHORIZATION] = "$BEARER_PREFIX$bearer"
|
||||
return HttpRequest(method = method, url = base + path, headers = headers, body = jsonBody)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user