diff --git a/README.md b/README.md index 70f678f..0b34b7d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Trifle SDK is implemented on client side in iOS and Android and on server side i ```swift // App start up -let trifle = try Trifle(reverseDomain: abc) +// The access group value must be added to the App Groups [entitlement file](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups). +let trifle = try Trifle(reverseDomain: abc, accessGroup: "group.trifle.cash.app") // Check if a key already exists. // If no key exists, generate a public key pair diff --git a/ios/Example/Podfile.lock b/ios/Example/Podfile.lock index 00c7307..6bf6183 100644 --- a/ios/Example/Podfile.lock +++ b/ios/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Trifle (0.2.3): + - Trifle (0.2.4): - Wire (~> 4) - Wire (4.5.1) - WireCompiler (4.5.1) @@ -18,10 +18,10 @@ EXTERNAL SOURCES: :path: "../../" SPEC CHECKSUMS: - Trifle: 1bc03d4dadabef03a928fc05924bbd7df1a53dd0 + Trifle: bb8422a8904ddfa192d5b7beedfbf5505087c947 Wire: b07a2ff1c4cd4b71f5ae26771cdd13fc7868c9df WireCompiler: 417c2ac583c01de328010738658758556ea92a92 PODFILE CHECKSUM: 4b8d6c2fc4c9668821977302cb1ff5c5891d9920 -COCOAPODS: 1.12.1 +COCOAPODS: 1.12.0 diff --git a/ios/Example/Tests/TrifleTests.swift b/ios/Example/Tests/TrifleTests.swift index 7122c35..acbde4c 100644 --- a/ios/Example/Tests/TrifleTests.swift +++ b/ios/Example/Tests/TrifleTests.swift @@ -145,6 +145,23 @@ final class TrifleTests: XCTestCase { // of our SDK instance and the key handle XCTAssertNotNil(signData2, "This is TODO - once this is validated, this test should FAIL") } + + func testDifferentAccessGroup_fail() throws { + let trifle = try Trifle(reverseDomain: TestFixtures.reverseDomain, accessGroup: "group.app.cash") + let otherTrifle = try Trifle(reverseDomain: TestFixtures.reverseDomain, accessGroup: "group.app.not.cash") + + let deviceCertificate = try TrifleCertificate.deserialize(data: TestFixtures.deviceTrifleCertEncoded2!) + let rootCertificate = try TrifleCertificate.deserialize(data: TestFixtures.rootTrifleCertEncoded!) + + let keyHandle = try trifle.generateKeyHandle() + + XCTAssertThrowsError(try otherTrifle.createSignedData( + data: TestFixtures.data, + keyHandle: keyHandle, + certificates: [deviceCertificate, rootCertificate]), + "unavailable keyPair" + ) + } func testSignEmptyData_fail() throws { let trifle = try Trifle(reverseDomain: TestFixtures.reverseDomain) diff --git a/ios/Example/Trifle.xcodeproj/project.pbxproj b/ios/Example/Trifle.xcodeproj/project.pbxproj index d064315..3d9b19b 100644 --- a/ios/Example/Trifle.xcodeproj/project.pbxproj +++ b/ios/Example/Trifle.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -65,6 +65,7 @@ B0075494B7FD4A77B59913B3 /* Pods-Trifle_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Trifle_Example.release.xcconfig"; path = "Target Support Files/Pods-Trifle_Example/Pods-Trifle_Example.release.xcconfig"; sourceTree = ""; }; B9892766F650E70A1D9BC557 /* Pods_Trifle_macOS_Dummy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Trifle_macOS_Dummy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C72A495F71F70549F960896D /* Pods_Trifle_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Trifle_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E1037DEF2AC642280015EAD1 /* Trifle_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Trifle_Example.entitlements; sourceTree = ""; }; E103FF9A29AD90F5004011C2 /* CertificateDecodableTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDecodableTest.swift; sourceTree = ""; }; E10D7FCB299410D0002E6B72 /* DEREncodableTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DEREncodableTest.swift; sourceTree = ""; }; E12D7D7E29B71649003D1FB1 /* Trifle.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; name = Trifle.podspec; path = ../../Trifle.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; @@ -119,6 +120,7 @@ 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( + E1037DEF2AC642280015EAD1 /* Trifle_Example.entitlements */, 607FACF51AFB993E008FA782 /* Podspec Metadata */, 607FACD21AFB9204008FA782 /* Example for Trifle */, 607FACE81AFB9204008FA782 /* Tests */, @@ -621,6 +623,7 @@ baseConfigurationReference = 14D99F6F8B2606DBCF35AE93 /* Pods-Trifle_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Trifle_Example.entitlements; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Trifle/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -641,6 +644,7 @@ baseConfigurationReference = B0075494B7FD4A77B59913B3 /* Pods-Trifle_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Trifle_Example.entitlements; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Trifle/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; diff --git a/ios/Example/Trifle_Example.entitlements b/ios/Example/Trifle_Example.entitlements new file mode 100644 index 0000000..c02d8d3 --- /dev/null +++ b/ios/Example/Trifle_Example.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.app.cash + + + diff --git a/ios/Trifle/Sources/Common/KeychainQueries.swift b/ios/Trifle/Sources/Common/KeychainQueries.swift index 3e945f3..d6095aa 100644 --- a/ios/Trifle/Sources/Common/KeychainQueries.swift +++ b/ios/Trifle/Sources/Common/KeychainQueries.swift @@ -12,9 +12,15 @@ protocol KeychainQueries { Constructs a query dictionary using the a given application tag - parameter applicationTag: an application tag used to distinguish the key from other keys in the keychain + - parameter accessGroup: the access group the security item belongs to. If no access group is set, then the + calling app's default access group is used. - parameter returnRef: whether a reference to `SecKey` should be returned */ - static func getQuery(with applicationTag: String, returnRef: Bool) -> NSMutableDictionary + static func getQuery( + with applicationTag: String, + _ accessGroup: String?, + returnRef: Bool + ) -> NSMutableDictionary } // MARK: - diff --git a/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveDigitalSignatureKeyManager.swift b/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveDigitalSignatureKeyManager.swift index 7c62751..b0f9d58 100644 --- a/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveDigitalSignatureKeyManager.swift +++ b/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveDigitalSignatureKeyManager.swift @@ -23,15 +23,17 @@ public class SecureEnclaveDigitalSignatureKeyManager // MARK: - Private Properties private let tagFormat: String + private let accessGroup: String? // MARK: - Initialization - public init(reverseDomain: String) throws { + public init(reverseDomain: String, accessGroup: String? = nil) throws { guard !reverseDomain.isEmpty else { // tag should not be empty throw TrifleError.invalidInput("Reverse domain cannot be empty") } self.tagFormat = reverseDomain + ".sign.{{uuid}}" + self.accessGroup = accessGroup } // MARK: - Public Methods (DigitalSignatureSigner) @@ -41,7 +43,7 @@ public class SecureEnclaveDigitalSignatureKeyManager of: "{{uuid}}", with: UUID().uuidString ) - try Self.generateKeypair(tag) + try Self.generateKeypair(tag, accessGroup) return tag } @@ -72,7 +74,7 @@ public class SecureEnclaveDigitalSignatureKeyManager internal func signingKey(_ tag: String) throws -> SecureEnclaveSigningKey { var keyRef: CFTypeRef? - let preparedQuery = SecureEnclaveKeychainQueries.getQuery(with: tag, returnRef: true) + let preparedQuery = SecureEnclaveKeychainQueries.getQuery(with: tag, accessGroup, returnRef: true) guard case let status = SecItemCopyMatching(preparedQuery, &keyRef), status == errSecSuccess, keyRef != nil else { @@ -94,12 +96,13 @@ public class SecureEnclaveDigitalSignatureKeyManager // MARK: - - private static func generateKeypair(_ tag: String) throws { + private static func generateKeypair(_ tag: String, _ accessGroup: String?) throws { let (keyType, keySize) = Self.keyInfo.curve.attrs let attributes = try SecureEnclaveKeychainQueries.attributes( with: tag, keyType: keyType, - keySize: keySize + keySize: keySize, + accessGroup: accessGroup ) var error: Unmanaged? @@ -108,8 +111,8 @@ public class SecureEnclaveDigitalSignatureKeyManager } } - internal static func deleteKeypair(_ tag: String) throws -> Bool { - let preparedQuery = SecureEnclaveKeychainQueries.getQuery(with: tag) + internal static func deleteKeypair(_ tag: String, _ accessGroup: String?) throws -> Bool { + let preparedQuery = SecureEnclaveKeychainQueries.getQuery(with: tag, accessGroup) let status = SecItemDelete(preparedQuery) switch status { @@ -120,8 +123,8 @@ public class SecureEnclaveDigitalSignatureKeyManager } } - internal static func keyExists(_ tag: String) throws -> Bool { - let preparedQuery = SecureEnclaveKeychainQueries.getQuery(with: tag) + internal static func keyExists(_ tag: String, _ accessGroup: String?) throws -> Bool { + let preparedQuery = SecureEnclaveKeychainQueries.getQuery(with: tag, accessGroup) let status = SecItemCopyMatching(preparedQuery, nil) switch status { case errSecSuccess: diff --git a/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveKeychainQueries.swift b/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveKeychainQueries.swift index 501c152..68d21f1 100644 --- a/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveKeychainQueries.swift +++ b/ios/Trifle/Sources/Digital Signature/Secure Enclave/SecureEnclaveKeychainQueries.swift @@ -33,7 +33,8 @@ internal struct SecureEnclaveKeychainQueries: KeychainQueries { internal static func attributes( with applicationTag: String, keyType: CFString, - keySize: Int + keySize: Int, + accessGroup: String? ) throws -> NSMutableDictionary { let attributes: NSMutableDictionary = [ kSecAttrKeyType: keyType, @@ -44,7 +45,11 @@ internal struct SecureEnclaveKeychainQueries: KeychainQueries { kSecAttrAccessControl: try Self.access, ] as [CFString: Any], ] - + // if accessGroup is nil, the calling application's access group will be used by default + if let accessGroup = accessGroup { + attributes[kSecAttrAccessGroup] = accessGroup + } + #if !targetEnvironment(simulator) attributes.setValue(kSecAttrTokenIDSecureEnclave, forKey: kSecAttrTokenID as String) #endif @@ -54,13 +59,21 @@ internal struct SecureEnclaveKeychainQueries: KeychainQueries { // MARK: - KeychainQueries - internal static func getQuery(with applicationTag: String, returnRef: Bool = false) -> NSMutableDictionary { - return [ + internal static func getQuery(with applicationTag: String, + _ accessGroup: String?, + returnRef: Bool = false + ) -> NSMutableDictionary { + let attributes: NSMutableDictionary = [ kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: applicationTag.data(using: .utf8)!, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: returnRef, + kSecReturnRef as String: returnRef, ] + // if accessGroup is nil, the calling application's access group will be used by default + if let accessGroup = accessGroup { + attributes[kSecAttrAccessGroup] = accessGroup + } + return attributes } } diff --git a/ios/Trifle/Sources/Trifle.swift b/ios/Trifle/Sources/Trifle.swift index c3e3a6e..f759397 100644 --- a/ios/Trifle/Sources/Trifle.swift +++ b/ios/Trifle/Sources/Trifle.swift @@ -15,6 +15,8 @@ public class Trifle { private let envelopeDataVersion = UInt32(0) private let contentSigner: ContentSigner + + private let accessGroup: String? /** Initialize the SDK with the key tag that is passed in. @@ -23,10 +25,21 @@ public class Trifle { certificate request and to sign messages. The library (Trifle) will automatically try to choose the best algorithm and key type available on this device. + + AccessGroup specifies the access group the Trifle key belongs to. Specifying this + attribute will mean that all key related APIs will be limited to the specified access group + (of which the calling application must be a member to obtain matching results.) + It is recommended that this value is set. + ** This value must be added to the App Group entitlement file. ** + + If the access group is not set, Trifle keys are created in the application's default access group. */ - public init(reverseDomain: String) throws { - self.contentSigner = - try SecureEnclaveDigitalSignatureKeyManager(reverseDomain: reverseDomain) + public init(reverseDomain: String, accessGroup: String? = nil) throws { + self.contentSigner = try SecureEnclaveDigitalSignatureKeyManager( + reverseDomain: reverseDomain, + accessGroup: accessGroup + ) + self.accessGroup = accessGroup } /** @@ -54,7 +67,7 @@ public class Trifle { // Other types of validity check to be added later eg type of key // right now we only check if key exists in key chain - return try SecureEnclaveDigitalSignatureKeyManager.keyExists(keyHandle.tag) + return try SecureEnclaveDigitalSignatureKeyManager.keyExists(keyHandle.tag, accessGroup) } /** @@ -71,7 +84,7 @@ public class Trifle { // Other types of validity check to be added later eg type of key // right now we only check if key exists in key chain - return try SecureEnclaveDigitalSignatureKeyManager.deleteKeypair(keyHandle.tag) + return try SecureEnclaveDigitalSignatureKeyManager.deleteKeypair(keyHandle.tag, accessGroup) } /**