Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trifle API support for Trifle key access group #166

Merged
merged 3 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions ios/Example/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- Trifle (0.2.3):
- Trifle (0.2.4):
- Wire (~> 4)
- Wire (4.5.1)
- WireCompiler (4.5.1)
Expand All @@ -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
17 changes: 17 additions & 0 deletions ios/Example/Tests/TrifleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion ios/Example/Trifle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -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 = "<group>"; };
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 = "<group>"; };
E103FF9A29AD90F5004011C2 /* CertificateDecodableTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDecodableTest.swift; sourceTree = "<group>"; };
E10D7FCB299410D0002E6B72 /* DEREncodableTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DEREncodableTest.swift; sourceTree = "<group>"; };
E12D7D7E29B71649003D1FB1 /* Trifle.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; name = Trifle.podspec; path = ../../Trifle.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
Expand Down Expand Up @@ -119,6 +120,7 @@
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
E1037DEF2AC642280015EAD1 /* Trifle_Example.entitlements */,
607FACF51AFB993E008FA782 /* Podspec Metadata */,
607FACD21AFB9204008FA782 /* Example for Trifle */,
607FACE81AFB9204008FA782 /* Tests */,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions ios/Example/Trifle_Example.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.cash</string>
</array>
</dict>
</plist>
8 changes: 7 additions & 1 deletion ios/Trifle/Sources/Common/KeychainQueries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -41,7 +43,7 @@ public class SecureEnclaveDigitalSignatureKeyManager
of: "{{uuid}}",
with: UUID().uuidString
)
try Self.generateKeypair(tag)
try Self.generateKeypair(tag, accessGroup)
return tag
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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<CFError>?
Expand All @@ -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 {
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down
23 changes: 18 additions & 5 deletions ios/Trifle/Sources/Trifle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

/**
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -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)
}

/**
Expand Down