feat(ios): device enrollment flow + silent cert rotation (B3)

Wire the SecureEnclave enroll library into a real flow (login->bearer->CSR->
/device/enroll->keychain identity), presented on the existing mTLS path; add a
rotation scheduler. Atomic keychain replace (add-before-delete); renew body is
{csr}-only; renewal-failing surfaced in the UI. ClientTLS 48 tests pass.
This commit is contained in:
Yaojia Wang
2026-07-18 13:32:05 +02:00
parent 9a5909f672
commit 07bcbf0c08
19 changed files with 1549 additions and 32 deletions

View File

@@ -29,6 +29,11 @@ struct SessionListScreen: View {
/// settings-like surface and is present in every empty state, so this is the
/// nav idiom that makes the orphaned screen reachable.
var onDeviceCert: () -> Void = {}
/// B3 · "" entry presents `EnrollmentScreen` (the zero-`.p12`
/// enrollment flow: login Secure-Enclave key + CSR device cert). Same
/// host-menu surface as `onDeviceCert`; that screen imports a `.p12`, this
/// one obtains a hardware-bound cert with no file at all.
var onEnroll: () -> Void = {}
/// T-iOS-28 (additive slot) · shared thumbnail pipeline: one cache + one
/// render-concurrency gate across ALL rows (scrolling must never spawn
/// unbounded offscreen terminals). `@State` keeps it stable across body
@@ -219,6 +224,12 @@ struct SessionListScreen: View {
} label: {
Label(ScreenCopy.addHost, systemImage: "plus")
}
Button {
onEnroll()
} label: {
Label(ScreenCopy.enroll, systemImage: "checkmark.shield")
}
.accessibilityIdentifier("sessions.enroll")
Button {
onDeviceCert()
} label: {
@@ -355,6 +366,7 @@ private enum ScreenCopy {
static let kill = "结束"
static let addHost = "配对新主机"
static let deviceCert = "设备证书"
static let enroll = "自动获取证书"
static let hostMenuFallback = "主机"
static let notPairedTitle = "还没有配对的主机"
static let notPairedHint = "先配对你电脑上的 web-terminal扫码或手输地址会话会出现在这里。"