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:
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
/// Replace a keychain-backed item so a crash/failure mid-write can NEVER leave
|
||||
/// zero installed copies — which, for the device identity, would lock the device
|
||||
/// out of its own mTLS cert until a manual re-enroll. This is the inverse of a
|
||||
/// delete-then-add sequence, whose window between the two calls has zero items:
|
||||
///
|
||||
/// 1. `addNew` FIRST. Once it succeeds the replacement is installed, so at
|
||||
/// least one valid item exists from here on. If `addNew` throws, the prior
|
||||
/// item is left untouched (the device keeps a working identity) and the
|
||||
/// error propagates.
|
||||
/// 2. `deleteOld` SECOND, removing the prior copy. A failure here is NON-fatal:
|
||||
/// the new item is already installed, so the write is a success. The stale
|
||||
/// leftover is reported via `reportStaleDeleteFailure` (never a lockout) and
|
||||
/// swept on the next write — throwing here would strand a working new cert.
|
||||
///
|
||||
/// Kept pure (closures, no `Security` types) so the ordering invariant is unit-
|
||||
/// testable with fakes; the keychain plumbing lives at the call sites.
|
||||
func replaceKeychainItemAtomically(
|
||||
addNew: () throws -> Void,
|
||||
deleteOld: () throws -> Void,
|
||||
reportStaleDeleteFailure: (Error) -> Void = { _ in }
|
||||
) throws {
|
||||
try addNew()
|
||||
do {
|
||||
try deleteOld()
|
||||
} catch {
|
||||
reportStaleDeleteFailure(error)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user