Import p12 certificate with full access to private key for my application (OS X) - iphone

I'm accessing the https webserver with certificate authentication from mac app, so I need to handle authentication and provide my certificate (URLSession -> NSURLAuthenticationMethodClientCertificate -> call SecPKCS12Import and extract identity from imported certificate -> create NSURLCredential from identity and provide it in completionHandler to the server) .
But after each https request the dialog box "MYAPP wants to sign using "privateKey" in your keychain" is displayed:
I want to avoid this message. My app is signed correctly. I think I need to set access for the certificate while importing (full access for my app), I'm trying to do it using SecAccessCreate and SecPKCS12Import options:
func extractIdentity(certData:NSData, certPassword:String) -> IdentityAndTrust {
var identityAndTrust:IdentityAndTrust!
var securityError:OSStatus = errSecSuccess
var items:CFArray?
//let certOptions:CFDictionary = [ kSecImportExportPassphrase.takeRetainedValue() as String: certPassword ];
let index: CFIndex = 2
let passwordKey = kSecImportExportPassphrase as String;
let passwordValue: CFString = "PASSWORD";
let accessKey = kSecImportExportAccess as String;
var access:SecAccessRef? = nil;
SecAccessCreate("CERTIFICATE_NAME", nil, &access);
var keys = [unsafeAddressOf(accessKey), unsafeAddressOf(passwordKey)]
var values = [unsafeAddressOf(access!), unsafeAddressOf(passwordValue)]
var keyCallbacks = kCFTypeDictionaryKeyCallBacks
var valueCallbacks = kCFTypeDictionaryValueCallBacks
let options = CFDictionaryCreate(kCFAllocatorDefault, &keys, &values, index, &keyCallbacks, &valueCallbacks)
// import certificate to read its entries
securityError = SecPKCS12Import(certData, options, &items);
if securityError == errSecSuccess {
let certItems:CFArray = items as CFArray!;
let certItemsArray:Array = certItems as Array
let dict:AnyObject? = certItemsArray.first;
if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
// grab the identity
let identityPointer:AnyObject? = certEntry["identity"];
let secIdentityRef:SecIdentityRef = identityPointer as! SecIdentityRef!;
// grab the trust
let trustPointer:AnyObject? = certEntry["trust"];
let trustRef:SecTrustRef = trustPointer as! SecTrustRef;
// grab the certificate chain
var certRef:SecCertificate?
SecIdentityCopyCertificate(secIdentityRef, &certRef);
let certArray:NSMutableArray = NSMutableArray();
certArray.addObject(certRef as SecCertificateRef!);
identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray);
}
}
return identityAndTrust;
}
Anyway it doesn't work. How can I avoid this dialog box?
This thread How do I add authorizations to code sign an app from new keychain without any human interaction is related to importing the certificate using "security" command, and suggestion was to use -A or -T flags while importing the certificate, but can I do it programmatically without console commands?

You're probably building and running the application multiple times, which means that the cert was added to the keychain the first time, and the executable that did it was authorized to use the private key. However when you made some changes and rebuilt the project, the executable was replaced and the new executable doesn't have access to the private key (this is also an issue when your users have to update your software or reinstall for any reason).
What I found I had to do was remove the cert from the keychain after I used it, and re-add it before every use. However I've read that you can grant permission to an app identifier so that may work in your case too.
I have a code sample in my question's answer: How do I kill the popup?

Related

How to download images from AWS S3 in swift?

is there a good function to download images from AWS S3 bucket? I have an access key and a secret key for permisson. The URL is thru a different database accessible. I also already imported AWSS3 and AWSCore.
I have already found a upload function:
func uploadFile(withImage image: UIImage) {
let access = "access_key"
let secret = "secret_key"
let credentials = AWSStaticCredentialsProvider(accessKey: access, secretKey: secret)
let configuration = AWSServiceConfiguration(region: AWSRegionType.EUCentral1, credentialsProvider: credentials)
AWSServiceManager.default().defaultServiceConfiguration = configuration
let s3BucketName = "bucket_name"
let compressedImage = image.resizedImage(newSize: CGSize(width: 80, height: 80))
let data: Data = compressedImage.pngData()!
let remoteName = generateRandomStringWithLength(length: 12)+"."+data.format
print("REMOTE NAME : ",remoteName)
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { (task, progress) in
DispatchQueue.main.async(execute: {
// Update a progress bar
})
}
var completionHandler: AWSS3TransferUtilityUploadCompletionHandlerBlock?
completionHandler = { (task, error) -> Void in
DispatchQueue.main.async(execute: {
// Do something e.g. Alert a user for transfer completion.
// On failed uploads, `error` contains the error object.
})
}
let transferUtility = AWSS3TransferUtility.default()
transferUtility.uploadData(data, bucket: s3BucketName, key: remoteName, contentType: "image/"+data.format, expression: expression, completionHandler: completionHandler).continueWith { (task) -> Any? in
if let error = task.error {
print("Error : \(error.localizedDescription)")
}
if task.result != nil {
let url = AWSS3.default().configuration.endpoint.url
let publicURL = url?.appendingPathComponent(s3BucketName).appendingPathComponent(remoteName)
if let absoluteString = publicURL?.absoluteString {
// Set image with URL
print("Image URL : ",absoluteString)
}
}
return nil
}
}
I would not recommend to download files directly from S3 using an access and secret key.
I'd propose you do the following:
Make sure the bucket is as "private" as can be.
Have an API with authentication and authorisation (AWS API Gateway) that checks if the user is authenticated and permitted to download the S3 object.
Generate a pre-signed download URL with that is only valid for a short period of time (15-60 minutes).
Return that pre-signed download URL to your app through the API.
Use the URL within your app to download the S3 object.
This way you don't have to ship username and password in your app and the bucket is closed off to the "outside" reducing the risk of accidental information leakage.
Why I wouldn't recommend using the access key and secret key:
This is a potential security issue. People that reverse engineer the app could gain access to those "static" keys and depending on the underlying IAM role do all sorts of harm. But even if you have proper IAM roles with very limited access, essentially shipping a username and password with your app is not a good idea under any circumstance. How would you "rotate" the secret if something bad happens etc.

Where does SecKeyCreateSignature get the key name for Keychain signing authorization dialog?

I have noticed a difference between certain keys in the Keychain with respect to how they appear in the Keychain signing dialog, and I cannot figure out why some are displayed a certain way while others are not.
Here is some test code to use identities in the Keychain to sign a sample bit of data.
func testCreateSignature() throws {
let query: [String: Any] = [kSecClass as String: kSecClassIdentity,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: false,
kSecReturnRef as String: true,
kSecReturnData as String: true]
var resultsRef: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &resultsRef)
guard status == errSecSuccess else { throw SecurityError.unhandledError(status: status) }
guard let results = resultsRef as? [[String:Any]] else {
throw SecurityError.unexpectedCertificateData
}
let data = Data([0xDE, 0xAD, 0xBE, 0xEF])
var privateKey: SecKey!
for result in results {
let secIdentity = result[kSecValueRef as String] as! SecIdentity
try SecIdentityCopyPrivateKey(secIdentity, &privateKey).check()
var error: Unmanaged<CFError>?
let signature = SecKeyCreateSignature(privateKey, .rsaSignatureMessagePKCS1v15SHA1, data as CFData, &error)!
if let error = error {
throw error.takeRetainedValue()
}
print(signature)
}
}
When the code attempts to use one of the keys that Xcode installed for code signing, the resulting dialog looks like the following:
However, when the code attempts to use a key that I've installed, no matter what the label on the key in the Keychain is, it always looks like this:
When my app attempts to use a key to sign, I would like the user to see the name of the key the app wants to use, instead of just generic "privateKey", but I cannot find where this information might be stored on the key.
I have checked the kSecAttrLabel and kSecAttrApplicationLabel attributes of both identities and the private keys and cannot find the text that appears in the dialogs.
I found it. It is a property of the Access Control List of a Keychain item. See 'descriptor' param for SecAccessCreate.
If you do not specify a custom ACL when importing a key, it will default to "privateKey".
I was using SecPKCS12Import to import a .pfx file. I attempted to set the kSecImportExportAccess key in the options parameter to a custom SecAccess object, but it would always import with a default ACL.
I ended up refactoring the code to use SecItemImport instead to import the .pfx file and supplied a custom SecAccess instance:
static func importIdentity(contentsOf url: URL, password: String) throws {
let data = try Data.init(contentsOf: url)
var access: SecAccess!
try SecAccessCreate("License Key" as CFString, nil, &access).check()
var keychain: SecKeychain!
var outItems: CFArray?
let filename: CFString? = url.isFileURL ? url.lastPathComponent as CFString : nil
var inputFormat: SecExternalFormat = .formatPKCS12
var itemType: SecExternalItemType = .itemTypeAggregate
let unmanagedPassword = Unmanaged<AnyObject>.passRetained(password as AnyObject)
let unmanagedAccess = Unmanaged<SecAccess>.passRetained(access)
var params: SecItemImportExportKeyParameters = SecItemImportExportKeyParameters(version: UInt32(SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION),
flags: .importOnlyOne,
passphrase: unmanagedPassword,
alertTitle: nil,
alertPrompt: nil,
accessRef: unmanagedAccess,
keyUsage: nil,
keyAttributes: nil)
try SecKeychainCopyDefault(&keychain).check()
try SecItemImport(data as CFData, filename, &inputFormat, &itemType, [], &params, keychain, &outItems).check()
}
Importing the identity as above will result in "License Key" being shown in the signing dialog rather than "privateKey".

Keychain references in Swift used in NEVPNManager

I'm trying to connect to a VPN using Swift in Xcode. I'm using KeychainSwift to keep keychain references. My code looks like this:
private func connectVPN(completion: #escaping () -> Void) {
let keychain = KeychainSwift()
keychain.set("<mypassword>", forKey: "passref")
keychain.set("<sharedsecretpassword>", forKey: "secretref")
NEVPNManager.shared().loadFromPreferences { error in
let vpnhost = "<11.11.11.11>"
let username = "<myusername>"
let p = NEVPNProtocolIPSec()
p.username = username
p.localIdentifier = username
p.serverAddress = vpnhost
p.remoteIdentifier = vpnhost
p.authenticationMethod = .sharedSecret
p.disconnectOnSleep = false
p.sharedSecretReference = keychain.getData("secretref")
p.passwordReference = keychain.getData("passref")
var rules = [NEOnDemandRule]()
let rule = NEOnDemandRuleConnect()
rule.interfaceTypeMatch = .any
rules.append(rule)
NEVPNManager.shared().localizedDescription = "My VPN"
NEVPNManager.shared().protocolConfiguration = p
NEVPNManager.shared().onDemandRules = rules
NEVPNManager.shared().isOnDemandEnabled = true
NEVPNManager.shared().isEnabled = true
NEVPNManager.shared().saveToPreferences { error in
if (error != nil) {
print(error!)
} else {
do {
try NEVPNManager.shared().connection.startVPNTunnel()
completion()
} catch {
print("can't connect VPN'")
}
}
}
}
}
I'm using keychain.getData("secretref"), because this field needs
A persistent keychain reference to a keychain item containing the IKE
shared secret.
What's more,
The persistent keychain reference must refer to a keychain item of
class kSecClassGenericPassword.
I'm not really sure, if I'm doing it right. I didn't subclass kSecClassGenericPassword or use it in any way.
When I'm using this function in code, a window shows with information, that there is no shared secret for this VPN. I think it means that this keychain doesn't work as it's supposed to.
In iPhone settings, it tries to connect, moves switch towards green and instantly the switch goes back to "off" state. When I put the same data as in code manually, the connection works.
What am I doing wrong? What should I correct?
Okay, I have the answer. In the query for the SecItemCopyMatching, I had to choose kSecReturnPersistentRef with kCFBooleanTrue - not kSecReturnData.

Validate AWS Cognito Token with Swift

I have nearly completed the process for a developer authenticated sign in using AWS. I cannot seem to authenticate the back-end token that I receive and cannot seem to find any current implementations that are performing developer authentication via a third-party back-end. The error that I get is listed below.
As of right now my code looks like this:
Class containing Custom identity provider:
import Foundation
import AWSCognitoIdentityProvider
class CustomIdentityProvider: NSObject, AWSIdentityProviderManager {
var tokens: [NSString: NSString]?
init(tokens: [NSString: NSString]) {
self.tokens = tokens
}
#objc func logins() -> AWSTask<NSDictionary> {
return AWSTask(result: tokens! as NSDictionary)
}
}
AWS-APIManager.swift {snippet}
/* obtained cognito token from my back-end via getOpenIdTokenForDeveloperIdentity*/
/* From here I my app receives an IdentityId and Token */
let client_cognito_id = String(describing: valid_response)
let session_token = json.dictionaryValue["Token"]!
let login_with_amazon = NSString(string: "cognito-identity.amazonaws.com")
let token = NSString(string: String(describing: session_token))
let customProviderManager = CustomIdentityProvider(tokens: [login_with_amazon: token])
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: self.AWS_REGION,
identityPoolId: "us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
identityProviderManager: customProviderManager
)
credentialsProvider.setIdentityProviderManagerOnce(customProviderManager)
credentialsProvider.getIdentityId().continue ({ (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("Error!!!: " + (task.error?.localizedDescription)!)
} else {
// the task result will contain the identity id
let cognitoId = task.result
print(cognitoId)
print("SUCCESS!!!")
}
return nil
})
}
For some odd reason odd reason I can cannot authenticate the token that I have received. I get an error "Invalid login token. Can't pass in a Cognito token.". I've tried to follow the documentation and piece together working code that I have found literally hundreds of sources and cannot seem to be able to move past this part of the authentication process. Any help would be greatly appreciated. Thanks!
I believe the issue here is that although you are supplying the token, you are not setting the identity id that you are getting from your backend. As such, it is calling GetId with a Cognito OpenIdConnectToken, which is not supported.
The simplest client implementation of Developer Authenticated Identities is to extend AWSCognitoCredentialsProviderHelper
Apologies for providing this in Objective C instead of Swift. In your implementation just override the token method.
- (AWSTask<NSString *> *)token {
//get the identity id and token from your server
//You can use AWSTaskCompletionSource if you don't have it and need to get it asynchronously.
//Once you have this information, simply set the identity id and return the token
self.identityId = identityId;
return [AWSTask taskWithResult:token];
}

Why doesn't writing to the keychain succeed when deployed with an Enterprise signing identity but fine testing on device?

This function writes to the keychain properly when testing on both the simulator and the device. It does not work when installed with an enterprise Signing Identity.
It is worth mentioning Keychain Sharing is enabled in the app and the app's extension.
Please note it works perfectly on the device when building with the development profile.
Additionally no errors are logged in the operation (see below) so there isn't any information as to why it failed.
public class func setToken(token: String, account: String, service: String = "APIrev1") {
var log = XCGLogger.defaultInstance()
log.info("Attempting to set token \(token) to account \(account)")
var secret: NSData = token.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
let objects: Array = [secClassGenericPassword(), service, account, secret]
let keys: Array = [secClass(), secAttrService(), secAttrAccount(), secValueData()]
let query = NSDictionary(objects: objects, forKeys: keys)
var p1 = SecItemDelete(query as CFDictionaryRef)
if (p1 != noErr) {
log.info("SecItemDelete, key not present to delete")
}
var p2 = SecItemAdd(query as CFDictionaryRef, nil)
if (p2 != noErr) {
log.error("SecItemAdd write of token \(token) to account \(account) failed")
}
}
I believe this is a bug in Swift iOS that only occurs in 64 bit devices. This solution worked but I have filed a report:
https://stackoverflow.com/a/27721235/737470