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.
91 lines
3.9 KiB
Swift
91 lines
3.9 KiB
Swift
import ClientTLS
|
|
import Foundation
|
|
import WireProtocol
|
|
|
|
/// B3 · Production wiring that connects the ClientTLS enrollment library to the
|
|
/// app's real transports. Kept in the assembly layer (not the leaf package) so
|
|
/// `ClientTLS` stays dependency-free and unit-testable; here we only adapt types
|
|
/// and read persisted config — no crypto, no keychain of our own.
|
|
|
|
/// Adapts the app's `HTTPTransport` (WireProtocol) to `ClientTLS`'s
|
|
/// `EnrollmentTransport`. The two protocols have an identical `send` shape; this
|
|
/// one-line bridge lets the SAME `URLSessionHTTPTransport` — which already
|
|
/// presents the device identity on a client-cert challenge (mTLS) — serve login,
|
|
/// enroll AND the silent renew, so renew authenticates with the current cert.
|
|
struct EnrollmentTransportAdapter: EnrollmentTransport {
|
|
private let http: any HTTPTransport
|
|
|
|
init(http: any HTTPTransport) {
|
|
self.http = http
|
|
}
|
|
|
|
func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
|
|
try await http.send(request)
|
|
}
|
|
}
|
|
|
|
/// B3 · Non-secret enrollment config persisted for the silent rotation scheduler
|
|
/// (the control-plane URL to renew against) and to prefill the re-enroll form.
|
|
/// Namespaced statics over `UserDefaults.standard` — nothing secret is stored
|
|
/// here (the bearer is never persisted; the private key stays in the Secure
|
|
/// Enclave; the leaf/chain live in the keychain via `KeychainClientIdentityStore`).
|
|
enum EnrollmentSettings {
|
|
private static let controlPlaneURLKey = "webterm.enroll.controlPlaneURL"
|
|
private static let subdomainKey = "webterm.enroll.subdomain"
|
|
|
|
static var controlPlaneURL: String? {
|
|
get { UserDefaults.standard.string(forKey: controlPlaneURLKey) }
|
|
set { UserDefaults.standard.set(newValue, forKey: controlPlaneURLKey) }
|
|
}
|
|
|
|
static var subdomain: String? {
|
|
get { UserDefaults.standard.string(forKey: subdomainKey) }
|
|
set { UserDefaults.standard.set(newValue, forKey: subdomainKey) }
|
|
}
|
|
}
|
|
|
|
enum DeviceEnrollmentWiringError: Error, Equatable, Sendable {
|
|
/// The control-plane URL is missing/invalid — cannot build a renew client.
|
|
case missingControlPlaneURL
|
|
/// The typed control-plane URL is not a valid absolute URL.
|
|
case invalidControlPlaneURL
|
|
}
|
|
|
|
/// Build the enrollment flow (login → SE keygen+CSR → enroll → store) against the
|
|
/// given control-plane URL, reusing the app's mTLS-capable transport.
|
|
func makeDeviceEnrollmentFlow(
|
|
controlPlaneURL: URL,
|
|
http: any HTTPTransport,
|
|
store: KeychainClientIdentityStore = KeychainClientIdentityStore()
|
|
) -> DeviceEnrollmentFlow {
|
|
DeviceEnrollmentFlow(
|
|
baseURL: controlPlaneURL,
|
|
transport: EnrollmentTransportAdapter(http: http),
|
|
store: store
|
|
)
|
|
}
|
|
|
|
/// Build the silent rotation scheduler. `renewalState` reads the installed leaf's
|
|
/// timing; `performRenew` re-CSRs from the SAME Secure-Enclave key and POSTs
|
|
/// `/device/:id/renew` over mTLS (nil bearer — the current cert authenticates).
|
|
/// The control-plane URL is read fresh from `EnrollmentSettings` at renew time.
|
|
func makeCertificateRotationScheduler(
|
|
http: any HTTPTransport,
|
|
store: KeychainClientIdentityStore = KeychainClientIdentityStore()
|
|
) -> CertificateRotationScheduler {
|
|
let transport = EnrollmentTransportAdapter(http: http)
|
|
return CertificateRotationScheduler(
|
|
renewalState: { try store.renewalState() },
|
|
performRenew: {
|
|
guard let urlString = EnrollmentSettings.controlPlaneURL,
|
|
let url = URL(string: urlString) else {
|
|
throw DeviceEnrollmentWiringError.missingControlPlaneURL
|
|
}
|
|
// nil bearer: /device/:id/renew authenticates by the presented
|
|
// client cert (mTLS), which the adapter's transport supplies.
|
|
let client = DeviceEnrollmentClient(baseURL: url, bearerToken: nil, transport: transport)
|
|
return try await store.renew(using: client)
|
|
}
|
|
)
|
|
}
|