Skip to content

Commit

Permalink
feat: root certificate installer
Browse files Browse the repository at this point in the history
  • Loading branch information
suphon-t committed Jan 21, 2024
1 parent 8fa2678 commit b5af8ce
Show file tree
Hide file tree
Showing 10 changed files with 604 additions and 51 deletions.
4 changes: 4 additions & 0 deletions DotLocal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
D5803EE92B51A1A200332743 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EE82B51A1A200332743 /* Updater.swift */; };
D5803EEB2B5C53BF00332743 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EEA2B5C53BF00332743 /* ApiClient.swift */; };
D5803EEC2B5C53BF00332743 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EEA2B5C53BF00332743 /* ApiClient.swift */; };
D5803EEE2B5CD0A700332743 /* CertHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EED2B5CD0A700332743 /* CertHelper.swift */; };
D59D89502B4FFC380009270C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D894F2B4FFC380009270C /* main.swift */; };
D59D89612B5048C40009270C /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89602B5048C40009270C /* SharedConstants.swift */; };
D59D89622B5048C40009270C /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89602B5048C40009270C /* SharedConstants.swift */; };
Expand Down Expand Up @@ -192,6 +193,7 @@
D5803EE62B51A19500332743 /* Uninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uninstaller.swift; sourceTree = "<group>"; };
D5803EE82B51A1A200332743 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
D5803EEA2B5C53BF00332743 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = "<group>"; };
D5803EED2B5CD0A700332743 /* CertHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertHelper.swift; sourceTree = "<group>"; };
D582E9052B4C5BC00054343B /* nginx */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = nginx; sourceTree = "<group>"; };
D582E90A2B4E7BA90054343B /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
D582E90B2B4E7C750054343B /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -309,6 +311,7 @@
D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */,
D5DEA9BF2B499C2F0029BB00 /* Defaults.swift */,
D56116C52B517DC800FEB087 /* ViewExtensions.swift */,
D5803EED2B5CD0A700332743 /* CertHelper.swift */,
D529B1BE2B47BF8E00DC288B /* DotLocalTests */,
D529B1B12B47BF8E00DC288B /* Assets.xcassets */,
D529B1B62B47BF8E00DC288B /* DotLocal.entitlements */,
Expand Down Expand Up @@ -712,6 +715,7 @@
D5803EEB2B5C53BF00332743 /* ApiClient.swift in Sources */,
D59D89692B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */,
D5DEA9BE2B4995DD0029BB00 /* SettingsView.swift in Sources */,
D5803EEE2B5CD0A700332743 /* CertHelper.swift in Sources */,
D503780B2B48718D008F9AA8 /* MappingList.swift in Sources */,
D529B1AE2B47BF8C00DC288B /* DotLocalApp.swift in Sources */,
D56116BC2B51621500FEB087 /* dot-local.pb.swift in Sources */,
Expand Down
61 changes: 61 additions & 0 deletions DotLocal/CertHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// CertHelper.swift
// DotLocal
//
// Created by Suphon Thanakornpakapong on 21/1/2567 BE.
//

import Foundation
import SecurityInterface
import SwiftProtobuf

struct CertHelper {
static func getRootCertificate() async throws -> CertHelper.Certificate {
let res = try await DaemonManager.shared.apiClient.getRootCertificate(Google_Protobuf_Empty())
return try await CertHelper.Certificate(res: res)
}

static func rootCertificateLogo() -> NSImage {
let bundle = Bundle(for: SFCertificateView.self)
return bundle.image(forResource: "CertLargeRoot")!
}

struct Certificate {
let secCertificate: SecCertificate
let commonName: String
let notBefore: Date
let notAfter: Date
let trusted: Bool

init(res: GetRootCertificateResponse) async throws {
guard let certificate = SecCertificateCreateWithData(nil, res.certificate as CFData) else {
throw CertHelperError.invalidCertificate
}
secCertificate = certificate
let tmp = UnsafeMutablePointer<CFString?>.allocate(capacity: 1)
SecCertificateCopyCommonName(certificate, tmp)
commonName = tmp.pointee! as String
notBefore = res.notBefore.date
notAfter = res.notAfter.date
trusted = try await evaluateTrust(certificate: secCertificate)
}
}
}

enum CertHelperError: Error {
case invalidCertificate
}

fileprivate func evaluateTrust(certificate: SecCertificate) async throws -> Bool {
var secTrust: SecTrust?
if SecTrustCreateWithCertificates(certificate, SecPolicyCreateBasicX509(), &secTrust) == errSecSuccess, let trust = secTrust {
let error = UnsafeMutablePointer<CFError?>.allocate(capacity: 1)
let result = SecTrustEvaluateWithError(trust, error)
if error.pointee != nil {
return false
}
return result
} else {
return false
}
}
124 changes: 123 additions & 1 deletion DotLocal/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import SecurityInterface
import LaunchAtLogin
import Defaults
import Foundation
Expand Down Expand Up @@ -44,12 +45,16 @@ struct GeneralSettingsView: View {
CliView().padding(.top, 8)
}
.padding(20)
.frame(minWidth: 350, maxWidth: 350)
.frame(minWidth: 400, maxWidth: 400)
}
}

struct HttpsView: View {
@Binding var prefs: Preferences
@State var rootCertificate: CertHelper.Certificate?
@State private var window: NSWindow?

@Environment(\.controlActiveState) var controlActiveState

var body: some View {
LabeledContent(content: {
Expand All @@ -60,6 +65,109 @@ struct HttpsView: View {
}, label: {
Text("HTTPS:")
})
.background(WindowAccessor(window: $window))
.onAppear {
Task {
await loadCertificate()
}
}
.onChange(of: controlActiveState) { activeState in
Task {
if activeState == .key {
await loadCertificate()
}
}
}
if let rootCertificate = rootCertificate, let window = window {
CertificateView(certificate: rootCertificate, window: window, reload: {
Task {
await loadCertificate()
}
})
}
}

private func loadCertificate() async {
do {
rootCertificate = try await CertHelper.getRootCertificate()
} catch {
print("failed to load certificate: \(error)")
}
}
}

struct CertificateView: View {
@State private var didError = false
@State private var errorTitle = ""
@State private var errorMessage = ""

var certificate: CertHelper.Certificate
var window: NSWindow
var reload: () -> Void

var body: some View {
LabeledContent(content: {
VStack(alignment: .leading) {
Text(certificate.commonName)
Text("Expires: \(certificate.notAfter.formatted(date: .abbreviated, time: .shortened))")
.foregroundStyle(.secondary)
.font(.system(size: 12))
if certificate.trusted {
(Text(Image(systemName: "checkmark.seal.fill"))+Text(" Trusted"))
.foregroundStyle(.green)
.font(.system(size: 12))
} else {
(Text(Image(systemName: "xmark.circle.fill"))+Text(" Not trusted"))
.foregroundStyle(.red)
.font(.system(size: 12))
}
HStack {
Button(action: {
SFCertificatePanel.shared().beginSheet(for: window, modalDelegate: nil, didEnd: nil, contextInfo: nil, certificates: [certificate.secCertificate], showGroup: false)
}, label: {
Text("Details")
})
if !certificate.trusted {
Button(action: {
var status = SecItemAdd([
kSecClass as String: kSecClassCertificate,
kSecValueRef as String: certificate.secCertificate
] as CFDictionary, nil)
guard status == errSecSuccess || status == errSecDuplicateItem else {
errorTitle = "Failed to add certificate to Keychain"
errorMessage = "\(status): " + (SecCopyErrorMessageString(status, nil)! as String)
didError = true
return
}
status = SecTrustSettingsSetTrustSettings(certificate.secCertificate, .user, [
[kSecTrustSettingsPolicy: SecPolicyCreateBasicX509()],
[kSecTrustSettingsPolicy: SecPolicyCreateSSL(true, nil)],
] as CFTypeRef)
guard status == errSecSuccess || status == errAuthorizationCanceled else {
errorTitle = "Failed to set trust settings for certificate"
errorMessage = "\(status): " + (SecCopyErrorMessageString(status, nil)! as String)
didError = true
return
}
reload()
}, label: {
Text("Trust")
})
}
}
}
}, label: {
Text("Root Certificate:")
})
.alert(
errorTitle,
isPresented: $didError,
presenting: errorMessage
) { _ in
Button("OK") {}
} message: { message in
Text(message)
}
}
}

Expand Down Expand Up @@ -99,6 +207,20 @@ struct SettingsView: View {
}
}

struct WindowAccessor: NSViewRepresentable {
@Binding var window: NSWindow?

func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}

func updateNSView(_ nsView: NSView, context: Context) {}
}

prefix func ! (value: Binding<Bool>) -> Binding<Bool> {
Binding<Bool>(
get: { !value.wrappedValue },
Expand Down
Loading

0 comments on commit b5af8ce

Please sign in to comment.