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) } ) }