How to Export a Public Key <Seckey> That Was Generated Using Seckeygeneratepair to Be Used on a Server

How do I export a public key SecKey that was generated using SecKeyGeneratePair to be used on a server?

SecItemCopyMatching is for you:

var dataPtr:Unmanaged<AnyObject>?
let query: [String:AnyObject] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: "com.example.site.public",
kSecReturnData: kCFBooleanTrue
]
let qResult = SecItemCopyMatching(query, &dataPtr)

// error handling with `qResult` ...

let publicKeyData = dataPtr!.takeRetainedValue() as NSData

// convert to Base64 string
let base64PublicKey = publicKeyData.base64EncodedStringWithOptions(nil)

Swift 4:

var dataPtr:CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "com.example.site.public",
kSecReturnData as String: true
]

let qResult = SecItemCopyMatching(query as CFDictionary, &dataPtr)

// error handling with `qResult` ...

let data = dataPtr as! Data
let base64PublicKey = data.base64EncodedString()

Note that the size of the data is 270, not the same as block size of the key. See this question on the crypto.stackexchange.com.

Swift 3 export SecKey to String

Export Key (iOS 10 only)

var error:Unmanaged<CFError>?
if let cfdata = SecKeyCopyExternalRepresentation(publicKey!, &error) {
let data:Data = cfdata as Data
let b64Key = data.base64EncodedString()
}

See https://stackoverflow.com/a/30662270/5276890 and https://stackoverflow.com/a/27935528/5276890 for longer ways which probably support iOS < 10.

Reimport Key

guard let data2 = Data.init(base64Encoded: b64Key) else {
return
}

let keyDict:[NSObject:NSObject] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: kSecAttrKeyClassPublic,
kSecAttrKeySizeInBits: NSNumber(value: 512),
kSecReturnPersistentRef: true as NSObject
]

guard let publicKey = SecKeyCreateWithData(data2 as CFData, keyDict as CFDictionary, nil) else {
return
}

Note: This generates a base64 key and not a certificate. A lot of code samples online deal with how to generate a public key from a certificate using SecCertificateCreateWithData

Also: 512 bit is fast to generate but worthless. Pick a longer and secure value once you're satisfied with the results.

I got valid results back when importing the key I generated and exported, so I assume it works, but I did not try to encrypt and decrypt with it.

Saving SecKeyRef device generated public/private key pair on disk

Ah, found the answer myself; you can get the bytes for a public key using SecItemCopyMatching().

- (NSData *)getPublicKeyBits {
OSStatus sanityCheck = noErr;
NSData * publicKeyBits = nil;

NSMutableDictionary * queryPublicKey = [[NSMutableDictionary alloc] init];

// Set the public key query dictionary.
[queryPublicKey setObject:(id)kSecClassKey forKey:(id)kSecClass];
[queryPublicKey setObject:publicTag forKey:(id)kSecAttrApplicationTag];
[queryPublicKey setObject:(id)kSecAttrKeyTypeRSA forKey:(id)kSecAttrKeyType];
[queryPublicKey setObject:[NSNumber numberWithBool:YES] forKey:(id)kSecReturnData];

// Get the key bits.
sanityCheck = SecItemCopyMatching((CFDictionaryRef)queryPublicKey, (CFTypeRef *)&publicKeyBits);

if (sanityCheck != noErr)
{
publicKeyBits = nil;
}

[queryPublicKey release];

return publicKeyBits;
}

The above is from Apple's CryptoExercise. Not sure if it works for private keys though.

How to generate RSA public and private keys using Swift 3?

Please try this :

let publicKeyAttr: [NSObject: NSObject] = [
kSecAttrIsPermanent:true as NSObject,
kSecAttrApplicationTag:"com.xeoscript.app.RsaFromScrach.public".data(using: String.Encoding.utf8)! as NSObject,
kSecClass: kSecClassKey, // added this value
kSecReturnData: kCFBooleanTrue] // added this value
let privateKeyAttr: [NSObject: NSObject] = [
kSecAttrIsPermanent:true as NSObject,
kSecAttrApplicationTag:"com.xeoscript.app.RsaFromScrach.private".data(using: String.Encoding.utf8)! as NSObject,
kSecClass: kSecClassKey, // added this value
kSecReturnData: kCFBooleanTrue] // added this value

var keyPairAttr = [NSObject: NSObject]()
keyPairAttr[kSecAttrKeyType] = kSecAttrKeyTypeRSA
keyPairAttr[kSecAttrKeySizeInBits] = 2048 as NSObject
keyPairAttr[kSecPublicKeyAttrs] = publicKeyAttr as NSObject
keyPairAttr[kSecPrivateKeyAttrs] = privateKeyAttr as NSObject

var publicKey : SecKey?
var privateKey : SecKey?;

let statusCode = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)

if statusCode == noErr && publicKey != nil && privateKey != nil {
print("Key pair generated OK")
var resultPublicKey: AnyObject?
var resultPrivateKey: AnyObject?
let statusPublicKey = SecItemCopyMatching(publicKeyAttr as CFDictionary, &resultPublicKey)
let statusPrivateKey = SecItemCopyMatching(privateKeyAttr as CFDictionary, &resultPrivateKey)

if statusPublicKey == noErr {
if let publicKey = resultPublicKey as? Data {
print("Public Key: \((publicKey.base64EncodedString()))")
}
}

if statusPrivateKey == noErr {
if let privateKey = resultPrivateKey as? Data {
print("Private Key: \((privateKey.base64EncodedString()))")
}
}
} else {
print("Error generating key pair: \(String(describing: statusCode))")
}

Client certificates and identities in iOS

We don't use that same method of CSR, but we have an equivalent thing where we do the following:

  1. Generate key pair
  2. Ship the public key to the remote server
  3. Remote server generates a signed client certificate using the public key
  4. Ship the client certificate back to the iOS device
  5. Add the client certificate to the keychain
  6. Later on, use the client certificate in an NSURLSession or similar.

As you seem to have discovered, iOS needs this extra thing called an "identity" to tie the client cert.

We also discovered that iOS has a weird thing where you need to DELETE the public key from the keychain before you add the client cert and identity into it, otherwise the identity doesn't seem to locate the client certificate properly instead. We chose to add the public key back in but as a "generic password" (i.e arbitrary user data) - we only do this because iOS doesn't have a sensible API for extracting a public key from a cert on the fly, and we need the public key for other strange things we happen to be doing.

If you're just doing TLS client certificate auth, once you have the certificate you won't need an explicit copy of the public key so you can simplify the process by simply deleting it, and skip the "add-back-in-as-generic-password" bit

Please excuse the giant pile of code, crypto stuff always seems to require a lot of work.

Here's bits of code to perform the above tasks:

Generating the keypair, and deleting/re-saving the public key

/// Returns the public key binary data in ASN1 format (DER encoded without the key usage header)
static func generateKeyPairWithPublicKeyAsGenericPassword(privateKeyTag: String, publicKeyAccount: String, publicKeyService: String) throws -> Data {
let tempPublicKeyTag = "TMPPUBLICKEY:\(privateKeyTag)" // we delete this public key and replace it with a generic password, but it needs a tag during the transition

let privateKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: privateKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]

let publicKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: tempPublicKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]

let keyPairAttr: [NSString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: 2048,
kSecPrivateKeyAttrs: privateKeyAttr,
kSecPublicKeyAttrs: publicKeyAttr ]

var publicKey: SecKey?, privateKey: SecKey?
let genKeyPairStatus = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
guard genKeyPairStatus == errSecSuccess else {
log.error("Generation of key pair failed. Error = \(genKeyPairStatus)")
throw KeychainError.generateKeyPairFailed(genKeyPairStatus)
}
// Would need CFRelease(publicKey and privateKey) here but swift does it for us

// we store the public key in the keychain as a "generic password" so that it doesn't interfere with retrieving certificates
// The keychain will normally only store the private key and the certificate
// As we want to keep a reference to the public key itself without having to ASN.1 parse it out of the certificate
// we can stick it in the keychain as a "generic password" for convenience
let findPubKeyArgs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey!,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecReturnData: true ]

var resultRef:AnyObject?
let status = SecItemCopyMatching(findPubKeyArgs as CFDictionary, &resultRef)
guard status == errSecSuccess, let publicKeyData = resultRef as? Data else {
log.error("Public Key not found: \(status))")
throw KeychainError.publicKeyNotFound(status)
}

// now we have the public key data, add it in as a generic password
let attrs: [NSString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrService: publicKeyService,
kSecAttrAccount: publicKeyAccount,
kSecValueData: publicKeyData ]

var result: AnyObject?
let addStatus = SecItemAdd(attrs as CFDictionary, &result)
if addStatus != errSecSuccess {
log.error("Adding public key to keychain failed. Error = \(addStatus)")
throw KeychainError.cannotAddPublicKeyToKeychain(addStatus)
}

// delete the "public key" representation of the public key from the keychain or it interferes with looking up the certificate
let pkattrs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey! ]

let deleteStatus = SecItemDelete(pkattrs as CFDictionary)
if deleteStatus != errSecSuccess {
log.error("Deletion of public key from keychain failed. Error = \(deleteStatus)")
throw KeychainError.cannotDeletePublicKeyFromKeychain(addStatus)
}
// no need to CFRelease, swift does this.
return publicKeyData
}

NOTE that publicKeyData isn't strictly in DER format, it's in "DER with the first 24 bytes trimmed off" format. I'm not sure what this is called officially, but both microsoft and apple seem to use it as the raw format for public keys. If your server is a microsoft one running .NET (desktop or core) then it will probably be happy with the public key bytes as-is. If it's Java and expects DER you may need to generate the DER header - this is a fixed sequence of 24 bytes you can probably just concatenate on.

Adding the client certificate to the keychain, generating an Identity

static func addIdentity(clientCertificate: Data, label: String) throws {
log.info("Adding client certificate to keychain with label \(label)")

guard let certificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, clientCertificate as CFData) else {
log.error("Could not create certificate, data was not valid DER encoded X509 cert")
throw KeychainError.invalidX509Data
}

// Add the client certificate to the keychain to create the identity
let addArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrLabel: label,
kSecValueRef: certificateRef,
kSecReturnAttributes: true ]

var resultRef: AnyObject?
let addStatus = SecItemAdd(addArgs as CFDictionary, &resultRef)
guard addStatus == errSecSuccess, let certAttrs = resultRef as? [NSString: Any] else {
log.error("Failed to add certificate to keychain, error: \(addStatus)")
throw KeychainError.cannotAddCertificateToKeychain(addStatus)
}

// Retrieve the client certificate issuer and serial number which will be used to retrieve the identity
let issuer = certAttrs[kSecAttrIssuer] as! Data
let serialNumber = certAttrs[kSecAttrSerialNumber] as! Data

// Retrieve a persistent reference to the identity consisting of the client certificate and the pre-existing private key
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrIssuer: issuer,
kSecAttrSerialNumber: serialNumber,
kSecReturnPersistentRef: true] // we need returnPersistentRef here or the keychain makes a temporary identity that doesn't stick around, even though we don't use the persistentRef

let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef);
guard copyStatus == errSecSuccess, let _ = resultRef as? Data else {
log.error("Identity not found, error: \(copyStatus) - returned attributes were \(certAttrs)")
throw KeychainError.cannotCreateIdentityPersistentRef(addStatus)
}

// no CFRelease(identityRef) due to swift
}

In our code we chose to return a label, and then look up the identity as-required using the label, and the following code. You could also chose to just return the identity ref from the above function rather than the label. Here's our getIdentity function anyway

Getting the identity later on

// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getIdentity(label: String) -> SecIdentity? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: label,
kSecReturnRef: true ]

var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}

// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecIdentity)
}

// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getCertificate(label: String) -> SecCertificate? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrLabel: label,
kSecReturnRef: true]

var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}

// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecCertificate)
}

And finally

Using the identity to authenticate against a server

This bit is in objc because that's how our app happens to work, but you get the idea:

SecIdentityRef _clientIdentity = [XYZ getClientIdentityWithLabel: certLabel];
if(_clientIdentity) {
CFRetain(_clientIdentity);
}
SecCertificateRef _clientCertificate = [XYZ getClientCertificateWithLabel:certLabel];
if(_clientCertificate) {
CFRetain(_clientCertificate);
}
...

- (void)URLSession:(nullable NSURLSession *)session
task:(nullable NSURLSessionTask *)task
didReceiveChallenge:(nullable NSURLAuthenticationChallenge *)challenge
completionHandler:(nullable void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {

if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
// supply the appropriate client certificate
id bridgedCert = (__bridge id)_clientCertificate;
NSArray* certificates = bridgedCert ? @[bridgedCert] : @[];
NSURLCredential* credential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceForSession];

completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
}

This code took a lot of time to get right. iOS certificate stuff is exceedingly poorly documented, hopefully this helps.



Related Topics



Leave a reply



Submit