diff --git a/SecureEnclaveToken.xcodeproj/project.pbxproj b/SecureEnclaveToken.xcodeproj/project.pbxproj index 8b5efef..bf13ac4 100644 --- a/SecureEnclaveToken.xcodeproj/project.pbxproj +++ b/SecureEnclaveToken.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -19,6 +19,9 @@ BD60D3BA25D58D9A0075CC34 /* SecureEnclaveTokenExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = BD60D3AB25D58D9A0075CC34 /* SecureEnclaveTokenExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; BD60D3C225D58E5A0075CC34 /* SecureEnclaveTokenUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD60D3C125D58E5A0075CC34 /* SecureEnclaveTokenUtils.swift */; }; BDE0445E25E0228C00C984E3 /* CertificateSigningRequest in Frameworks */ = {isa = PBXBuildFile; productRef = BDE0445D25E0228C00C984E3 /* CertificateSigningRequest */; }; + D98B035928DF8CFE00D6EB1C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98B035828DF8CFE00D6EB1C /* main.swift */; }; + D98B035C28DF955900D6EB1C /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = D98B035B28DF955900D6EB1C /* ArgumentParser */; }; + D98B035E28E0915200D6EB1C /* SecureEnclaveTokenCLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98B035D28E0915200D6EB1C /* SecureEnclaveTokenCLI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -53,6 +56,16 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + D99219A828DF7DAE0088BA56 /* Embed CLI */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 6; + files = ( + ); + name = "Embed CLI"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -72,6 +85,8 @@ BD60D3B625D58D9A0075CC34 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BD60D3B725D58D9A0075CC34 /* SecureEnclaveTokenExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SecureEnclaveTokenExtension.entitlements; sourceTree = ""; }; BD60D3C125D58E5A0075CC34 /* SecureEnclaveTokenUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveTokenUtils.swift; sourceTree = ""; }; + D98B035828DF8CFE00D6EB1C /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + D98B035D28E0915200D6EB1C /* SecureEnclaveTokenCLI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveTokenCLI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -79,6 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D98B035C28DF955900D6EB1C /* ArgumentParser in Frameworks */, BDE0445E25E0228C00C984E3 /* CertificateSigningRequest in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -118,11 +134,12 @@ children = ( BD60D39325D58D6F0075CC34 /* AppDelegate.swift */, BD60D39525D58D6F0075CC34 /* ContentView.swift */, + D98B035D28E0915200D6EB1C /* SecureEnclaveTokenCLI.swift */, BD60D3C125D58E5A0075CC34 /* SecureEnclaveTokenUtils.swift */, - BD60D39725D58D6F0075CC34 /* Assets.xcassets */, - BD60D39C25D58D6F0075CC34 /* Main.storyboard */, - BD60D39F25D58D6F0075CC34 /* Info.plist */, BD60D3A025D58D6F0075CC34 /* SecureEnclaveToken.entitlements */, + BD60D39F25D58D6F0075CC34 /* Info.plist */, + D98B035828DF8CFE00D6EB1C /* main.swift */, + BD60D39725D58D6F0075CC34 /* Assets.xcassets */, BD60D39925D58D6F0075CC34 /* Preview Content */, ); path = SecureEnclaveToken; @@ -131,6 +148,7 @@ BD60D39925D58D6F0075CC34 /* Preview Content */ = { isa = PBXGroup; children = ( + BD60D39C25D58D6F0075CC34 /* Main.storyboard */, BD60D39A25D58D6F0075CC34 /* Preview Assets.xcassets */, ); path = "Preview Content"; @@ -168,6 +186,7 @@ BD60D38E25D58D6F0075CC34 /* Resources */, BD60D3BE25D58D9A0075CC34 /* Embed App Extensions */, BD6D6EE625DD910E00C507B4 /* Embed Frameworks */, + D99219A828DF7DAE0088BA56 /* Embed CLI */, ); buildRules = ( ); @@ -177,6 +196,7 @@ name = SecureEnclaveToken; packageProductDependencies = ( BDE0445D25E0228C00C984E3 /* CertificateSigningRequest */, + D98B035B28DF955900D6EB1C /* ArgumentParser */, ); productName = SecureEnclaveToken; productReference = BD60D39025D58D6F0075CC34 /* SecureEnclaveToken.app */; @@ -205,7 +225,7 @@ BD60D38825D58D6F0075CC34 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1240; + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1320; TargetAttributes = { BD60D38F25D58D6F0075CC34 = { @@ -217,7 +237,7 @@ }; }; buildConfigurationList = BD60D38B25D58D6F0075CC34 /* Build configuration list for PBXProject "SecureEnclaveToken" */; - compatibilityVersion = "Xcode 9.3"; + compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -227,6 +247,7 @@ mainGroup = BD60D38725D58D6F0075CC34; packageReferences = ( BDE0445C25E0228C00C984E3 /* XCRemoteSwiftPackageReference "CertificateSigningRequest" */, + D98B035A28DF955900D6EB1C /* XCRemoteSwiftPackageReference "swift-argument-parser" */, ); productRefGroup = BD60D39125D58D6F0075CC34 /* Products */; projectDirPath = ""; @@ -263,8 +284,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D98B035E28E0915200D6EB1C /* SecureEnclaveTokenCLI.swift in Sources */, BD60D39625D58D6F0075CC34 /* ContentView.swift in Sources */, BD60D39425D58D6F0075CC34 /* AppDelegate.swift in Sources */, + D98B035928DF8CFE00D6EB1C /* main.swift in Sources */, BD60D3C225D58E5A0075CC34 /* SecureEnclaveTokenUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -435,11 +458,12 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = SecureEnclaveToken/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = com.mwielgoszewski.SecureEnclaveToken; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -465,11 +489,12 @@ "$(PROJECT_DIR)", ); INFOPLIST_FILE = SecureEnclaveToken/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 2.0; PRODUCT_BUNDLE_IDENTIFIER = com.mwielgoszewski.SecureEnclaveToken; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -563,6 +588,14 @@ kind = branch; }; }; + D98B035A28DF955900D6EB1C /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -571,6 +604,11 @@ package = BDE0445C25E0228C00C984E3 /* XCRemoteSwiftPackageReference "CertificateSigningRequest" */; productName = CertificateSigningRequest; }; + D98B035B28DF955900D6EB1C /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = D98B035A28DF955900D6EB1C /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = BD60D38825D58D6F0075CC34 /* Project object */; diff --git a/SecureEnclaveToken.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SecureEnclaveToken.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ce18929..12a5f22 100644 --- a/SecureEnclaveToken.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SecureEnclaveToken.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,16 +1,23 @@ { - "object": { - "pins": [ - { - "package": "CertificateSigningRequest", - "repositoryURL": "https://github.com/mwielgoszewski/CertificateSigningRequest", - "state": { - "branch": "feature/extension-requests", - "revision": "0ee3a1a90c525b65a3f0079c4436ae4ce17944b0", - "version": null - } + "pins" : [ + { + "identity" : "certificatesigningrequest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mwielgoszewski/CertificateSigningRequest", + "state" : { + "branch" : "feature/extension-requests", + "revision" : "f2cf3284bf2763772ea0e81f107df030ac7b9e23" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1", + "version" : "1.1.4" + } + } + ], + "version" : 2 } diff --git a/SecureEnclaveToken/AppDelegate.swift b/SecureEnclaveToken/AppDelegate.swift index 3929b27..4423247 100644 --- a/SecureEnclaveToken/AppDelegate.swift +++ b/SecureEnclaveToken/AppDelegate.swift @@ -7,20 +7,29 @@ import Cocoa import SwiftUI +import ArgumentParser -@main class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! func applicationDidFinishLaunching(_ aNotification: Notification) { + // arg.0 is the current executable + let args = Array(CommandLine.arguments.dropFirst()) + + // execute this as a cli app, then exit immediately + if args.first == "cli" { + SecureEnclaveTokenCLI.main(Array(args.dropFirst())) + exit(0) + } + // Create the SwiftUI view that provides the window contents. let contentView = ContentView() // Create the window and set the content view. window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), - styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + contentRect: NSRect(x: 0, y: 0, width: 480, height: 640), + styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView], backing: .buffered, defer: false) window.isReleasedWhenClosed = false window.center() diff --git a/SecureEnclaveToken/ContentView.swift b/SecureEnclaveToken/ContentView.swift index 2086591..f2534b4 100644 --- a/SecureEnclaveToken/ContentView.swift +++ b/SecureEnclaveToken/ContentView.swift @@ -11,8 +11,6 @@ import CryptoTokenKit import CertificateSigningRequest struct ContentView: View { - @State private var keysIsEmpty = false - @State private var loadButton = "Query Token" @State private var keysLoaded = 0 @State private var certificateLabel = "" @State private var keyLabel = "" @@ -28,112 +26,77 @@ struct ContentView: View { @State private var keyAccessibilityFlags = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly @State private var keyAccessControlFlags = 0 - let driverConfig = TKTokenDriver.Configuration.driverConfigurations["com.mwielgoszewski.SecureEnclaveToken.SecureEnclaveTokenExtension"] + @State private var genTag = "" + @State private var csrTag = "" + @State private var deleteTag = "" + @State private var exportTag = "" - // A unique, persistent identifier for this token. - // This value is typically generated from the serial number of the target hardware. - var tokenID: String { - let fallbackInstanceID = "819D11D7A8F7D609F236F529996E9F4C" - let platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice") ) + let queryTokenTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - guard platformExpert > 0 else { - return fallbackInstanceID - } - - guard let serialNumber = (IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String) else { - return fallbackInstanceID - } - - IOObjectRelease(platformExpert) - - let serialHash = SHA256.hash(data: serialNumber.data(using: .utf8)!).hexStr.dropLast(32) - return String(serialHash) - } - - var tokenConfig: TKToken.Configuration { + var tokenConfig: TKToken.Configuration? { loadTokenConfig() } - func loadTokenConfig() -> TKToken.Configuration { - if driverConfig!.tokenConfigurations.isEmpty { - driverConfig?.addTokenConfiguration(for: tokenID) - } - var tokenConfig = driverConfig!.tokenConfigurations[tokenID] - if tokenConfig == nil { - tokenConfig = driverConfig?.addTokenConfiguration(for: tokenID) + var keyTags: [String] { + var item: AnyObject? + + let query: [String: Any] = [kSecClass as String: kSecClassKey, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecReturnRef as String: true, + kSecReturnAttributes as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + ] + + let status = SecItemCopyMatching(query as CFDictionary, &item) + guard status == errSecSuccess else { + return [] } - return tokenConfig! - } - func unloadTokenConfig() { - driverConfig!.removeTokenConfiguration(for: tokenID) - } + var items: [String] = [] - func clearAllTokenConfigs() { - for tokenConfigurationID in driverConfig!.tokenConfigurations.keys { - print("Removing token configuration for \(tokenConfigurationID)") - driverConfig!.removeTokenConfiguration(for: tokenConfigurationID) + for attr in item as! [NSDictionary] { + let atag = attr[kSecAttrApplicationTag as String] as? Data + items.append(String(data: atag!, encoding: .utf8)!) } + return items } var body: some View { - let tag = "com.mwielgoszewski.SecureEnclaveToken.Key".data(using: .utf8)! - - VStack(alignment: .leading, spacing: 5) { - HStack { - Button(action: { - if tokenConfig.keychainItems.isEmpty { - let panel = NSOpenPanel() - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.allowedFileTypes = ["cer"] - if panel.runModal() == .OK { - let certificate = panel.url!.absoluteURL - _ = loadCertificateForTagIntoTokenConfig(certificatePath: certificate, tag: tag, tokenConfig: tokenConfig) - } - } else if self.loadButton == "Unload Token" { - tokenConfig.keychainItems.removeAll() - } - self.loadButton = tokenConfig.keychainItems.isEmpty ? "Load Token" : "Unload Token" - keysLoaded = tokenConfig.keychainItems.count - - if keysLoaded > 0 { - do { - let tkcert = try tokenConfig.certificate(for: tag) - let tkkey = try tokenConfig.key(for: tag) - certificateLabel = " • \(tkcert.label ?? "")" - keyLabel = " • \(tkkey.label ?? "")" - } catch { - } - } else { - certificateLabel = "" - keyLabel = "" - } - - }) { - Text(loadButton) + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .top) { + VStack { + Text("SecureEnclaveToken; see cli help for more options") } + } + HStack(alignment: .top) { VStack(alignment: .leading) { Text("\(keysLoaded) token keychain items loaded") Text(certificateLabel) Text(keyLabel) - } + }.onReceive(queryTokenTimer, perform: { _ in + keysLoaded = tokenConfig?.keychainItems.count ?? 0 + }) } HStack(alignment: .top) { - Button(action: { - var secKey = loadSecureEnclaveKey(tag: tag) - if secKey != nil { - generateKeyDescription = "Found key: \(secKey.debugDescription)" - } else { - secKey = generateKeyInEnclave(tag: tag, accessibility: keyAccessibilityFlags, accessControlFlags: keyAccessControlFlags) - generateKeyDescription = "Generated key: \(secKey.debugDescription)" + VStack(alignment: .leading) { + Button(action: { + let tag = genTag.data(using: .utf8)! + var secKey = loadSecureEnclaveKey(tag: tag) + if secKey != nil { + generateKeyDescription = "Found key: \(secKey.debugDescription)" + } else { + secKey = generateKeyInEnclaveFromUi(tag: tag, accessibility: keyAccessibilityFlags, accessControlFlags: keyAccessControlFlags) + generateKeyDescription = "Generated key: \(secKey.debugDescription)" + } + }) { + Text("Generate Key") } - }) { - Text("Generate Key") - } + }.padding(15) VStack(alignment: .leading) { + TextField("Tag", text: $genTag) + Picker(selection: $keyAccessibilityFlags, label: Text("Access:")) { Text("After first unlock").tag(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) Text("When unlocked").tag(kSecAttrAccessibleWhenUnlockedThisDeviceOnly) @@ -151,96 +114,126 @@ struct ContentView: View { } HStack { - Button(action: { - self.showDeleteConfirmation = true - }) { - Text("Delete Key") - } - .alert(isPresented: $showDeleteConfirmation) { - Alert(title: Text("Are you sure you want to delete this key?"), message: Text("Deleted keys cannot be recovered."), - primaryButton: .cancel(), - secondaryButton: .destructive(Text("Delete Key"), action: { + VStack(alignment: .leading) { + Button(action: { + self.showDeleteConfirmation = true + }) { + Text("Delete Key") + } + .alert(isPresented: $showDeleteConfirmation) { + Alert(title: Text("Are you sure you want to delete this key?"), message: Text("Deleted keys cannot be recovered."), + primaryButton: .cancel(), + secondaryButton: .destructive(Text("Delete Key"), action: { + let tag = deleteTag.data(using: .utf8)! if deleteSecureEnclaveKey(tag: tag) { print("Deleted key from enclave, unloading token configuration") unloadTokenConfig() } - }) - ) - } - } + }) + ) + } + }.padding(20) - HStack { - Button(action: { - let secKey = loadSecureEnclaveKey(tag: tag) - let publicKey = SecKeyCopyPublicKey(secKey!) - - var error: Unmanaged? - let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) - let bytes: Data = ixy! as Data - - // seq / seq / id-ecPublicKey / prime256v1 asn.1 - var der = Data(base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA=")! - // EC keys should be in ANSI X9.63, uncompressed byte string 04 || X || Y - der.append(bytes) - print(der.base64EncodedString()) - - let panel = NSSavePanel() - // default filename to .pub - panel.nameFieldStringValue = String(decoding: tag, as: UTF8.self) + ".pub" - if panel.runModal() == .OK { - let exportFilename = panel.url!.absoluteURL - do { - try der.write(to: exportFilename) - } catch { - print("Failed to write to \(exportFilename)") + VStack { + Picker(selection: $deleteTag, label: Text("")) { + ForEach(0 ..< keyTags.count, id: \.self) { index in + Text(self.keyTags[index]).tag(self.keyTags[index]) } } - }) { - Text("Export Public Key") } } - HStack(alignment: .top) { - Button(action: { - let secKey = loadSecureEnclaveKey(tag: tag) - if secKey != nil { + HStack { + VStack(alignment: .leading) { + Button(action: { + let tag = exportTag.data(using: .utf8)! + let secKey = loadSecureEnclaveKey(tag: tag) let publicKey = SecKeyCopyPublicKey(secKey!) var error: Unmanaged? let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) - let publicKeyBits: Data = ixy! as Data + let bytes: Data = ixy! as Data + + // seq / seq / id-ecPublicKey / prime256v1 asn.1 + var der = Data(base64Encoded: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgA=")! + // EC keys should be in ANSI X9.63, uncompressed byte string 04 || X || Y + der.append(bytes) + print(der.base64EncodedString()) - let savePanel = NSSavePanel() - // default filename to .req - savePanel.nameFieldStringValue = String(decoding: tag, as: UTF8.self) + ".req" - if savePanel.runModal() == .OK { - let exportFilename = savePanel.url! + let panel = NSSavePanel() + // default filename to .pub + panel.nameFieldStringValue = String(decoding: tag, as: UTF8.self) + ".pub" + if panel.runModal() == .OK { + let exportFilename = panel.url!.absoluteURL do { - let keyAlgorithm = KeyAlgorithm.ec(signatureType: .sha256) - let csr = CertificateSigningRequest.init( - commonName: commonName, - organizationName: organizationName, - organizationUnitName: organizationUnitName, - countryName: countryName, - stateOrProvinceName: stateOrProvinceName, - localityName: localityName, - emailAddress: emailAddress, - description: nil, - keyAlgorithm: keyAlgorithm) - - let pem = csr.buildCSRAndReturnString(publicKeyBits, privateKey: secKey!, publicKey: publicKey) - try pem?.data(using: .ascii)!.write(to: exportFilename) + try der.write(to: exportFilename) } catch { print("Failed to write to \(exportFilename)") } } + }) { + Text("Export Public Key") } + } - }) { - Text("Generate Signing Request") + VStack { + Picker(selection: $exportTag, label: Text("")) { + ForEach(0 ..< keyTags.count, id: \.self) { index in + Text(self.keyTags[index]).tag(self.keyTags[index]) + } + } } + } + + HStack(alignment: .top) { + VStack { + Button(action: { + let tag = csrTag.data(using: .utf8)! + let secKey = loadSecureEnclaveKey(tag: tag) + if secKey != nil { + let publicKey = SecKeyCopyPublicKey(secKey!) + + var error: Unmanaged? + let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) + let publicKeyBits: Data = ixy! as Data + + let savePanel = NSSavePanel() + // default filename to .req + savePanel.nameFieldStringValue = String(decoding: tag, as: UTF8.self) + ".req" + if savePanel.runModal() == .OK { + let exportFilename = savePanel.url! + do { + let keyAlgorithm = KeyAlgorithm.ec(signatureType: .sha256) + let csr = CertificateSigningRequest.init( + commonName: commonName, + organizationName: organizationName, + organizationUnitName: organizationUnitName.split(separator: ";").map(String.init), + countryName: countryName, + stateOrProvinceName: stateOrProvinceName, + localityName: localityName, + emailAddress: emailAddress.split(separator: ";").map(String.init), + description: nil, + keyAlgorithm: keyAlgorithm) + + let pem = csr.buildCSRAndReturnString(publicKeyBits, privateKey: secKey!, publicKey: publicKey) + try pem?.data(using: .ascii)!.write(to: exportFilename) + } catch { + print("Failed to write to \(exportFilename)") + } + } + } + + }) { + Text("Generate CSR") + } + }.padding(15) VStack(alignment: .leading) { + Picker(selection: $csrTag, label: Text("For key:")) { + ForEach(0 ..< keyTags.count, id: \.self) { index in + Text(self.keyTags[index]).tag(self.keyTags[index]) + } + } Text("Enter fields below:") TextField("Common Name", text: $commonName) TextField("Email Address (delimit using semicolon)", text: $emailAddress) @@ -263,12 +256,3 @@ struct ContentView_Previews: PreviewProvider { ContentView() } } - -extension Digest { - var bytes: [UInt8] { Array(makeIterator()) } - var data: Data { Data(bytes) } - - var hexStr: String { - bytes.map { String(format: "%02X", $0) }.joined() - } -} diff --git a/SecureEnclaveToken/Base.lproj/Main.storyboard b/SecureEnclaveToken/Preview Content/Base.lproj/Main.storyboard similarity index 100% rename from SecureEnclaveToken/Base.lproj/Main.storyboard rename to SecureEnclaveToken/Preview Content/Base.lproj/Main.storyboard diff --git a/SecureEnclaveToken/SecureEnclaveToken.entitlements b/SecureEnclaveToken/SecureEnclaveToken.entitlements index c38af1f..50b0c9b 100644 --- a/SecureEnclaveToken/SecureEnclaveToken.entitlements +++ b/SecureEnclaveToken/SecureEnclaveToken.entitlements @@ -4,6 +4,8 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + com.apple.security.files.user-selected.read-write keychain-access-groups diff --git a/SecureEnclaveToken/SecureEnclaveTokenCLI.swift b/SecureEnclaveToken/SecureEnclaveTokenCLI.swift new file mode 100644 index 0000000..bfcae26 --- /dev/null +++ b/SecureEnclaveToken/SecureEnclaveTokenCLI.swift @@ -0,0 +1,476 @@ +// +// SecureEnclaveTokenCLI.swift +// SecureEnclaveToken +// +// Created by Marcin Wielgoszewski on 9/25/22. +// + +import Foundation +import ArgumentParser +import CertificateSigningRequest +import CryptoTokenKit +import CryptoKit + +struct TokenConfiguration: Codable { + var keys: [Data] +} + +struct SecureEnclaveTokenCLI: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "SecureEnclaveToken cli", + abstract: "A utility for managing secure enclave keys and tokens.", + subcommands: [New.self, Destroy.self, Req.self, Pub.self, Keys.self, Token.self] + ) +} + +struct Options: ParsableArguments { + @Argument(help: "Private tag of the key.", transform: ({return $0.data(using: .utf8)!})) var key: Data +} + +extension SecureEnclaveTokenCLI { + struct New: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Generate a new secure enclave backed key." + ) + + enum Require: String, ExpressibleByArgument { + case none, userPresence, biometryAny, biometryCurrentSet, devicePasscode + } + + enum Availability: String, ExpressibleByArgument { + case whenUnlocked, afterFirstUnlock + } + + @OptionGroup var options: Options + @Option(help: "Possible values: none, userPresence, biometryAny, biometryCurrentSet, or devicePasscode") var require: Require = .none + @Option(help: "Possible values: whenUnlocked or afterFirstUnlock") var available: Availability = .afterFirstUnlock + + mutating func run() throws { + let keyExists = loadSecureEnclaveKey(tag: options.key) + + guard keyExists == nil else { + print("Key \(String(data: options.key, encoding: .utf8) ?? "") already exists, destroy it before generating a new one.") + throw ExitCode.failure + } + + var flags: SecAccessControlCreateFlags + + switch require { + case .userPresence: + flags = [SecAccessControlCreateFlags.privateKeyUsage, SecAccessControlCreateFlags.userPresence] + case .devicePasscode: + flags = [SecAccessControlCreateFlags.privateKeyUsage, SecAccessControlCreateFlags.devicePasscode] + case .biometryAny: + flags = [SecAccessControlCreateFlags.privateKeyUsage, SecAccessControlCreateFlags.biometryAny] + case .biometryCurrentSet: + flags = [SecAccessControlCreateFlags.privateKeyUsage, SecAccessControlCreateFlags.biometryCurrentSet] + case .none: + flags = [SecAccessControlCreateFlags.privateKeyUsage] + } + + var accessibility: CFString + switch available { + case .whenUnlocked: + accessibility = kSecAttrAccessibleWhenUnlockedThisDeviceOnly + case .afterFirstUnlock: + accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + } + + _ = generateKeyInEnclave(tag: options.key, accessibility: accessibility, flags: flags) + } + } + + struct Destroy: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Destroy a key in the secure enclave." + ) + + @OptionGroup var options: Options + + mutating func run() throws { + guard let tokenConfig = loadTokenConfig() else { + print("Could not load token configuration.") + throw ExitCode.failure + } + guard loadSecureEnclaveKey(tag: options.key) != nil else { + print("Key \(String(data: options.key, encoding: .utf8) ?? "") not found") + throw ExitCode.failure + } + guard deleteSecureEnclaveKey(tag: options.key) else { + print("Failed to delete key.") + throw ExitCode.failure + } + var config = try? JSONDecoder().decode(TokenConfiguration.self, from: tokenConfig.configurationData ?? Data()) + config?.keys.removeAll(where: {$0 == options.key}) + tokenConfig.configurationData = try? JSONEncoder().encode(config) + print("Successfully deleted key.") + } + } + + struct Req: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Generate a certificate signing request for a given key." + ) + + @OptionGroup var options: Options + @Option(name: [.customLong("cn", withSingleDash: true)], help: "Common Name (eg, fully qualified host name)") var commonName: String? + @Option(name: [.customShort("o")], help: "Organization Name (eg, company)") var organizationName: String? + @Option(name: [.customLong("ou", withSingleDash: true)], help: "Organizational Unit Name (eg, section)") var organizationUnitName: String? + @Option(name: [.customShort("c")], help: "Country Name (2 letter code)") var countryName: String? + @Option(name: [.customLong("st", withSingleDash: true)], help: "State or Province Name (full name)") var stateOrProvinceName: String? + @Option(name: [.customShort("l")], help: "Locality Name (eg, city)") var localityName: String? + @Option(name: [.customLong("email", withSingleDash: true)], help: "Email Address") var emailAddress: String? + @Option(name: [.customShort("d")], help: "Description") var description: String? + @Flag(name: [.customLong("include-serial", withSingleDash: true)], help: "Include device serial number (default: false)") var includeSerialNumber = false + + mutating func run() throws { + guard let secKey = loadSecureEnclaveKey(tag: options.key) else { + print("Key \(String(data: options.key, encoding: .utf8) ?? "") not found") + throw ExitCode.failure + } + + let publicKey = SecKeyCopyPublicKey(secKey) + + var error: Unmanaged? + let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) + let publicKeyBits: Data = ixy! as Data + + let keyAlgorithm = KeyAlgorithm.ec(signatureType: .sha256) + let csr = CertificateSigningRequest.init(commonName: commonName, + organizationName: organizationName, + organizationUnitName: organizationUnitName?.split(separator: ";", omittingEmptySubsequences: true).map(String.init), + countryName: countryName, + stateOrProvinceName: stateOrProvinceName, + localityName: localityName, + serialNumber: includeSerialNumber ? serialNumber : nil, + emailAddress: emailAddress?.split(separator: ";", omittingEmptySubsequences: true).map(String.init), + description: description, + keyAlgorithm: keyAlgorithm) + + csr.addKeyUsage([KeyUsage.digitalSignature, KeyUsage.keyAgreement]) + csr.addExtendedKeyUsage(ExtendedKeyUsage.clientAuth) + + if let emailAddress = emailAddress { + for email in emailAddress.split(separator: ";", omittingEmptySubsequences: true).map(String.init) { + csr.addSubjectAlternativeName(SubjectAlternativeName.rfc822Name(String(email))) + } + } + + guard let pem = csr.buildCSRAndReturnString(publicKeyBits, privateKey: secKey, publicKey: publicKey) else { + print("Could not generate certificate signing request") + throw ExitCode.failure + } + print(pem) + } + } + + struct Pub: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Get the public key for a secure enclave backed key." + ) + + @OptionGroup var options: Options + + mutating func run() throws { + guard let secKey = loadSecureEnclaveKey(tag: options.key) else { + print("Key \(String(data: options.key, encoding: .utf8) ?? "") not found") + throw ExitCode.failure + } + + let publicKey = SecKeyCopyPublicKey(secKey) + + var error: Unmanaged? + let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) + let bytes: Data = ixy! as Data + print(bytes.base64EncodedString()) + } + } + + struct Keys: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "List known private keys stored in the secure enclave." + ) + + mutating func run() throws { + var item: AnyObject? + + let query: [String: Any] = [kSecClass as String: kSecClassKey, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecReturnRef as String: true, + kSecReturnAttributes as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + ] + + let status = SecItemCopyMatching(query as CFDictionary, &item) + guard status == errSecSuccess else { + print("Error querying secure enclave, \(status)") + throw ExitCode.failure + } + + // https://opensource.apple.com/source/Security/Security-59754.80.3/OSX/sec/Security/SecItemConstants.c.auto.html + + print("Public Key Hash Created Tag") + for attr in item as! [NSDictionary] { + let atag = attr[kSecAttrApplicationTag as String] as? Data + let cdat = attr[kSecAttrCreationDate as String] as? Date + let klbl = attr[kSecAttrApplicationLabel as String] as? Data + print(String(format: "%@ %@ %@", klbl!.hexEncodedString(), cdat!.description, String(data: atag!, encoding: .utf8)!)) + } + } + } + + struct Token: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Manage secure enclave token configuration.", + subcommands: [Info.self, List.self, Import.self, Remove.self, Clear.self], + defaultSubcommand: Info.self + ) + } + +} + +extension SecureEnclaveTokenCLI.Token { + struct Info: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Display information about this token configuration." + ) + + mutating func run() throws { + guard let tokenConfig = loadTokenConfig() else { + print("Could not load token configuration.") + throw ExitCode.failure + } + + print("Token instance ID: \(tokenConfig.instanceID)") + print("\(tokenConfig.keychainItems.count) items loaded.") + } + } + + struct List: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "List keys and certificates in the token configuration." + ) + + mutating func run() throws { + guard let tokenConfig = loadTokenConfig() else { + print("Could not load token configuration.") + throw ExitCode.failure + } + + let items = Dictionary(grouping: tokenConfig.keychainItems, by: { + (element: TKTokenKeychainItem) in return element.objectID as! Data }) + + for objectID in items.keys { + let certificate = try? tokenConfig.certificate(for: objectID) + let key = try? tokenConfig.key(for: objectID) + var usage: [String] = [] + if let key = key { + if key.canSign { + usage.append("sign") + } + if key.canDecrypt { + usage.append("decrypt") + } + if key.canPerformKeyExchange { + usage.append("derive") + } + + var keyType: String + switch key.keyType as CFString { + case kSecAttrKeyTypeRSA: + keyType = "RSA" + case kSecAttrKeyTypeECSECPrimeRandom: + keyType = "ECC" + default: + keyType = "ECC" + } + + print(""" +Private Key Object; \(keyType) + label: \(key.label ?? "") + ID: \(String(data: objectID, encoding: .utf8) ?? "") + Usage: \(usage.joined(separator: ", ")) +Public Key Object; \(keyType) \(key.keySizeInBits) bits + label: \(key.label ?? "") + ID: \(String(data: objectID, encoding: .utf8) ?? "") + keyHash: \(key.publicKeyHash?.map { String(format: "%02hhx", $0) }.joined() ?? "") + keyData: \(key.publicKeyData?.base64EncodedString() ?? "") +""") + } + + if let certificate = certificate { + let cert = SecCertificateCreateWithData(nil, certificate.data as CFData)! + let subject = SecCertificateCopySubjectSummary(cert) as? String + print(""" +Certificate Object; type = X.509 cert + label: \(certificate.label ?? "") + subject: \(subject ?? "") + ID: \(String(data: objectID, encoding: .utf8) ?? "") +""") + } + } + + } + } + + struct Import: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Map a certificate to a key in the token configuration." + ) + + @OptionGroup var options: Options + @Argument(help: "The DER-encoded certificate", transform:({return URL(fileURLWithPath: $0)})) var cert: URL + + mutating func run() throws { + guard let tokenConfig = loadTokenConfig() else { + print("Could not load token configuration.") + throw ExitCode.failure + } + + do { + try tokenConfig.certificate(for: options.key) + print("A token is already loaded for this key, remove it before importing a new certificate.") + throw ExitCode.failure + } catch is TKError { + // carry on + } + + var derBytes: Data + do { + derBytes = try Data(contentsOf: cert) + } catch { + print("Error reading file, exiting.") + throw ExitCode.failure + } + + guard let certificate = SecCertificateCreateWithData(nil, derBytes as CFData) else { + print("Error parsing certificate, exiting.") + throw ExitCode.failure + } + + do { + try addCertificateToToken(certificate: certificate, tag: options.key, tokenConfig: tokenConfig) + print("Successfully loaded certificate into token \(String(data: options.key, encoding: .utf8) ?? "")") + } catch CertificateImportError.keysMismatch { + print("Error laoding certificate into token, public key does not match.") + throw ExitCode.failure + } catch CertificateImportError.keyDoesNotExist { + print("Error loading certificate into token, \(String(data: options.key, encoding: .utf8) ?? "") does not exist") + throw ExitCode.failure + } catch { + print("Error loading certificate into token, exiting.") + throw ExitCode.failure + } + + return + } + } + + struct Remove: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Remove a mapped certificate and key from the token configuration." + ) + + @OptionGroup var options: Options + + mutating func run() throws { + guard let tokenConfig = loadTokenConfig() else { + print("Could not load token configuration.") + throw ExitCode.failure + } + + let foundItems = tokenConfig.keychainItems.filter({ $0.objectID as! Data == options.key }) + + guard !foundItems.isEmpty else { + print("Token not found with name \(String(data: options.key, encoding: .utf8) ?? "")") + throw ExitCode.failure + } + + tokenConfig.keychainItems = tokenConfig.keychainItems.filter({ $0.objectID as! Data != options.key }) + } + } + + struct Clear: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Clear keys and certificates from token configuration." + ) + + mutating func run() throws { + guard let tokenConfig = loadTokenConfig() else { + print("Could not load token configuration.") + throw ExitCode.failure + } + + guard !tokenConfig.keychainItems.isEmpty else { + print("Token configuration is already empty.") + throw ExitCode.failure + } + + tokenConfig.keychainItems.removeAll() + print("Cleared token configuration.") + } + } +} + + +var driverConfig: TKTokenDriver.Configuration { + TKTokenDriver.Configuration.driverConfigurations["com.mwielgoszewski.SecureEnclaveToken.SecureEnclaveTokenExtension"]! +} + +var serialNumber: String? { + let platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice") ) + + guard platformExpert > 0 else { + return nil + } + + let serialNumber = (IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String) + + IOObjectRelease(platformExpert) + return serialNumber +} + + +// A unique, persistent identifier for this token. +// This value is typically generated from the serial number of the target hardware. +var tokenID: String { + let fallbackInstanceID = "819D11D7A8F7D609F236F529996E9F4C" + + guard serialNumber != nil else { + return fallbackInstanceID + } + + let serialHash = SHA256.hash(data: serialNumber!.data(using: .utf8)!).hexStr.dropLast(32) + return String(serialHash) +} + + +func loadTokenConfig() -> TKToken.Configuration? { + if driverConfig.tokenConfigurations.isEmpty { + driverConfig.addTokenConfiguration(for: tokenID) + } + var tokenConfig = driverConfig.tokenConfigurations[tokenID] + if tokenConfig == nil { + tokenConfig = driverConfig.addTokenConfiguration(for: tokenID) + } + return tokenConfig +} + +func unloadTokenConfig() { + driverConfig.removeTokenConfiguration(for: tokenID) +} + +extension Data { + func hexEncodedString() -> String { + return map { String(format: "%02hhx", $0) }.joined() + } +} + +extension Digest { + var bytes: [UInt8] { Array(makeIterator()) } + var data: Data { Data(bytes) } + + var hexStr: String { + bytes.map { String(format: "%02X", $0) }.joined() + } +} diff --git a/SecureEnclaveToken/SecureEnclaveTokenUtils.swift b/SecureEnclaveToken/SecureEnclaveTokenUtils.swift index 35cec4c..f5c987e 100644 --- a/SecureEnclaveToken/SecureEnclaveTokenUtils.swift +++ b/SecureEnclaveToken/SecureEnclaveTokenUtils.swift @@ -10,9 +10,7 @@ import Security import CryptoKit import CryptoTokenKit -func generateKeyInEnclave(tag: Data, accessibility: CFString, accessControlFlags: Int) -> SecKey { - print("Generating key ...") - +func generateKeyInEnclaveFromUi(tag: Data, accessibility: CFString, accessControlFlags: Int) -> SecKey { var flags: SecAccessControlCreateFlags switch accessControlFlags { @@ -26,6 +24,10 @@ func generateKeyInEnclave(tag: Data, accessibility: CFString, accessControlFlags flags = [SecAccessControlCreateFlags.privateKeyUsage] } + return generateKeyInEnclave(tag: tag, accessibility: accessibility, flags: flags) +} + +func generateKeyInEnclave(tag: Data, accessibility: CFString, flags: SecAccessControlCreateFlags) -> SecKey { let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, flags, @@ -44,26 +46,15 @@ func generateKeyInEnclave(tag: Data, accessibility: CFString, accessControlFlags var publicKey, privateKey: SecKey? - let status = SecKeyGeneratePair(attributes as CFDictionary, &publicKey, &privateKey) - - print(status) - print(privateKey!) - print(publicKey!) + _ = SecKeyGeneratePair(attributes as CFDictionary, &publicKey, &privateKey) var error: Unmanaged? // Create a bogus signature of the data to prove biometric - let signature = SecKeyCreateSignature(privateKey!, - .ecdsaSignatureMessageX962SHA256, - tag as CFData, - &error) as Data? - print("Signature: \(String(describing: signature))") - - let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) - print(ixy!) - - let bytes: Data = ixy! as Data - print(bytes.base64EncodedString()) + _ = SecKeyCreateSignature(privateKey!, + .ecdsaSignatureMessageX962SHA256, + tag as CFData, + &error) as Data? return privateKey! } @@ -96,55 +87,68 @@ func deleteSecureEnclaveKey(tag: Data) -> Bool { return status == errSecSuccess } -func loadCertificateForTagIntoTokenConfig(certificatePath: URL, tag: Data, tokenConfig: TKToken.Configuration) -> SecCertificate? { +enum CertificateImportError: Error { + case keysMismatch, keyDoesNotExist +} - if FileManager.default.fileExists(atPath: certificatePath.path) { - do { - let certificateData = try Data(contentsOf: certificatePath) - print("Read certificate") +func addCertificateToToken(certificate: SecCertificate, tag: Data, tokenConfig: TKToken.Configuration) throws { - let certificate = SecCertificateCreateWithData(nil, certificateData as CFData)! - print(certificate) + let certificatePublicKey = SecCertificateCopyKey(certificate) + + let secKey = loadSecureEnclaveKey(tag: tag) + guard secKey != nil else { + throw CertificateImportError.keyDoesNotExist + } - let certificatePublicKey = SecCertificateCopyKey(certificate) + let publicKey = SecKeyCopyPublicKey(secKey!) - let secKey = loadSecureEnclaveKey(tag: tag)! - let publicKey = SecKeyCopyPublicKey(secKey) + var error: Unmanaged? + let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) + let bytes: Data = ixy! as Data - var error: Unmanaged? - let ixy = SecKeyCopyExternalRepresentation(publicKey!, &error) - let bytes: Data = ixy! as Data + let ixy2 = SecKeyCopyExternalRepresentation(certificatePublicKey!, &error) + let bytes2: Data = ixy2! as Data - let ixy2 = SecKeyCopyExternalRepresentation(certificatePublicKey!, &error) - let bytes2: Data = ixy2! as Data + if bytes != bytes2 { + throw CertificateImportError.keysMismatch + } - if bytes != bytes2 { - print("Public key bytes of certificate do not match that of key for this tag") - throw NSError() - } + let publicKeyHash = Insecure.SHA1.hash(data: bytes) + + var commonName: CFString? + _ = SecCertificateCopyCommonName(certificate, &commonName) + + let tokenCertificate = TKTokenKeychainCertificate(certificate: certificate, objectID: tag) + tokenCertificate?.label = "\(String(data: tag, encoding: .utf8) ?? "") certificate" + + let tokenKey = TKTokenKeychainKey(certificate: certificate, objectID: tag) + tokenKey?.label = "\(String(data: tag, encoding: .utf8) ?? "") key" + tokenKey?.canSign = true + tokenKey?.canPerformKeyExchange = true + tokenKey?.isSuitableForLogin = true + tokenKey?.canDecrypt = false + tokenKey?.applicationTag = tag + tokenKey?.keyType = kSecAttrKeyTypeECSECPrimeRandom as String + tokenKey?.publicKeyData = bytes + tokenKey?.publicKeyHash = publicKeyHash.data + + tokenConfig.keychainItems.append(tokenKey!) + tokenConfig.keychainItems.append(tokenCertificate!) + return +} - let publicKeyHash = Insecure.SHA1.hash(data: bytes) - var commonName: CFString? - _ = SecCertificateCopyCommonName(certificate, &commonName) +func loadCertificateForTagIntoTokenConfig(certificatePath: URL, tag: Data, tokenConfig: TKToken.Configuration) -> SecCertificate? { - let tokenCertificate = TKTokenKeychainCertificate(certificate: certificate, objectID: tag) - tokenCertificate?.label = "Certificate for PIV Authentication (\(commonName ?? "Secure Enclave" as CFString))" + if FileManager.default.fileExists(atPath: certificatePath.path) { + do { + let certificateData = try Data(contentsOf: certificatePath) + print("Read certificate") - let tokenKey = TKTokenKeychainKey(certificate: certificate, objectID: tag) - tokenKey?.label = "PIV AUTH key" - tokenKey?.canSign = true - tokenKey?.canPerformKeyExchange = true - tokenKey?.isSuitableForLogin = true - tokenKey?.canDecrypt = false - tokenKey?.applicationTag = tag - tokenKey?.keySizeInBits = 256 - tokenKey?.keyType = kSecAttrKeyTypeECSECPrimeRandom as String - tokenKey?.publicKeyData = bytes - tokenKey?.publicKeyHash = publicKeyHash.data + let certificate = SecCertificateCreateWithData(nil, certificateData as CFData)! + print(certificate) - tokenConfig.keychainItems.append(tokenKey!) - tokenConfig.keychainItems.append(tokenCertificate!) + try addCertificateToToken(certificate: certificate, tag: tag, tokenConfig: tokenConfig) return certificate } catch { print("Failed to create cert??") @@ -194,7 +198,6 @@ func importCertificateAndCreateSecIdentity(key: SecKey, certificatePath: URL, ta } return identity - } } catch { diff --git a/SecureEnclaveToken/main.swift b/SecureEnclaveToken/main.swift new file mode 100644 index 0000000..489667b --- /dev/null +++ b/SecureEnclaveToken/main.swift @@ -0,0 +1,18 @@ +// +// main.swift +// SecureEnclaveToken +// +// Created by Marcin Wielgoszewski on 9/24/22. +// + +import Foundation +import AppKit + + +let app = NSApplication.shared +let delegate = AppDelegate() +app.delegate = delegate + +// 2 +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) + diff --git a/SecureEnclaveTokenExtension/Token.swift b/SecureEnclaveTokenExtension/Token.swift index 5231b6b..168d6b7 100644 --- a/SecureEnclaveTokenExtension/Token.swift +++ b/SecureEnclaveTokenExtension/Token.swift @@ -14,14 +14,6 @@ class Token: TKToken, TKTokenDelegate { NSLog("Got instanceID: \(instanceID)") super.init(tokenDriver: tokenDriver, instanceID: instanceID) self.keychainContents?.fill(with: configuration.keychainItems) - do { - let tag = "com.mwielgoszewski.SecureEnclaveToken.Key".data(using: .utf8)! - let certificate = try self.keychainContents?.certificate(forObjectID: tag) - NSLog("Got certificate for \(String(describing: certificate?.label)) -> \(String(describing: certificate?.data.base64EncodedString()))") - } catch { - NSLog("Failed pulling certificate") - } - NSLog("Got keychain items: \(String(describing: self.keychainContents?.items.count))") } func createSession(_ token: TKToken) throws -> TKTokenSession { diff --git a/SecureEnclaveTokenExtension/TokenSession.swift b/SecureEnclaveTokenExtension/TokenSession.swift index ebcd2ed..f63d349 100644 --- a/SecureEnclaveTokenExtension/TokenSession.swift +++ b/SecureEnclaveTokenExtension/TokenSession.swift @@ -102,8 +102,7 @@ class TokenSession: TKTokenSession, TKTokenSessionDelegate { throw NSError(domain: TKErrorDomain, code: TKError.Code.notImplemented.rawValue, userInfo: nil) } - func tokenSession(_ session: TKTokenSession, performKeyExchange otherPartyPublicKeyData: Data, keyObjectID objectID: Any, algorithm: TKTokenKeyAlgorithm, parameters: TKTokenKeyExchangeParameters) throws -> - Data { + func tokenSession(_ session: TKTokenSession, performKeyExchange otherPartyPublicKeyData: Data, keyObjectID objectID: Any, algorithm: TKTokenKeyAlgorithm, parameters: TKTokenKeyExchangeParameters) throws -> Data { let tag = String(data: objectID as! Data, encoding: .utf8)! NSLog("Querying for keyObjectID: \(tag) to perform key exchange")