diff --git a/.gitignore b/.gitignore index 7862e4f43..fd4a2a4ce 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ /TestResults /ShieldHost/**/xcuserdata/ -/ShieldHost/**/project.xcworkspace/ \ No newline at end of file diff --git a/ShieldHost/ShieldHost Watch App/ShieldHost Watch App.entitlements b/ShieldHost/ShieldHost Watch App/ShieldHost Watch App.entitlements new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/ShieldHost/ShieldHost Watch App/ShieldHost Watch App.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ShieldHost/ShieldHost.xcodeproj/project.pbxproj b/ShieldHost/ShieldHost.xcodeproj/project.pbxproj index 6b35577ec..c27440a26 100644 --- a/ShieldHost/ShieldHost.xcodeproj/project.pbxproj +++ b/ShieldHost/ShieldHost.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ AA2151F02975D9CF0072F6CA /* ShieldHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2151EF2975D9CF0072F6CA /* ShieldHostApp.swift */; }; - AA2151F42975D9D00072F6CA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA2151F32975D9D00072F6CA /* Assets.xcassets */; }; - AA2151F82975D9D00072F6CA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA2151F72975D9D00072F6CA /* Preview Assets.xcassets */; }; AA2152422975DF5F0072F6CA /* CertificationRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2152342975DF5F0072F6CA /* CertificationRequestBuilderTests.swift */; }; AA2152442975DF5F0072F6CA /* HmacTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2152362975DF5F0072F6CA /* HmacTests.swift */; }; AA2152452975DF5F0072F6CA /* OIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2152372975DF5F0072F6CA /* OIDTests.swift */; }; @@ -26,8 +24,6 @@ AA2152632975E3600072F6CA /* Shield in Frameworks */ = {isa = PBXBuildFile; productRef = AA2152622975E3600072F6CA /* Shield */; }; AA5768A12975E7C300142200 /* ShieldHost Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = AA5768A02975E7C300142200 /* ShieldHost Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AA5768A62975E7C300142200 /* ShieldHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5768A52975E7C300142200 /* ShieldHostApp.swift */; }; - AA5768AA2975E7C400142200 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA5768A92975E7C400142200 /* Assets.xcassets */; }; - AA5768AD2975E7C400142200 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA5768AC2975E7C400142200 /* Preview Assets.xcassets */; }; AA5768E02975E85E00142200 /* Shield in Frameworks */ = {isa = PBXBuildFile; productRef = AA5768DF2975E85E00142200 /* Shield */; }; AA5768E12975E87C00142200 /* HmacTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2152362975DF5F0072F6CA /* HmacTests.swift */; }; AA5768E22975E87C00142200 /* DistinguishedNameComposerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2152382975DF5F0072F6CA /* DistinguishedNameComposerTests.swift */; }; @@ -42,6 +38,8 @@ AA5768EC2975E87C00142200 /* DigestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA21523D2975DF5F0072F6CA /* DigestTests.swift */; }; AA5768ED2975E87C00142200 /* CertificateBuilderECTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA21523A2975DF5F0072F6CA /* CertificateBuilderECTests.swift */; }; AA5768EE2975E87C00142200 /* DistinguishedNameParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2152402975DF5F0072F6CA /* DistinguishedNameParserTests.swift */; }; + AAC4C7862ADDFFAD00487E0A /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC4C7852ADDFFAD00487E0A /* Utils.swift */; }; + AAC4C7872ADDFFAD00487E0A /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC4C7852ADDFFAD00487E0A /* Utils.swift */; }; AADD77C929A3E278005D0955 /* CertificateDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AADD77C629A3E278005D0955 /* CertificateDecoderTests.swift */; }; AADD77CA29A3E278005D0955 /* CertificateDecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AADD77C629A3E278005D0955 /* CertificateDecoderTests.swift */; }; /* End PBXBuildFile section */ @@ -87,10 +85,7 @@ /* Begin PBXFileReference section */ AA2151EC2975D9CF0072F6CA /* ShieldHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShieldHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA2151EF2975D9CF0072F6CA /* ShieldHostApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShieldHostApp.swift; sourceTree = ""; }; - AA2151F12975D9CF0072F6CA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - AA2151F32975D9D00072F6CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AA2151F52975D9D00072F6CA /* ShieldHost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShieldHost.entitlements; sourceTree = ""; }; - AA2151F72975D9D00072F6CA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; AA2151FD2975D9D00072F6CA /* ShieldHostTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShieldHostTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AA21521F2975DCA40072F6CA /* Shield */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Shield; path = ..; sourceTree = ""; }; AA2152342975DF5F0072F6CA /* CertificationRequestBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificationRequestBuilderTests.swift; sourceTree = ""; }; @@ -109,10 +104,9 @@ AA57689B2975E7C300142200 /* ShieldHost Watch Container.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ShieldHost Watch Container.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AA5768A02975E7C300142200 /* ShieldHost Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ShieldHost Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AA5768A52975E7C300142200 /* ShieldHostApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShieldHostApp.swift; sourceTree = ""; }; - AA5768A72975E7C300142200 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - AA5768A92975E7C400142200 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - AA5768AC2975E7C400142200 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; AA5768B22975E7C400142200 /* ShieldHost Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ShieldHost Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + AAC4C7812ADDCBE600487E0A /* ShieldHost Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ShieldHost Watch App.entitlements"; sourceTree = ""; }; + AAC4C7852ADDFFAD00487E0A /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; AADD77C629A3E278005D0955 /* CertificateDecoderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificateDecoderTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -178,22 +172,11 @@ isa = PBXGroup; children = ( AA2151EF2975D9CF0072F6CA /* ShieldHostApp.swift */, - AA2151F12975D9CF0072F6CA /* ContentView.swift */, - AA2151F32975D9D00072F6CA /* Assets.xcassets */, AA2151F52975D9D00072F6CA /* ShieldHost.entitlements */, - AA2151F62975D9D00072F6CA /* Preview Content */, ); path = ShieldHost; sourceTree = ""; }; - AA2151F62975D9D00072F6CA /* Preview Content */ = { - isa = PBXGroup; - children = ( - AA2151F72975D9D00072F6CA /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; AA21521E2975DCA40072F6CA /* Packages */ = { isa = PBXGroup; children = ( @@ -219,6 +202,7 @@ AA21523F2975DF5F0072F6CA /* SecIdentityTests.swift */, AA2152402975DF5F0072F6CA /* DistinguishedNameParserTests.swift */, AA2152412975DF5F0072F6CA /* SecKeyPairTests.swift */, + AAC4C7852ADDFFAD00487E0A /* Utils.swift */, ); name = Tests; path = ../Tests; @@ -234,22 +218,12 @@ AA5768A42975E7C300142200 /* ShieldHost Watch App */ = { isa = PBXGroup; children = ( + AAC4C7812ADDCBE600487E0A /* ShieldHost Watch App.entitlements */, AA5768A52975E7C300142200 /* ShieldHostApp.swift */, - AA5768A72975E7C300142200 /* ContentView.swift */, - AA5768A92975E7C400142200 /* Assets.xcassets */, - AA5768AB2975E7C400142200 /* Preview Content */, ); path = "ShieldHost Watch App"; sourceTree = ""; }; - AA5768AB2975E7C400142200 /* Preview Content */ = { - isa = PBXGroup; - children = ( - AA5768AC2975E7C400142200 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -402,8 +376,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - AA2151F82975D9D00072F6CA /* Preview Assets.xcassets in Resources */, - AA2151F42975D9D00072F6CA /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -425,8 +397,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - AA5768AD2975E7C400142200 /* Preview Assets.xcassets in Resources */, - AA5768AA2975E7C400142200 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -457,6 +427,7 @@ AA2152452975DF5F0072F6CA /* OIDTests.swift in Sources */, AA2152462975DF5F0072F6CA /* DistinguishedNameComposerTests.swift in Sources */, AA2152492975DF5F0072F6CA /* SecKeyTests.swift in Sources */, + AAC4C7862ADDFFAD00487E0A /* Utils.swift in Sources */, AA21524D2975DF5F0072F6CA /* SecIdentityTests.swift in Sources */, AA21524A2975DF5F0072F6CA /* SecCertificateTests.swift in Sources */, AA21524E2975DF5F0072F6CA /* DistinguishedNameParserTests.swift in Sources */, @@ -486,6 +457,7 @@ AA5768EE2975E87C00142200 /* DistinguishedNameParserTests.swift in Sources */, AA5768E72975E87C00142200 /* SecIdentityTests.swift in Sources */, AA5768EC2975E87C00142200 /* DigestTests.swift in Sources */, + AAC4C7872ADDFFAD00487E0A /* Utils.swift in Sources */, AA5768E82975E87C00142200 /* CryptorTests.swift in Sources */, AA5768E22975E87C00142200 /* DistinguishedNameComposerTests.swift in Sources */, AA5768ED2975E87C00142200 /* CertificateBuilderECTests.swift in Sources */, @@ -765,8 +737,10 @@ AA5768C52975E7C500142200 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_ENTITLEMENTS = "ShieldHost Watch App/ShieldHost Watch App.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = ShieldHost; @@ -790,8 +764,10 @@ AA5768C62975E7C500142200 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_ENTITLEMENTS = "ShieldHost Watch App/ShieldHost Watch App.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = ShieldHost; @@ -819,6 +795,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; INFOPLIST_KEY_CFBundleDisplayName = ShieldHost; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.outfoxx.ShieldHost; @@ -834,6 +811,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; INFOPLIST_KEY_CFBundleDisplayName = ShieldHost; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.outfoxx.ShieldHost; @@ -851,6 +829,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "io.outfoxx.ShieldHost-Watch-AppTests"; @@ -870,6 +849,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "io.outfoxx.ShieldHost-Watch-AppTests"; diff --git a/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..756a62bbd --- /dev/null +++ b/ShieldHost/ShieldHost.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,88 @@ +{ + "object": { + "pins": [ + { + "package": "BigInt", + "repositoryURL": "https://github.com/attaswift/BigInt.git", + "state": { + "branch": null, + "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version": "5.3.0" + } + }, + { + "package": "Float16", + "repositoryURL": "https://github.com/SusanDoggie/Float16.git", + "state": { + "branch": null, + "revision": "936ae66adccf1c91bcaeeb9c0cddde78a13695c3", + "version": "1.1.1" + } + }, + { + "package": "PotentCodables", + "repositoryURL": "https://github.com/outfoxx/PotentCodables.git", + "state": { + "branch": null, + "revision": "0c423eb5fdbbefffd36926430bf99f9f998c0cad", + "version": "3.1.1" + } + }, + { + "package": "Regex", + "repositoryURL": "https://github.com/sharplet/Regex.git", + "state": { + "branch": null, + "revision": "76c2b73d4281d77fc3118391877efd1bf972f515", + "version": "2.1.1" + } + }, + { + "package": "swift-algorithms", + "repositoryURL": "https://github.com/apple/swift-algorithms", + "state": { + "branch": null, + "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version": "1.0.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version": "1.0.4" + } + }, + { + "package": "SwiftDocCPlugin", + "repositoryURL": "https://github.com/apple/swift-docc-plugin", + "state": { + "branch": null, + "revision": "10bc670db657d11bdd561e07de30a9041311b2b1", + "version": "1.1.0" + } + }, + { + "package": "SymbolKit", + "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "state": { + "branch": null, + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", + "version": "1.0.0" + } + }, + { + "package": "swift-numerics", + "repositoryURL": "https://github.com/apple/swift-numerics", + "state": { + "branch": null, + "revision": "0a5bc04095a675662cf24757cc0640aa2204253b", + "version": "1.0.2" + } + } + ] + }, + "version": 1 +} diff --git a/Sources/ShieldSecurity/AlgorithmIdentifier.swift b/Sources/ShieldSecurity/AlgorithmIdentifier.swift index ce935c58a..6a2090cd1 100644 --- a/Sources/ShieldSecurity/AlgorithmIdentifier.swift +++ b/Sources/ShieldSecurity/AlgorithmIdentifier.swift @@ -68,7 +68,7 @@ public extension AlgorithmIdentifier { case .ec: let curve: OID - switch try publicKey.attributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 { + switch try publicKey.keyAttributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 { case 192: // P-192, secp192r1 curve = iso.memberBody.us.ansix962.curves.prime.prime192v1.oid diff --git a/Sources/ShieldSecurity/SecAccessibility.swift b/Sources/ShieldSecurity/SecAccessibility.swift new file mode 100644 index 000000000..bb11b244a --- /dev/null +++ b/Sources/ShieldSecurity/SecAccessibility.swift @@ -0,0 +1,55 @@ +// +// SecAccessibility.swift +// Shield +// +// Copyright © 2021 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Security + + +public enum SecAccessibility: Equatable { + case `default` + case unlocked(afterFirst: Bool, shared: Bool) + case passcodeEnabled +#if ACCESSIBILITY_ALWAYS_ENABLED + case always(shared: Bool) +#endif +} + + +extension SecAccessibility { + + var attr: Any { + + switch self { + +#if ACCESSIBILITY_ALWAYS_ENABLED + case .always(shared: true): + return kSecAttrAccessibleAlways as String + + case .always(shared: false): + return kSecAttrAccessibleAlwaysThisDeviceOnly as String +#endif + + case .unlocked(afterFirst: true, shared: true): + return kSecAttrAccessibleAfterFirstUnlock as String + + case .unlocked(afterFirst: true, shared: false): + return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + + case .unlocked(afterFirst: false, shared: true), .default: + return kSecAttrAccessibleWhenUnlocked as String + + case .unlocked(afterFirst: false, shared: false): + return kSecAttrAccessibleWhenUnlockedThisDeviceOnly as String + + case .passcodeEnabled: + return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly as String + } + } + +} diff --git a/Sources/ShieldSecurity/SecCertificate.swift b/Sources/ShieldSecurity/SecCertificate.swift index 53ef72453..9be76e4d2 100644 --- a/Sources/ShieldSecurity/SecCertificate.swift +++ b/Sources/ShieldSecurity/SecCertificate.swift @@ -29,6 +29,7 @@ public enum SecCertificateError: Int, Error { case trustValidationError = 5 case publicKeyRetrievalFailed = 6 case parsingFailed = 7 + case saveDuplicate = 8 } @@ -224,49 +225,52 @@ public extension SecCertificate { func attributes() throws -> [String: Any] { - #if os(iOS) || os(watchOS) || os(tvOS) - - let query = [ - kSecReturnAttributes as String: kCFBooleanTrue!, - kSecValueRef as String: self, - ] as [String: Any] as CFDictionary - - var data: CFTypeRef? + let query: [String: Any] = [ + kSecReturnAttributes as String: true, + kSecValueRef as String: self, + kSecUseDataProtectionKeychain as String: true, + ] - let status = SecItemCopyMatching(query as CFDictionary, &data) - if status != errSecSuccess { - throw SecCertificateError.queryFailed - } + var data: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &data) - #elseif os(macOS) + guard status == errSecSuccess, let attrs = data as? [String: Any] else { + throw SecCertificateError.queryFailed + } - let query: [String: Any] = [ - kSecReturnAttributes as String: kCFBooleanTrue!, - kSecUseItemList as String: [self] as CFArray, - ] + return attrs + } - var data: AnyObject? + func save(accessibility: SecAccessibility = .default) throws { - let status = SecItemCopyMatching(query as CFDictionary, &data) - if status != errSecSuccess { - throw SecCertificateError.queryFailed - } + let query: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecAttrLabel as String: UUID().uuidString, + kSecValueRef as String: self, + kSecAttrAccessible as String: accessibility.attr, + kSecUseDataProtectionKeychain as String: true, + ] - #endif + var data: CFTypeRef? + let status = SecItemAdd(query as CFDictionary, &data) - return data as! [String: Any] // swiftlint:disable:this force_cast + if status == errSecDuplicateItem { + throw SecCertificateError.saveDuplicate + } + else if status != errSecSuccess { + throw SecCertificateError.saveFailed + } } - func save() throws { + func delete() throws { - let query = [ + let query: [String: Any] = [ kSecClass as String: kSecClassCertificate, kSecValueRef as String: self, - ] as [String: Any] as CFDictionary - - var data: CFTypeRef? + kSecUseDataProtectionKeychain as String: true, + ] - let status = SecItemAdd(query, &data) + let status = SecItemDelete(query as CFDictionary) if status != errSecSuccess { throw SecCertificateError.saveFailed diff --git a/Sources/ShieldSecurity/SecIdentity.swift b/Sources/ShieldSecurity/SecIdentity.swift index d101d5a1b..fd97c9efc 100644 --- a/Sources/ShieldSecurity/SecIdentity.swift +++ b/Sources/ShieldSecurity/SecIdentity.swift @@ -25,35 +25,40 @@ public extension SecIdentity { case copyCertificateFailed } - static func create(certificate: SecCertificate, keyPair: SecKeyPair) throws -> SecIdentity { + static func create( + certificate: SecCertificate, + keyPair: SecKeyPair, + accessibility: SecAccessibility = .default + ) throws -> SecIdentity { - return try create(certificate: certificate, privateKey: keyPair.privateKey) + return try create(certificate: certificate, privateKey: keyPair.privateKey, accessibility: accessibility) } - static func create(certificate: SecCertificate, privateKey: SecKey) throws -> SecIdentity { + static func create( + certificate: SecCertificate, + privateKey: SecKey, + accessibility: SecAccessibility = .default + ) throws -> SecIdentity { do { - try privateKey.save() - } - catch SecKey.Error.saveDuplicate { - // Allowable... + do { + try privateKey.save(accessibility: accessibility) + } + catch SecKey.Error.saveDuplicate { + // Allowable... + } + + do { + try certificate.save(accessibility: accessibility) + } + catch SecKey.Error.saveDuplicate { + // Allowable... + } } catch { - throw Error.saveFailed - } - - let query: [String: Any] = [ - kSecClass as String: kSecClassCertificate, - kSecAttrLabel as String: UUID().uuidString, - kSecValueRef as String: certificate, - ] - - var data: CFTypeRef? - - let status = SecItemAdd(query as CFDictionary, &data) - - if status != errSecSuccess { try? privateKey.delete() + try? certificate.delete() + throw Error.saveFailed } diff --git a/Sources/ShieldSecurity/SecKey.swift b/Sources/ShieldSecurity/SecKey.swift index 0e0cccd8e..eaa3fbbd0 100644 --- a/Sources/ShieldSecurity/SecKey.swift +++ b/Sources/ShieldSecurity/SecKey.swift @@ -62,6 +62,7 @@ public extension SecKey { let query: [String: Any] = [ kSecValueRef as String: self, kSecReturnPersistentRef as String: kCFBooleanTrue!, + kSecUseDataProtectionKeychain as String: true, ] var ref: CFTypeRef? @@ -80,6 +81,7 @@ public extension SecKey { let query: [String: Any] = [ kSecValuePersistentRef as String: pref, kSecReturnRef as String: kCFBooleanTrue!, + kSecUseDataProtectionKeychain as String: true, ] var ref: CFTypeRef? @@ -108,8 +110,8 @@ public extension SecKey { var error: Unmanaged? - guard let key = SecKeyCreateWithData(data as CFData, attrs, &error), error == nil else { - throw error!.takeRetainedValue() + guard let key = SecKeyCreateWithData(data as CFData, attrs, &error) else { + throw error?.takeRetainedValue() ?? Error.importFailed } return key @@ -120,13 +122,13 @@ public extension SecKey { var error: Unmanaged? guard let data = SecKeyCopyExternalRepresentation(self, &error) else { - throw error!.takeRetainedValue() + throw error?.takeRetainedValue() ?? Error.exportFailed } return data as Data } - func attributes() throws -> [String: Any] { + func keyAttributes() throws -> [String: Any] { guard let attrs = SecKeyCopyAttributes(self) as? [String: Any] else { throw Error.noAttributes @@ -135,6 +137,24 @@ public extension SecKey { return attrs } + func attributes() throws -> [String: Any] { + + let query: [String: Any] = [ + kSecReturnAttributes as String: kCFBooleanTrue!, + kSecValueRef as String: self, + kSecUseDataProtectionKeychain as String: true, + ] + + var data: CFTypeRef? + + let status = SecItemCopyMatching(query as CFDictionary, &data) + guard status == errSecSuccess, let attrs = data as? [String: Any] else { + throw Error.build(error: .queryFailed, message: "Unable to load attributes", status: status) + } + + return attrs + } + func keyType() throws -> SecKeyType { let typeStr = try self.type() as CFString @@ -148,7 +168,7 @@ public extension SecKey { func type() throws -> String { - let attrs = try attributes() + let attrs = try keyAttributes() // iOS 10 SecKeyCopyAttributes returns string values, SecItemCopyMatching returns number values guard let type = (attrs[kSecAttrKeyType as String] as? NSNumber)? @@ -159,14 +179,16 @@ public extension SecKey { return type } - func save() throws { + func save(accessibility: SecAccessibility = .default) throws { - let attrs = try attributes() + let attrs = try keyAttributes() let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrKeyClass as String: attrs[kSecAttrKeyClass as String]!, kSecValueRef as String: self, + kSecUseDataProtectionKeychain as String: true, + kSecAttrAccessible as String: accessibility.attr, ] let status = SecItemAdd(query as CFDictionary, nil) @@ -182,7 +204,16 @@ public extension SecKey { func delete() throws { - try SecKey.delete(persistentReference: try persistentReference()) + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecValueRef as String: self, + kSecUseDataProtectionKeychain as String: true, + ] + + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + throw Error.deleteFailed + } } static func delete(persistentReference ref: Data) throws { @@ -190,6 +221,7 @@ public extension SecKey { let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecValuePersistentRef as String: ref, + kSecUseDataProtectionKeychain as String: true, ] let status = SecItemDelete(query as CFDictionary) @@ -235,12 +267,7 @@ public extension SecKey { var error: Unmanaged? guard let cipherText = SecKeyCreateEncryptedData(self, algorithm, plainText as CFData, &error) else { - if let error = error { - throw error.takeRetainedValue() - } - else { - throw Error.decryptionFailed - } + throw error?.takeRetainedValue() ?? Error.encryptionFailed } return cipherText as Data @@ -283,12 +310,7 @@ public extension SecKey { var error: Unmanaged? guard let plainText = SecKeyCreateDecryptedData(self, algorithm, cipherText as CFData, &error) else { - if let error = error { - throw error.takeRetainedValue() - } - else { - throw Error.decryptionFailed - } + throw error?.takeRetainedValue() ?? Error.decryptionFailed } return plainText as Data @@ -376,7 +398,7 @@ public extension SecKey { var error: Unmanaged? guard let signature = SecKeyCreateSignature(self, algorithm, data as CFData, &error) else { - throw error!.takeRetainedValue() + throw error?.takeRetainedValue() ?? Error.signFailed } return signature as Data diff --git a/Sources/ShieldSecurity/SecKeyPair.swift b/Sources/ShieldSecurity/SecKeyPair.swift index 0829ebf2b..8d3c53f25 100644 --- a/Sources/ShieldSecurity/SecKeyPair.swift +++ b/Sources/ShieldSecurity/SecKeyPair.swift @@ -87,6 +87,8 @@ public struct SecKeyPair { public enum Flag { /// Generate the key pair in the secure enclave. case secureEnclave + /// Should the key be saved in the keychain automatically. + case permanent } /// Type of key pair to generate ``SecKeyType/ec`` or ``SecKeyType/rsa``. @@ -120,36 +122,45 @@ public struct SecKeyPair { /// /// - Parameters: /// - label: User-visible label of the keys (optional). - /// - flat: Flags controlling the generation of the key pair. + /// - flags: Flags controlling the generation of the key pair. + /// - accessibility: Accessibility of the generated key pair. /// - Returns: Generated key pair. /// - Throws: Errors are thrown when the key generation of persistence to the kaychain fails. /// - public func generate(label: String? = nil, flags: Set = []) throws -> SecKeyPair { + public func generate( + label: String? = nil, + flags: Set = [.permanent], + accessibility: SecAccessibility = .default + ) throws -> SecKeyPair { guard let type = type else { fatalError("missing key type") } guard let keySize = keySize else { fatalError("missing key size") } - var attrs: [CFString: Any] = [ - kSecAttrKeyType: type.systemValue, - kSecAttrKeySizeInBits: keySize, - kSecAttrIsPermanent: true, + let isPermanent = flags.contains(.permanent) || flags.contains(.secureEnclave) || accessibility != .default + + var attrs: [String: Any] = [ + kSecAttrKeyType as String: type.systemValue, + kSecAttrKeySizeInBits as String: keySize, + kSecAttrIsPermanent as String: isPermanent, + kSecAttrAccessible as String: accessibility.attr, + kSecUseDataProtectionKeychain as String: true, ] if let label = label { - attrs[kSecAttrLabel] = label + attrs[kSecAttrLabel as String] = label } if flags.contains(.secureEnclave) { - attrs[kSecAttrTokenID] = kSecAttrTokenIDSecureEnclave + attrs[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave } var error: Unmanaged? guard let privateKey = SecKeyCreateRandomKey(attrs as CFDictionary, &error) else { - throw SecKeyPair.Error.generateFailed + throw error?.takeRetainedValue() ?? SecKeyPair.Error.generateFailed } guard let publicKey = SecKeyCopyPublicKey(privateKey) else { - throw SecKeyPair.Error.failedToCopyPublicKeyFromPrivateKey + throw SecKeyPair.Error.failedToCopyPublicKeyFromPrivateKey } return SecKeyPair(privateKey: privateKey, publicKey: publicKey) @@ -223,10 +234,10 @@ public struct SecKeyPair { /// /// - Throws: Errors are thrown if either of the keys could not be saved. /// - public func save() throws { + public func save(accessibility: SecAccessibility = .default) throws { - try privateKey.save() - try publicKey.save() + try privateKey.save(accessibility: accessibility) + try publicKey.save(accessibility: accessibility) } /// Delete the public and private key from the `Keychain`. @@ -619,12 +630,12 @@ private extension SecKey { return PrivateKeyInfo(version: .zero, privateKeyAlgorithm: .init(algorithm: iso.memberBody.us.ansix962.keyType.ecPublicKey.oid, parameters: ASN1.objectIdentifier(curveOID.fields)), - privateKey: privateKeyData) + privateKey: privateKeyData) } func getECCurveAndNumberSize() throws -> (OID, Int) { - switch try attributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 { + switch try keyAttributes()[kSecAttrKeySizeInBits as String] as? Int ?? 0 { case 192: // P-192, secp192r1 return (iso.memberBody.us.ansix962.curves.prime.prime192v1.oid, 24) diff --git a/Tests/CertificateBuilderECTests.swift b/Tests/CertificateBuilderECTests.swift index 1294b014c..a274f74ff 100644 --- a/Tests/CertificateBuilderECTests.swift +++ b/Tests/CertificateBuilderECTests.swift @@ -15,19 +15,15 @@ import XCTest class CertificateBuilderECTests: XCTestCase { - let outputEnabled = false - static var keyPair: SecKeyPair! - - override class func setUp() { - // Keys are comparatively slow to generate... so we do it once - guard let keyPair = try? SecKeyPair.Builder(type: .ec, keySize: 256).generate(label: "Test") else { - return XCTFail("Key pair generation failed") - } - Self.keyPair = keyPair + static let outputEnabled = false + var keyPair: SecKeyPair! + + override func setUpWithError() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) } - override class func tearDown() { - try? keyPair.delete() + override func tearDownWithError() throws { + try? keyPair?.delete() } func testBuildVer1() throws { @@ -38,10 +34,10 @@ class CertificateBuilderECTests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -59,10 +55,10 @@ class CertificateBuilderECTests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -81,10 +77,10 @@ class CertificateBuilderECTests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer, uniqueID: issuerID) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -104,10 +100,10 @@ class CertificateBuilderECTests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer, uniqueID: issuerID) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -129,14 +125,14 @@ class CertificateBuilderECTests: XCTestCase { try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) .addSubjectAlternativeNames(names: .dnsName("github.com/outfoxx/Shield")) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .extendedKeyUsage( keyPurposes: [iso.org.dod.internet.security.mechanisms.pkix.kp.serverAuth.oid], isCritical: false ) .issuer(name: issuer, uniqueID: issuerID) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -156,10 +152,10 @@ class CertificateBuilderECTests: XCTestCase { try Certificate.Builder() .subject(name: subject) .addSubjectAlternativeNames(names: .dnsName("github.com/outfoxx/Shield")) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -179,18 +175,18 @@ class CertificateBuilderECTests: XCTestCase { try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) .addSubjectAlternativeNames(names: .dnsName("github.com/outfoxx/Shield")) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer, uniqueID: issuerID) .addIssuerAlternativeNames(names: .dnsName("github.com/outfoxx/Shield/CA")) .basicConstraints(ca: true) .authorityKeyIdentifier( - Digester.digest(Self.keyPair.encodedPublicKey(), using: .sha1), + Digester.digest(keyPair.encodedPublicKey(), using: .sha1), certIssuer: [.dnsName("github.com/outfoxx/Shield/CA")], certSerialNumber: Certificate.Builder.randomSerialNumber() ) .computeSubjectKeyIdentifier() .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -208,8 +204,8 @@ class CertificateBuilderECTests: XCTestCase { let csrData = try CertificationRequest.Builder() .subject(name: NameBuilder().add("Shield Subject", forTypeName: "CN").name) - .publicKey(keyPair: Self.keyPair) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .publicKey(keyPair: keyPair) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let csr = try ASN1Decoder.decode(CertificationRequest.self, from: csrData) @@ -222,7 +218,7 @@ class CertificateBuilderECTests: XCTestCase { .request(csr) .issuer(name: csr.certificationRequestInfo.subject) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -237,12 +233,12 @@ class CertificateBuilderECTests: XCTestCase { try CertificationRequest.Builder() .subject(name: NameBuilder().add("Shield Subject", forTypeName: "CN").name) .addAlternativeNames(names: altNames) - .publicKey(keyPair: Self.keyPair, usage: [.dataEncipherment]) + .publicKey(keyPair: keyPair, usage: [.dataEncipherment]) .extendedKeyUsage( keyPurposes: [iso.org.dod.internet.security.mechanisms.pkix.kp.serverAuth.oid], isCritical: false ) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let csr = try ASN1Decoder.decode(CertificationRequest.self, from: csrData) @@ -255,7 +251,7 @@ class CertificateBuilderECTests: XCTestCase { .request(csr) .issuer(name: csr.certificationRequestInfo.subject) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -264,7 +260,7 @@ class CertificateBuilderECTests: XCTestCase { } func output(_ value: Encodable & SchemaSpecified) { - guard outputEnabled else { return } + guard Self.outputEnabled else { return } guard let data = try? value.encoded().base64EncodedString() else { return } print(data) } diff --git a/Tests/CertificateBuilderRSATests.swift b/Tests/CertificateBuilderRSATests.swift index f5b6eea2c..8a0be2221 100644 --- a/Tests/CertificateBuilderRSATests.swift +++ b/Tests/CertificateBuilderRSATests.swift @@ -15,19 +15,15 @@ import XCTest class CertificateBuilderRSATests: XCTestCase { - let outputEnabled = false - static var keyPair: SecKeyPair! - - override class func setUp() { - // Keys are comparatively slow to generate... so we do it once - guard let keyPair = try? SecKeyPair.Builder(type: .rsa, keySize: 2048).generate(label: "Test") else { - return XCTFail("Key pair generation failed") - } - Self.keyPair = keyPair + static let outputEnabled = false + var keyPair: SecKeyPair! + + override func setUpWithError() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) } - override class func tearDown() { - try? keyPair.delete() + override func tearDownWithError() throws { + try? keyPair?.delete() } func testBuildVer1() throws { @@ -38,10 +34,10 @@ class CertificateBuilderRSATests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -59,10 +55,10 @@ class CertificateBuilderRSATests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -81,10 +77,10 @@ class CertificateBuilderRSATests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer, uniqueID: issuerID) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -104,10 +100,10 @@ class CertificateBuilderRSATests: XCTestCase { let cert = try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer, uniqueID: issuerID) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -129,14 +125,14 @@ class CertificateBuilderRSATests: XCTestCase { try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) .addSubjectAlternativeNames(names: .dnsName("github.com/outfoxx/Shield")) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .extendedKeyUsage( keyPurposes: [iso.org.dod.internet.security.mechanisms.pkix.kp.serverAuth.oid], isCritical: false ) .issuer(name: issuer, uniqueID: issuerID) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -156,10 +152,10 @@ class CertificateBuilderRSATests: XCTestCase { try Certificate.Builder() .subject(name: subject) .addSubjectAlternativeNames(names: .dnsName("github.com/outfoxx/Shield")) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -179,18 +175,18 @@ class CertificateBuilderRSATests: XCTestCase { try Certificate.Builder() .subject(name: subject, uniqueID: subjectID) .addSubjectAlternativeNames(names: .dnsName("github.com/outfoxx/Shield")) - .publicKey(keyPair: Self.keyPair) + .publicKey(keyPair: keyPair) .issuer(name: issuer, uniqueID: issuerID) .addIssuerAlternativeNames(names: .dnsName("github.com/outfoxx/Shield/CA")) .basicConstraints(ca: true) .authorityKeyIdentifier( - Digester.digest(Self.keyPair.encodedPublicKey(), using: .sha1), + Digester.digest(keyPair.encodedPublicKey(), using: .sha1), certIssuer: [.dnsName("github.com/outfoxx/Shield/CA")], certSerialNumber: Certificate.Builder.randomSerialNumber() ) .computeSubjectKeyIdentifier() .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -208,8 +204,8 @@ class CertificateBuilderRSATests: XCTestCase { let csrData = try CertificationRequest.Builder() .subject(name: NameBuilder().add("Shield Subject", forTypeName: "CN").name) - .publicKey(keyPair: Self.keyPair) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .publicKey(keyPair: keyPair) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let csr = try ASN1Decoder.decode(CertificationRequest.self, from: csrData) @@ -222,7 +218,7 @@ class CertificateBuilderRSATests: XCTestCase { .request(csr) .issuer(name: csr.certificationRequestInfo.subject) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -237,12 +233,12 @@ class CertificateBuilderRSATests: XCTestCase { try CertificationRequest.Builder() .subject(name: NameBuilder().add("Shield Subject", forTypeName: "CN").name) .addAlternativeNames(names: altNames) - .publicKey(keyPair: Self.keyPair, usage: [.dataEncipherment]) + .publicKey(keyPair: keyPair, usage: [.dataEncipherment]) .extendedKeyUsage( keyPurposes: [iso.org.dod.internet.security.mechanisms.pkix.kp.serverAuth.oid], isCritical: false ) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let csr = try ASN1Decoder.decode(CertificationRequest.self, from: csrData) @@ -255,7 +251,7 @@ class CertificateBuilderRSATests: XCTestCase { .request(csr) .issuer(name: csr.certificationRequestInfo.subject) .valid(for: 86400 * 365) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(cert) @@ -264,7 +260,7 @@ class CertificateBuilderRSATests: XCTestCase { } func output(_ value: Encodable & SchemaSpecified) { - guard outputEnabled else { return } + guard Self.outputEnabled else { return } guard let data = try? value.encoded().base64EncodedString() else { return } print(data) } diff --git a/Tests/CertificateDecoderTests.swift b/Tests/CertificateDecoderTests.swift index 0a14bbbe0..1a30447b7 100644 --- a/Tests/CertificateDecoderTests.swift +++ b/Tests/CertificateDecoderTests.swift @@ -17,10 +17,12 @@ import XCTest class CertificateDecoderTests: XCTestCase { + static let outputEnabled = false + func testDecodingTestCerts() throws { for (title, pem) in Self.certificates { - print("Checking: \(title)") + output("Checking: \(title)") guard let testCert = try SecCertificate.load(pem: pem).first else { XCTFail("Failed to load '\(title)'") continue @@ -28,44 +30,44 @@ class CertificateDecoderTests: XCTestCase { do { let decoded = try ASN1.Decoder.decode(Certificate.self, from: testCert.derEncoded) let subjectName = try NameStringComposer().append(rdnSequence: decoded.tbsCertificate.subject).string - print("Version: \(decoded.tbsCertificate.version)") - print("Serial Number: \(decoded.tbsCertificate.serialNumber.magnitude.serialize())") - print("Subject: \(subjectName)") + output("Version: \(decoded.tbsCertificate.version)") + output("Serial Number: \(decoded.tbsCertificate.serialNumber.magnitude.serialize())") + output("Subject: \(subjectName)") let issuerName = try NameStringComposer().append(rdnSequence: decoded.tbsCertificate.issuer).string - print("Issuer: \(issuerName)") - print("Validity:") - print(" Not Before: \(decoded.tbsCertificate.validity.notBefore.zonedDate.iso8601EncodedString())") - print(" Not After: \(decoded.tbsCertificate.validity.notAfter.zonedDate.iso8601EncodedString())") + output("Issuer: \(issuerName)") + output("Validity:") + output(" Not Before: \(decoded.tbsCertificate.validity.notBefore.zonedDate.iso8601EncodedString())") + output(" Not After: \(decoded.tbsCertificate.validity.notAfter.zonedDate.iso8601EncodedString())") for ext in decoded.tbsCertificate.extensions ?? [] { - print("Extension:") + output("Extension:") switch ext.extnID { case iso_itu.ds.certificateExtension.basicConstraints.oid: let basicContstraints = try ASN1Decoder.decode(BasicConstraints.self, from: ext.extnValue) - print(" \(basicContstraints)") + output(" \(basicContstraints)") case iso_itu.ds.certificateExtension.extKeyUsage.oid: let extKeyUsage = try ASN1Decoder.decode(ExtKeyUsage.self, from: ext.extnValue) - print(" \(extKeyUsage)") + output(" \(extKeyUsage)") case iso_itu.ds.certificateExtension.subjectKeyIdentifier.oid: let subjKeyId = try ASN1Decoder.decode(SubjectKeyIdentifier.self, from: ext.extnValue) - print(" \(subjKeyId)") + output(" \(subjKeyId)") case iso_itu.ds.certificateExtension.authorityKeyIdentifier.oid: let authKeyId = try ASN1Decoder.decode(AuthorityKeyIdentifier.self, from: ext.extnValue) - print(" \(authKeyId)") + output(" \(authKeyId)") case iso_itu.ds.certificateExtension.subjectAltName.oid: let subjectAltName = try ASN1Decoder.decode(SubjectAltName.self, from: ext.extnValue) - print(" \(subjectAltName)") + output(" \(subjectAltName)") case iso_itu.ds.certificateExtension.issuerAltName.oid: let issuerAltName = try ASN1Decoder.decode(IssuerAltName.self, from: ext.extnValue) - print(" \(issuerAltName)") + output(" \(issuerAltName)") case let oid: - print(" Unknown: \(oid)") + output(" Unknown: \(oid)") } } } @@ -175,4 +177,15 @@ class CertificateDecoderTests: XCTestCase { """, ] + func output(_ value: String) { + guard Self.outputEnabled else { return } + print(value) + } + + func output(_ value: Encodable & SchemaSpecified) { + guard Self.outputEnabled else { return } + guard let data = try? value.encoded().base64EncodedString() else { return } + output(data) + } + } diff --git a/Tests/CertificationRequestBuilderTests.swift b/Tests/CertificationRequestBuilderTests.swift index 5e043d443..518ed76be 100644 --- a/Tests/CertificationRequestBuilderTests.swift +++ b/Tests/CertificationRequestBuilderTests.swift @@ -16,18 +16,14 @@ import XCTest class CertificationRequestBuilderTests: XCTestCase { static let outputEnabled = false - static var keyPair: SecKeyPair! - - override class func setUp() { - // Keys are comparatively slow to generate... so we do it once - guard let keyPair = try? SecKeyPair.Builder(type: .rsa, keySize: 2048).generate(label: "Test") else { - return XCTFail("Key pair generation failed") - } - Self.keyPair = keyPair + var keyPair: SecKeyPair! + + override func setUpWithError() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) } - override class func tearDown() { - try? keyPair.delete() + override func tearDownWithError() throws { + try? keyPair?.delete() } func testBuildParse() throws { @@ -36,9 +32,9 @@ class CertificationRequestBuilderTests: XCTestCase { let csr = try CertificationRequest.Builder() .subject(name: NameBuilder().add("Outfox Signing", forTypeName: "CN").name) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .extendedKeyUsage(keyPurposes: [keyPurpose.clientAuth.oid, keyPurpose.serverAuth.oid], isCritical: true) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(csr) @@ -73,9 +69,9 @@ class CertificationRequestBuilderTests: XCTestCase { try CertificationRequest.Builder() .subject(name: NameBuilder().add("Outfox Signing", forTypeName: "CN").name) .addAlternativeNames(names: sans) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .extendedKeyUsage(keyPurposes: [keyPurpose.clientAuth.oid, keyPurpose.serverAuth.oid], isCritical: true) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) output(csr) diff --git a/Tests/SecCertificateTests.swift b/Tests/SecCertificateTests.swift index 9649210af..fa6aca830 100644 --- a/Tests/SecCertificateTests.swift +++ b/Tests/SecCertificateTests.swift @@ -14,20 +14,79 @@ import XCTest class SecCertificateTests: XCTestCase { static let outputEnabled = false - static var keyPair: SecKeyPair! + var keyPair: SecKeyPair! - override class func setUp() { - // Keys are comparatively slow to generate... so we do it once - guard let keyPair = try? SecKeyPair.Builder(type: .rsa, keySize: 2048).generate(label: "Test") else { - return XCTFail("Key pair generation failed") - } - Self.keyPair = keyPair + override func setUpWithError() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) } - override class func tearDown() { - try? keyPair.delete() + override func tearDownWithError() throws { + try? keyPair?.delete() } + func testAttributesFailForNonPermanentCerts() throws { + + let subjectName = try NameBuilder() + .add("Unit Testing", forTypeName: "CN") + .add("123456", forTypeName: "UID") + .name + + let issuerName = try NameBuilder() + .add("Test Issuer", forTypeName: "CN") + .name + + let certData = + try Certificate.Builder() + .subject(name: subjectName) + .issuer(name: issuerName) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) + .valid(for: 86400 * 5) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) + .encoded() + + let cert = try SecCertificate.from(data: certData) + XCTAssertThrowsError(try cert.attributes()) + } + + func testSaveAcccessibilityUnlockedNotShared() throws { + + let subjectName = try NameBuilder() + .add("Unit Testing", forTypeName: "CN") + .add("123456", forTypeName: "UID") + .name + + let issuerName = try NameBuilder() + .add("Test Issuer", forTypeName: "CN") + .name + + let certData = + try Certificate.Builder() + .subject(name: subjectName) + .issuer(name: issuerName) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) + .valid(for: 86400 * 5) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) + .encoded() + + output(certData) + + let cert = try SecCertificate.from(data: certData) + defer { try? cert.delete() } + + do { + try cert.save(accessibility: .unlocked(afterFirst: true, shared: false)) + } + catch SecCertificateError.saveFailed { + #if os(macOS) + throw XCTSkip("Missing keychain entitlement") + #endif + } + + let attrs = try cert.attributes() + XCTAssertEqual(attrs[kSecAttrAccessible as String] as? String, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String) + } + + func testCertificateProperties() throws { let subjectName = try NameBuilder() @@ -43,9 +102,9 @@ class SecCertificateTests: XCTestCase { try Certificate.Builder() .subject(name: subjectName) .issuer(name: issuerName) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() output(certData) @@ -71,16 +130,16 @@ class SecCertificateTests: XCTestCase { try Certificate.Builder() .subject(name: subjectName) .issuer(name: issuerName) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() output(certData) let cert = try SecCertificate.from(data: certData) - XCTAssertEqual(try cert.publicKey?.encode(), try Self.keyPair.publicKey.encode()) + XCTAssertEqual(try cert.publicKey?.encode(), try keyPair.publicKey.encode()) } func testPEM() throws { @@ -98,9 +157,9 @@ class SecCertificateTests: XCTestCase { try Certificate.Builder() .subject(name: subjectName) .issuer(name: issuerName) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let certSec = try SecCertificate.from(data: certData) @@ -124,9 +183,9 @@ class SecCertificateTests: XCTestCase { try Certificate.Builder() .subject(name: subjectName) .issuer(name: issuerName) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let certSec = try SecCertificate.from(data: certData) @@ -140,25 +199,25 @@ class SecCertificateTests: XCTestCase { let rootName = try NameBuilder().add("Unit Testing Root", forTypeName: "CN").name let rootID = try Random.generate(count: 10) let rootSerialNumber = try Certificate.Builder.randomSerialNumber() - let rootKeyHash = try Digester.digest(Self.keyPair.encodedPublicKey(), using: .sha1) + let rootKeyHash = try Digester.digest(keyPair.encodedPublicKey(), using: .sha1) let rootCertData = try Certificate.Builder() .serialNumber(rootSerialNumber) .subject(name: rootName, uniqueID: rootID) .subjectAlternativeNames(names: .dnsName("io.outfoxx.shield.tests.ca")) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .subjectKeyIdentifier(rootKeyHash) .issuer(name: rootName) .issuerAlternativeNames(names: .dnsName("io.outfoxx.shield.tests.ca")) .basicConstraints(ca: true) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() output(rootCertData) let rootCert = try SecCertificate.from(data: rootCertData) - let certKeyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate() + let certKeyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) defer { try? certKeyPair.delete() } let certName = try NameBuilder().add("Unit Testing", forTypeName: "CN").name @@ -175,7 +234,7 @@ class SecCertificateTests: XCTestCase { .issuerAlternativeNames(names: .dnsName("io.outfoxx.shield.tests.ca")) .authorityKeyIdentifier(rootKeyHash, certIssuer: [.directoryName(rootName)], certSerialNumber: rootSerialNumber) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() output(certData) @@ -206,25 +265,25 @@ class SecCertificateTests: XCTestCase { let rootName = try NameBuilder().add("Unit Testing Root", forTypeName: "CN").name let rootID = try Random.generate(count: 10) let rootSerialNumber = try Certificate.Builder.randomSerialNumber() - let rootKeyHash = try Digester.digest(Self.keyPair.encodedPublicKey(), using: .sha1) + let rootKeyHash = try Digester.digest(keyPair.encodedPublicKey(), using: .sha1) let rootCertData = try Certificate.Builder() .serialNumber(rootSerialNumber) .subject(name: rootName, uniqueID: rootID) .subjectAlternativeNames(names: .dnsName("io.outfoxx.shield.tests.ca")) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign, .cRLSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign, .cRLSign]) .subjectKeyIdentifier(rootKeyHash) .issuer(name: rootName) .issuerAlternativeNames(names: .dnsName("io.outfoxx.shield.tests.ca")) .basicConstraints(ca: true) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() output(rootCertData) let rootCert = try SecCertificate.from(data: rootCertData) - let certKeyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate() + let certKeyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) defer { try? certKeyPair.delete() } let certName = try NameBuilder().add("Unit Testing", forTypeName: "CN").name @@ -241,7 +300,7 @@ class SecCertificateTests: XCTestCase { .issuerAlternativeNames(names: .dnsName("io.outfoxx.shield.tests.ca")) .authorityKeyIdentifier(rootKeyHash, certIssuer: [.directoryName(rootName)], certSerialNumber: rootSerialNumber) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() output(certData) @@ -262,13 +321,13 @@ class SecCertificateTests: XCTestCase { try Certificate.Builder() .subject(name: rootName) .issuer(name: rootName) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() ) - let certKeyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate() + let certKeyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) defer { try? certKeyPair.delete() } let certName = try NameBuilder().add("Unit Testing", forTypeName: "CN").name @@ -310,13 +369,13 @@ class SecCertificateTests: XCTestCase { try Certificate.Builder() .subject(name: rootName) .issuer(name: rootName) - .publicKey(keyPair: Self.keyPair, usage: [.keyCertSign]) + .publicKey(keyPair: keyPair, usage: [.keyCertSign]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() ) - let certKeyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate() + let certKeyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) defer { try? certKeyPair.delete() } let certName = try NameBuilder().add("Unit Testing", forTypeName: "CN").name @@ -327,7 +386,7 @@ class SecCertificateTests: XCTestCase { .issuer(name: rootName) .publicKey(keyPair: certKeyPair, usage: [.keyEncipherment]) .valid(for: 86400 * 5) - .build(signingKey: Self.keyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() ) diff --git a/Tests/SecIdentityTests.swift b/Tests/SecIdentityTests.swift index 5b8fab885..1ca0e84d6 100644 --- a/Tests/SecIdentityTests.swift +++ b/Tests/SecIdentityTests.swift @@ -16,15 +16,15 @@ class SecIdentityTests: XCTestCase { func testBuildAndFetch() throws { + let keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) + defer { try? keyPair.delete() } + let subject: Name = try NameBuilder() .add("Test Guy", forTypeName: "CN") .add("Test Corp", forTypeName: "O") .add("TC", forTypeName: "C") .name - let keyPair = try SecKeyPair.Builder(type: .rsa, keySize: 2048).generate(label: "Test") - defer { try? keyPair.delete() } - // Build a self-signed certificate for importing let cert = try Certificate.Builder() @@ -43,8 +43,21 @@ class SecIdentityTests: XCTestCase { } // Ensure all went well - let ident = try SecIdentity.create(certificate: cert, keyPair: keyPair) - defer {} + let ident: SecIdentity + do { + ident = try SecIdentity.create(certificate: cert, keyPair: keyPair) + } + catch SecIdentity.Error.saveFailed { + #if os(macOS) + throw XCTSkip("Missing keychain entitlement") + #else + return XCTFail("Save failed") + #endif + } + defer { + try? cert.delete() + try? keyPair.delete() + } XCTAssertNotNil(ident) XCTAssertNotNil(try ident.certificate()) diff --git a/Tests/SecKeyPairTests.swift b/Tests/SecKeyPairTests.swift index 939886567..02298bb1e 100644 --- a/Tests/SecKeyPairTests.swift +++ b/Tests/SecKeyPairTests.swift @@ -15,28 +15,17 @@ import XCTest class SecKeyPairTests: XCTestCase { - var rsaKeyPair: SecKeyPair! - var ecKeyPair: SecKeyPair! - - override func setUpWithError() throws { - try super.setUpWithError() - - rsaKeyPair = try SecKeyPair.Builder(type: .rsa, keySize: 2048).generate(label: "Test RSA Key") - ecKeyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate(label: "Test EC Key") - } + var keyPair: SecKeyPair! override func tearDownWithError() throws { - - try? rsaKeyPair?.delete() - try? ecKeyPair?.delete() - - try super.tearDownWithError() + try? keyPair?.delete() } func testGeneratedRSA() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048) let privateKeyAttrs = [ - kSecAttrLabel: "Test RSA Key", + kSecAttrLabel: "Test RSA Key Pair", kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecReturnRef: kCFBooleanTrue!, @@ -46,20 +35,21 @@ class SecKeyPairTests: XCTestCase { XCTAssertNotNil(privateKeyRef) let publicKeyAttrs = [ - kSecAttrLabel: "Test RSA Key", + kSecAttrLabel: "Test RSA Key Pair", kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPublic, kSecReturnRef: kCFBooleanTrue!, ] as [String: Any] as CFDictionary var publicKeyRef: CFTypeRef? - XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs as CFDictionary, &publicKeyRef), errSecSuccess) + XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs, &publicKeyRef), errSecSuccess) XCTAssertNotNil(publicKeyRef) } func testGeneratedEC() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256) let privateKeyAttrs = [ - kSecAttrLabel: "Test EC Key", + kSecAttrLabel: "Test EC Key Pair", kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecReturnRef: kCFBooleanTrue!, @@ -69,45 +59,50 @@ class SecKeyPairTests: XCTestCase { XCTAssertNotNil(privateKeyRef) let publicKeyAttrs = [ - kSecAttrLabel: "Test EC Key", + kSecAttrLabel: "Test EC Key Pair", kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPublic, kSecReturnRef: kCFBooleanTrue!, ] as [String: Any] as CFDictionary var publicKeyRef: CFTypeRef? - XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs as CFDictionary, &publicKeyRef), errSecSuccess) + XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs, &publicKeyRef), errSecSuccess) XCTAssertNotNil(publicKeyRef) } func testInitECFromExternalPrivateKey() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) - let external = try ecKeyPair.privateKey.encode() + let external = try keyPair.privateKey.encode() - XCTAssertNoThrow(try SecKeyPair(type: ecKeyPair.privateKey.keyType(), privateKeyData: external)) + XCTAssertNoThrow(try SecKeyPair(type: keyPair.privateKey.keyType(), privateKeyData: external)) } func testInitRSAFromExternalPrivateKey() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) - let external = try rsaKeyPair.privateKey.encode() + let external = try keyPair.privateKey.encode() - XCTAssertNoThrow(try SecKeyPair(type: rsaKeyPair.privateKey.keyType(), privateKeyData: external)) + XCTAssertNoThrow(try SecKeyPair(type: keyPair.privateKey.keyType(), privateKeyData: external)) } func testPersistentLoadRSA() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: [.permanent]) - let (privateKeyRef, publicKeyRef) = try rsaKeyPair.persistentReferences() + let (privateKeyRef, publicKeyRef) = try keyPair.persistentReferences() XCTAssertNotNil(try SecKeyPair(privateKeyRef: privateKeyRef, publicKeyRef: publicKeyRef)) } func testPersistentLoadEC() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: [.permanent]) - let (privateKeyRef, publicKeyRef) = try ecKeyPair.persistentReferences() + let (privateKeyRef, publicKeyRef) = try keyPair.persistentReferences() XCTAssertNotNil(try SecKeyPair(privateKeyRef: privateKeyRef, publicKeyRef: publicKeyRef)) } func testCertificateMatching() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) let name = try NameBuilder().add("Unit Testing", forTypeName: "CN").name @@ -115,9 +110,9 @@ class SecKeyPairTests: XCTestCase { try Certificate.Builder() .subject(name: name) .issuer(name: name) - .publicKey(keyPair: rsaKeyPair, usage: [.keyEncipherment]) + .publicKey(keyPair: keyPair, usage: [.keyEncipherment]) .valid(for: 86400 * 5) - .build(signingKey: rsaKeyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let cert = SecCertificateCreateWithData(nil, certData as CFData)! @@ -127,7 +122,7 @@ class SecKeyPairTests: XCTestCase { DispatchQueue.global(qos: .userInitiated).async { defer { finishedX.fulfill() } - let result = self.rsaKeyPair.matchesCertificate(certificate: cert, trustedCertificates: [cert]) + let result = self.keyPair.matchesCertificate(certificate: cert, trustedCertificates: [cert]) XCTAssertTrue(result) } @@ -137,6 +132,7 @@ class SecKeyPairTests: XCTestCase { #if swift(>=5.5) func testCertificateMatchingAsync() async throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) let name = try NameBuilder().add("Unit Testing", forTypeName: "CN").name @@ -144,21 +140,22 @@ class SecKeyPairTests: XCTestCase { try Certificate.Builder() .subject(name: name) .issuer(name: name) - .publicKey(keyPair: rsaKeyPair, usage: [.keyEncipherment]) + .publicKey(keyPair: keyPair, usage: [.keyEncipherment]) .valid(for: 86400 * 5) - .build(signingKey: rsaKeyPair.privateKey, digestAlgorithm: .sha256) + .build(signingKey: keyPair.privateKey, digestAlgorithm: .sha256) .encoded() let cert = SecCertificateCreateWithData(nil, certData as CFData)! - let result = await self.rsaKeyPair.matchesCertificate(certificate: cert, trustedCertificates: [cert]) + let result = await self.keyPair.matchesCertificate(certificate: cert, trustedCertificates: [cert]) XCTAssertTrue(result) } #endif func testImportExportEncryptedRSA() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) - let exportedKeyData = try rsaKeyPair.export(password: "123") + let exportedKeyData = try keyPair.export(password: "123") let importedKeyPair = try SecKeyPair.import(data: exportedKeyData, password: "123") @@ -166,7 +163,7 @@ class SecKeyPairTests: XCTestCase { let plainText = try Random.generate(count: 171) - let cipherText1 = try rsaKeyPair.publicKey.encrypt(plainText: plainText, padding: .oaep) + let cipherText1 = try keyPair.publicKey.encrypt(plainText: plainText, padding: .oaep) let plainText2 = try importedKeyPair.privateKey.decrypt(cipherText: cipherText1, padding: .oaep) @@ -174,12 +171,12 @@ class SecKeyPairTests: XCTestCase { let cipherText2 = try importedKeyPair.publicKey.encrypt(plainText: plainText, padding: .oaep) - let plainText3 = try rsaKeyPair.privateKey.decrypt(cipherText: cipherText2, padding: .oaep) + let plainText3 = try keyPair.privateKey.decrypt(cipherText: cipherText2, padding: .oaep) XCTAssertEqual(plainText, plainText3) - try rsaKeyPair.delete() - defer { rsaKeyPair = nil } + try? keyPair.delete() + defer { keyPair = nil } let cipherText3 = try importedKeyPair.publicKey.encrypt(plainText: plainText, padding: .oaep) @@ -190,14 +187,15 @@ class SecKeyPairTests: XCTestCase { } func testImportExportRSA() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) - let exportedKeyData = try rsaKeyPair.export() + let exportedKeyData = try keyPair.export() let importedKeyPair = try SecKeyPair.import(data: exportedKeyData) let plainText = try Random.generate(count: 171) - let cipherText1 = try rsaKeyPair.publicKey.encrypt(plainText: plainText, padding: .oaep) + let cipherText1 = try keyPair.publicKey.encrypt(plainText: plainText, padding: .oaep) let plainText2 = try importedKeyPair.privateKey.decrypt(cipherText: cipherText1, padding: .oaep) @@ -205,8 +203,9 @@ class SecKeyPairTests: XCTestCase { } func testImportExportEncryptedEC() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) - let exportedKeyData = try ecKeyPair.export(password: "123") + let exportedKeyData = try keyPair.export(password: "123") _ = try SecKeyPair.import(data: exportedKeyData, password: "123") @@ -214,87 +213,65 @@ class SecKeyPairTests: XCTestCase { } func testImportExportEC192() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 192, flags: []) - let ecKeyPair = - try SecKeyPair.Builder(type: .ec, keySize: 192) - .generate(label: "Test 192 EC Key") - defer { try? ecKeyPair.delete() } - - XCTAssertThrowsError(try SecKeyPair.import(data: ecKeyPair.export())) { error in + XCTAssertThrowsError(try SecKeyPair.import(data: keyPair.export())) { error in XCTAssertTrue(error is AlgorithmIdentifier.Error) } } func testImportExportEC256() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) - let ecKeyPair = - try SecKeyPair.Builder(type: .ec, keySize: 256) - .generate(label: "Test 256 EC Key") - defer { try? ecKeyPair.delete() } - - _ = try SecKeyPair.import(data: ecKeyPair.export()) + _ = try SecKeyPair.import(data: keyPair.export()) } func testImportExportEC384() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 384, flags: []) - let ecKeyPair = - try SecKeyPair.Builder(type: .ec, keySize: 384) - .generate(label: "Test 384 EC Key") - defer { try? ecKeyPair.delete() } - - _ = try SecKeyPair.import(data: ecKeyPair.export()) + _ = try SecKeyPair.import(data: keyPair.export()) } func testImportExportEC521() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 521, flags: []) - let ecKeyPair = - try SecKeyPair.Builder(type: .ec, keySize: 521) - .generate(label: "Test 521 EC Key") - defer { try? ecKeyPair.delete() } - - _ = try SecKeyPair.import(data: ecKeyPair.export()) + _ = try SecKeyPair.import(data: keyPair.export()) } - func testCodable() throws { + func testCodableRSA() throws { + keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048) + + let rsaData = try JSONEncoder().encode(keyPair) + let testKeyPair = try JSONDecoder().decode(SecKeyPair.self, from: rsaData) + XCTAssertEqual(testKeyPair.privateKey, keyPair.privateKey) + XCTAssertEqual(testKeyPair.publicKey, keyPair.publicKey) + } - let rsaData = try JSONEncoder().encode(rsaKeyPair) - let testRSAKeyPair = try JSONDecoder().decode(SecKeyPair.self, from: rsaData) - XCTAssertEqual(testRSAKeyPair.privateKey, rsaKeyPair.privateKey) - XCTAssertEqual(testRSAKeyPair.publicKey, rsaKeyPair.publicKey) + func testCodableEC() throws { + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 521) - let ecData = try JSONEncoder().encode(ecKeyPair) - let testECKeyPair = try JSONDecoder().decode(SecKeyPair.self, from: ecData) - XCTAssertEqual(testECKeyPair.privateKey, ecKeyPair.privateKey) - XCTAssertEqual(testECKeyPair.publicKey, ecKeyPair.publicKey) + let ecData = try JSONEncoder().encode(keyPair) + let testKeyPair = try JSONDecoder().decode(SecKeyPair.self, from: ecData) + XCTAssertEqual(testKeyPair.privateKey, keyPair.privateKey) + XCTAssertEqual(testKeyPair.publicKey, keyPair.publicKey) } func testGenerateSecureEnclave() throws { -#if os(macOS) - try XCTSkipIf(true, "Code signing complexities require this to be disabled for macOS") -#else - try XCTSkipUnless(SecureEnclave.isAvailable, "Only runs on iPhone/iPad/AppleTV") -#endif - - let keyPairBuilder = SecKeyPair.Builder(type: .ec, keySize: 256) + try XCTSkipUnless(SecureEnclave.isAvailable, "Requires secure enclave") - var keyPair: SecKeyPair? - XCTAssertNoThrow(keyPair = try keyPairBuilder.generate(label: "Test Secure Key", flags: [.secureEnclave])) - XCTAssertNoThrow(try keyPair?.delete()) + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: [.secureEnclave]) } func testGeneratedSecureEnclave() throws { + try XCTSkipUnless(SecureEnclave.isAvailable, "Requires secure enclave") #if os(macOS) try XCTSkipIf(true, "Code signing complexities require this to be disabled for macOS") -#else - try XCTSkipUnless(SecureEnclave.isAvailable, "Only runs on iPhone/iPad/AppleTV") #endif - let ecKeyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate(label: "Test Secure Enclave EC Key", - flags: [.secureEnclave]) - defer { try? ecKeyPair.delete() } + keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: [.secureEnclave]) let privateKeyAttrs = [ - kSecAttrLabel: "Test Secure Enclave EC Key", + kSecAttrLabel: "Test Secure Enclave EC Key Pair", kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecReturnRef: kCFBooleanTrue!, @@ -304,13 +281,13 @@ class SecKeyPairTests: XCTestCase { XCTAssertNotNil(privateKeyRef) let publicKeyAttrs = [ - kSecAttrLabel: "Test Secure Enclave EC Key", + kSecAttrLabel: "Test Secure Enclave EC Key Pair", kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPublic, kSecReturnRef: kCFBooleanTrue!, ] as [String: Any] as CFDictionary var publicKeyRef: CFTypeRef? - XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs as CFDictionary, &publicKeyRef), errSecSuccess) + XCTAssertEqual(SecItemCopyMatching(publicKeyAttrs, &publicKeyRef), errSecSuccess) XCTAssertNotNil(publicKeyRef) } diff --git a/Tests/SecKeyTests.swift b/Tests/SecKeyTests.swift index 1e76c4482..7e6a2d352 100644 --- a/Tests/SecKeyTests.swift +++ b/Tests/SecKeyTests.swift @@ -8,13 +8,55 @@ // Distributed under the MIT License, See LICENSE for details. // +import Security @testable import Shield import XCTest class SecKeyTests: XCTestCase { + func testAttributesFailForNonPermanentKeys() throws { + let keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) + defer { try? keyPair.delete() } + + XCTAssertNoThrow(try keyPair.privateKey.keyAttributes()) + XCTAssertThrowsError(try keyPair.privateKey.attributes()) + } + + func testSaveAcccessibilityUnlocked() throws { + // Check entitlement + try generateTestKeyPairChecked(type: .ec, keySize: 256).delete() + + let keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) + defer { try? keyPair.delete() } + + try keyPair.save(accessibility: .unlocked(afterFirst: false, shared: true)) + + let attrs = try keyPair.privateKey.attributes() + XCTAssertEqual(attrs[kSecAttrAccessible as String] as? String, kSecAttrAccessibleWhenUnlocked as String) + } + + func testGenerateAccessibilityUnlocked() throws { + let keyPair = try generateTestKeyPairChecked(type: .ec, + keySize: 256, + accessibility: .unlocked(afterFirst: false, shared: true)) + defer { try? keyPair.delete() } + + let attrs = try keyPair.privateKey.attributes() + XCTAssertEqual(attrs[kSecAttrAccessible as String] as? String, kSecAttrAccessibleWhenUnlocked as String) + } + + func testGenerateAccessibilityAfterFirstUnlockNotShared() throws { + let keyPair = try generateTestKeyPairChecked(type: .ec, + keySize: 256, + accessibility: .unlocked(afterFirst: true, shared: false)) + defer { try? keyPair.delete() } + + let attrs = try keyPair.privateKey.attributes() + XCTAssertEqual(attrs[kSecAttrAccessible as String] as? String, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String) + } + func testRSA() throws { - let keyPair = try SecKeyPair.Builder(type: .rsa, keySize: 2048).generate(label: "Test") + let keyPair = try generateTestKeyPairChecked(type: .rsa, keySize: 2048, flags: []) defer { try? keyPair.delete() } print("Checking: RSA Encrypt/Decrypt") @@ -36,7 +78,8 @@ class SecKeyTests: XCTestCase { } func testEC() throws { - let keyPair = try SecKeyPair.Builder(type: .ec, keySize: 256).generate(label: "Test") + let keyPair = try generateTestKeyPairChecked(type: .ec, keySize: 256, flags: []) + defer { try? keyPair.delete() } print("Checking: EC Encrypt/Decrypt") testFailedEncryptError(keyPair) @@ -58,7 +101,7 @@ class SecKeyTests: XCTestCase { func testECGeneration() throws { try [192, 256, 384, 521].forEach { keySize in - let keyPair = try SecKeyPair.Builder(type: .ec, keySize: keySize).generate(label: "Test") + let keyPair = try generateTestKeyPairChecked(type: .ec, keySize: keySize, flags: []) defer { try? keyPair.delete() } _ = try AlgorithmIdentifier(publicKey: keyPair.publicKey) diff --git a/Tests/Utils.swift b/Tests/Utils.swift new file mode 100644 index 000000000..8920e6c7d --- /dev/null +++ b/Tests/Utils.swift @@ -0,0 +1,45 @@ +// +// Utils.swift +// Shield +// +// Copyright © 2021 Outfox, inc. +// +// +// Distributed under the MIT License, See LICENSE for details. +// + +import Shield +import Security +import XCTest + +func generateTestKeyPairChecked( + type: SecKeyType, + keySize: Int, + flags: Set = [.permanent], + accessibility: SecAccessibility = .default +) throws -> SecKeyPair { + let label: String = [ + "Test", + flags.contains(.secureEnclave) ? "Secure Enclave" : nil, + type == .ec ? "EC" : "RSA", + "Key Pair", + ].compactMap { $0 }.joined(separator: " ") + + do { + return try SecKeyPair.Builder(type: type, keySize: keySize).generate(label: label, + flags: flags, + accessibility: accessibility) + } + catch let error where isEntitlementMissingError(error) { + #if os(macOS) + throw XCTSkip("Missing keychain entitlement") + #else + throw error + #endif + } +} + +func isEntitlementMissingError(_ error: Error) -> Bool { + let error = error as NSError + return error.domain == NSOSStatusErrorDomain && error.code == -34018 +}