Make PKCS12 not exportable from mac keychain

Good evening, I have a problem with pfx12 certificates.I can import the pkcs12 in the keychain, but I cannot set the private key as non-exportable in the Mac OSX keychain. Is there any solution?

This is my code

Code Block swiftlet PKCS12Data = NSData(base64Encoded: pfx, options: .ignoreUnknownCharacters)let inPKCS12Data = CFDataCreate(kCFAllocatorDefault, PKCS12Data!.bytes.assumingMemoryBound(to: UInt8.self), (PKCS12Data?.length)!) var items: CFArray?let optionDict: NSMutableDictionary = [            kSecImportExportPassphrase as NSString: "Password",            kSecAttrKeyType:kSecAttrKeyTypeRSA,            kSecAttrKeySizeInBits:2048,            kSecAttrIsExtractable:false,                       kSecPrivateKeyAttrs:[                kSecAttrIsPermanent:"YES",                kSecAttrCanDerive:"NO",                kSecAttrIsSensitive:"YES"            ],           ]let sanityCheck = SecPKCS12Import(PKCS12Data!, optionDict, &items)


Thanks you for the support

Answered by DTS Engineer in 824644022

[AFAICT slackware1590 never did open a DTS case about this, so I didn’t get a chance to dig into it at the time.]

This came up today in a different context, and I wanted to share my results. Consider this small test program:

import Foundation
let benjyP12Bytes: [UInt8] = [
]
func main() throws {
let keychains = try secCall { SecKeychainCopySearchList($0) } as! [SecKeychain]
let scratchQ = keychains.first { kc in
kc.url?.deletingPathExtension().lastPathComponent == "Scratch"
}
let scratch = try secCall { scratchQ }
let identity: SecIdentity
do {
print("will import")
let benjyP12Data = Data(benjyP12Bytes)
var format = SecExternalFormat.formatPKCS12
var type = SecExternalItemType.itemTypeUnknown
var parameters = SecItemImportExportKeyParameters()
let passphrase = Unmanaged.passRetained("test" as NSString as AnyObject)
defer { passphrase.release() }
parameters.passphrase = passphrase
let keyAttributes = Unmanaged.passRetained([kSecAttrIsPermanent, kSecAttrIsSensitive] as CFArray)
defer { keyAttributes.release() }
parameters.keyAttributes = keyAttributes
let identities = try secCall { SecItemImport(
benjyP12Data as NSData,
"Benjy.p12" as NSString,
&format,
&type,
[],
&parameters,
scratch,
$0
) } as! [SecIdentity]
print("did import, identities: \(identities)")
identity = try secCall { identities.first }
} catch {
print("did not import, error: \(error)")
throw error
}
let privateKey = try secCall { SecIdentityCopyPrivateKey(identity, $0) }
let attributes = try secCall { SecKeyCopyAttributes(privateKey) } as! [String: Any]
for (attrName, attrValue) in attributes {
print("\(attrName): \(attrValue)")
}
}
try main()

Note This isn’t a complete program. At the end of this post I have more details on how I ran this test.

When I run this I see:

did import, identities: [<SecIdentity 0x600003823fc0 [0x20638e240]>]
extr: 0

extr is kSecAttrIsExtractable, and a value of 0 indicates that this private key is not extractable.

In contrast, if I comment out the line that sets parameters.keyAttributes and run it again, it prints:

did import, identities: [<SecIdentity 0x600000515280 [0x20638e240]>]
extr: 1

indicating that the key is extractable in that case.

I’m pretty sure that this is ‘case closed’, but if you have any follow-up questions please post them here.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


I ran my test on macOS 15.2 with Xcode 16.2.

I used Keychain Access to create a new keychain, called “Scratch”, for this test to target. That ensures that the imported digital identity doesn’t get lost in my main login keychain.

Between each test I used Keychain Access to delete the imported digital identity (well, the certificate and private key) from the “Scratch” keychain.

I’ve elided the PKCS#12 data in the above program because it’s quite long. To test this yourself:

  • Fill in benjyP12Bytes with the hex bytes of your PKCS#12 blob.

  • Replace test with its passphrase.

The program uses the helpers from Calling Security Framework from Swift

Finally, the code relies on one additional helper method:

extension SecKeychain {
var url: URL? {
var length: UInt32 = 1024
var buffer = [CChar](repeating: 0, count: Int(length))
let err = SecKeychainGetPath(self, &length, &buffer)
guard err == errSecSuccess else { return nil }
return URL(fileURLWithFileSystemRepresentation: buffer, isDirectory: false, relativeTo: nil)
}
}

but I cannot set the private key as non-exportable in the Mac OSX
keychain

Most people have the opposite problem, that is, they want to make the key exportable (-:

btw I’m presuming that by “exportable” you mean “extractable”.

Have you tried using SecItemImport for this? SecPKCS12Import is a compatibility API brought over from iOS whereas SecItemImport is much better aligned with the features of macOS’s legacy file-based keychain.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hi Quinn, thanks for your support.

yes, i mean extractable and not exportable for the private key.

I tried using SecItemImport, but i have this problem:

I can extract and import into keychain certificate and key from pkcs12, but i can't modify the key attributes.

Code Block swift let sanityCheck = SecPKCS12Import(PKCS12Data!, optionDict, &items)        if sanityCheck == errSecSuccess {let items = items! as! [[String: Any]]            let firstItem = items[0]            let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity            var cert: SecCertificate?            SecIdentityCopyCertificate(identity, &cert)            var certDer = SecCertificateCopyData(cert!)             var key: SecKey?            SecIdentityCopyPrivateKey(identity, &key)            let keyDict = SecKeyCopyAttributes(key!) }


In the keyDict i can see the attributes,and the extractable attribute is TRUE.

Can I edit this attribute?


I tried this, but not work (same attributes from the original key)

Code Block swift            let keyDataTest: [UInt8]? = key?.keyData            var keyData:NSData = NSData(bytes: keyDataTest, length: keyDataTest!.count);                       var hostError: Unmanaged<CFError>?            var hostPrivateKey: SecKey?            hostPrivateKey = SecKeyCreateFromData(optionDict as CFDictionary, keyData as! CFData, &hostError)            SecIdentityCopyPrivateKey(identity, &hostPrivateKey)            let keyDict = SecKeyCopyAttributes(hostPrivateKey!)



Thanks you

Accepted Answer
This is my code

Code Block swift let keyBase64 = "MIIEpAIBAAKCAQEA5B7lqLrwVCFNUiCmwMr5Q48iuArOolxb7DAuclGnoZVX0SaJ8mrvCOtd6qY/VeBw227txWEPH7840qX/yGxxqTngdNCuDATqYrrbxFOGV30GZmg6NpZYKShTlsftkhiCsoXW0A7m5MCZUkH2/sNBC8oRHCNDXRlsU5bq/yPaAMt6xlBsUgLt/++INcuw+rx1Rm7LJv0FeukQmlekUOL/DMJXcLXCa05StTbvHPiAHOLej07pThCZoX3XHFpOTQ6379EsjvSZHtNhr67qrtRb8or2rX7wt5NWzXHbhUDlyzEcIBB/7G8ygqWhyZTEIMFiRMWSa3KGYZE3nZe5weC7SQIDAQABAoIBAQCjjxehA+++kmYK5YhKIP3Zl64QAQeo18m8rcsPgkZLj3V4a0Zq/orGfWNIE8zDePnSC1YFuBKM86D9P7IGdOKFsA6kEt9HlNqs0UczG6Pt5KGLGV3rt54cXGKacFyA7HwBHf8oDBc2mnUTymIaxcpEdqwP3aS2Ar1trX5uUrlC6UcZyspBZVYvMlU+uAKL1ZtFxjsv0EzuQQW1HX7b2WPUAoxp/yBC/EBRM9K8WbG9i7NB4FTFHAdTMt/EZLGUESizFgrai6lp3s96Apz5GvncRUI+UVP/7zbUaFYdRMW5lrcR8+PL9NACkL2rnQuLoyLKWZWPPlD3WEE9EzY4bH6FAoGBAPu8hL8goEbWMFDZuox04Ouy6EpXR8BDTq8ut6hmad6wpFgZD15Xu7pYEbbsPntdYODKDDAIJCBsiBgf2emL50BpiQkzMPhxyMsN5Pzry9Ys+AzPkJcQ7g+/Wbto9lCC+JmgxtGQ7JIibo1QH7BTsuK9+k72HnZne6oIfaYKbBZfAoGBAOf7/D2Q7NiNcEgxpZRn06+mnkHMb8PfCKfJf/BFf5WKXSkDBZ3XhWSPnZyQnE3gW3lzJjzUwHS+YDk8A0Xl2piAHa/d5O/8eoijB8wa6UGVDBIXqUnfM3Udfry78rM71FOpbzV3H48G7u4CUJMGwOpEqF0TfgtQr4uf8OurdH9XAoGAbdNhVsE1K7Jmgd97s6uKNUpobYaGlyrGOUd4eM+1gKIwEP9d5RsBm9qwX83RtKCYk3mSt6HVoQ+4kE3VFD8lNMTWNF1REBMUNwJo1K9KzrXvwicMPdv1AInK7ChuzdFWBDBQjT1c+KRs9tnt+U+Ky8F2Ytydjaq4GQZ7SuVhIqECgYBMsS+IovrJ9KhkFZWp5FFFRo4XLqDcXkWcQq87HZ66L03xGwCmV/PPdPMkKWKjFELpebnwbl1Zuv5QrZhfaUfFFsW5uF/RPuS7ezo+rb7jYYTmDlB3DYUTeLbHalMoEeV16xPK1yDlxeMDaFx+3sK0MBKBAsqurvP58txQ7RPMbQKBgQCzRcURopG0DF4VF4+xQJS8FpTcnQsQnO/2MJR35npA2iUb+ffs+0lgEdeWs4W46kvaF1iVEPbr6She+aKROzE9Bs25ZCgGLv97oUxDQo0IPvURX7ucN+xOUU1hw9oQDVdGKl1JZh93fn+bjtMTe+26asGLmM0r9YQX1P8qaw3KOg=="            let keyData1 = Data(base64Encoded: keyBase64)!            let keyTest = SecKeyCreateWithData(keyData1 as NSData, [                kSecAttrKeyType: kSecAttrKeyTypeRSA,                kSecAttrKeyClass: kSecAttrKeyClassPrivate,                              kSecPrivateKeyAttrs:[                    kSecAttrIsPermanent:"NO",                                       kSecAttrIsExtractable:"NO",                    kSecAttrCanDerive:"NO",                    kSecAttrIsSensitive:"YES"                ],            ] as NSDictionary, nil)!                       let keyDict = SecKeyCopyAttributes(keyTest)


extr attribute into keyDict is true.

Is there an option to set it to false?

I tried using SecItemImport

I’m confused. All the code you posted is using SecPKCS12Import rather than SecItemImport.

The reason I suggested SecItemImport is that it has a keyParams parameter of type SecItemImportExportKeyParameters, and that structure includes a keyAttributes array when you set kSecAttrIsExtractable to control extractability.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hi Quinn,

thank you for your time.

this is my example code:

Code Block swift  let keyString = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgQCVXkvZifORfS8VVEp+BQTrnXu2a2+KL+Rw5FPHeSAOXjrS5DoC0GxK29jTKPGkJrg2WOiL/ZSbKvTq8wBUZzoUGaJQ+kzBJ40ShVtbJYGNFixubuKrSjUNQB149t25lxHnJia0i9i1sLfzrPnqPJ4ABf7lnhkTbNt8g/KriwoqmQICAQE="            let keyData = NSData(base64Encoded: keyString, options: .ignoreUnknownCharacters)!            var hostError: Unmanaged<CFError>?            guard let hostPrivateKey = SecKeyCreateRandomKey(optionDict as CFDictionary, &hostError) else {                fatalError("😡 Could not generate the private key.")            }            let passphrase = "Password" as CFString            let keyDict1 = SecKeyCopyAttributes(hostPrivateKey)            var keyParams = SecItemImportExportKeyParameters()            keyParams.version = UInt32(SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION)            keyParams.passphrase = Unmanaged.passUnretained(passphrase)            /*            keyParams.keyAttributes=?? <---- ????            */            var inputFormat :SecExternalFormat = .formatPKCS12            var itemType :SecExternalItemType = .itemTypeUnknown            var itemsCFArray :CFArray? = nil            let error = SecItemImport(hostPrivateKey as! CFData , nil, &inputFormat, &itemType, SecItemImportExportFlags(rawValue: UInt32(0)), &keyParams, "login" as! SecKeychain, &itemsCFArray)


I should set the parameters on line 19, but I don't understand how to set them
Yeah, that requires some black belt level Swift / C integration (-: Try this:

Code Block  var keyParams = SecItemImportExportKeyParameters()let keyAttributes = [kSecAttrIsExtractable] as CFArraylet keyAttributesCF = Unmanaged.passRetained(keyAttributes)defer { keyAttributesCF.release() }keyParams.keyAttributes = keyAttributesCFlet err = SecItemImport(, &keyParams, )


The problem here is that SecItemImportExportKeyParameters is a C struct and thus Swift can’t reason about the lifetime of the object references within that structure. That’s why the keyAttributes property in Unmanaged; this is Swift’s way of making you do all the heavy lifting.

The fix is to create the array (line 2) and then create an unmanaged reference to it (line 3). You can then stash that unmanaged reference in the struct (line 5). Finally, the defer statement (line 4) cleans up the unmanaged reference at the end of this block.

IMPORTANT You’ll need to do the same thing for the keyParams.passphrase property (line 16 of your snippet). Right now you’re using passUnretained(_:), which doesn’t guarantee that the CFString stays around until the end of the block. Indeed, the last reference to passphrase is on line 16 so the Swift compiler is free to release it on line 17, leaving a dangling reference in the keyParams.passphrase property. These bugs are tricky because things tend to work in the Debug build and then fail in the Release build, where the optimiser works hard to shorten object lifespans.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hi Quinn, thank you for your invaluable support.

I have a more general question for you. Is it possible to disable the export of the private key from the keychain of the mac osx?

I have noticed that everything is exportable. This way I could import a certificate to another system

Is there any way to avoid this?


Is it possible to disable the export of the private key from the
keychain of the [macOS]?

Isn’t that what you get when you leave off kSecAttrIsExtractable? To quote the doc comment in <Security/SecImportExport.h>:

Note that if the caller provides a keyAttributes array but does not
set kSecAttrIsExtractable, this key will never be able to be
extracted from the keychain in any form, not even in wrapped form.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hi Quinn,

thanks for your suggestions.

I tried to change the code, but the key is always exportable.

This is the code


Code Block swift            var keyParams = SecItemImportExportKeyParameters()            let keyAttributes = [kSecAttrIsExtractable] as CFArray            let keyAttributesCF = Unmanaged.passRetained(keyAttributes)            defer { keyAttributesCF.release() }            keyParams.keyAttributes = keyAttributesCF            var itemsCFArray :CFArray? = nil            var inputFormat :SecExternalFormat = .formatPKCS12            var itemType :SecExternalItemType = .itemTypeUnknown            let error = SecItemImport(keyData as CFData, nil, &inputFormat, &itemType, [], &keyParams, nil, &itemsCFArray)            let errorString = SecCopyErrorMessageString(error, nil)


do you have any other advice for me?


do you have any other advice for me?

Not immediately. I’m going to recommend that you open a DTS tech support incident so I can dedicate some time to look at your specific situation in detail.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[AFAICT slackware1590 never did open a DTS case about this, so I didn’t get a chance to dig into it at the time.]

This came up today in a different context, and I wanted to share my results. Consider this small test program:

import Foundation
let benjyP12Bytes: [UInt8] = [
]
func main() throws {
let keychains = try secCall { SecKeychainCopySearchList($0) } as! [SecKeychain]
let scratchQ = keychains.first { kc in
kc.url?.deletingPathExtension().lastPathComponent == "Scratch"
}
let scratch = try secCall { scratchQ }
let identity: SecIdentity
do {
print("will import")
let benjyP12Data = Data(benjyP12Bytes)
var format = SecExternalFormat.formatPKCS12
var type = SecExternalItemType.itemTypeUnknown
var parameters = SecItemImportExportKeyParameters()
let passphrase = Unmanaged.passRetained("test" as NSString as AnyObject)
defer { passphrase.release() }
parameters.passphrase = passphrase
let keyAttributes = Unmanaged.passRetained([kSecAttrIsPermanent, kSecAttrIsSensitive] as CFArray)
defer { keyAttributes.release() }
parameters.keyAttributes = keyAttributes
let identities = try secCall { SecItemImport(
benjyP12Data as NSData,
"Benjy.p12" as NSString,
&format,
&type,
[],
&parameters,
scratch,
$0
) } as! [SecIdentity]
print("did import, identities: \(identities)")
identity = try secCall { identities.first }
} catch {
print("did not import, error: \(error)")
throw error
}
let privateKey = try secCall { SecIdentityCopyPrivateKey(identity, $0) }
let attributes = try secCall { SecKeyCopyAttributes(privateKey) } as! [String: Any]
for (attrName, attrValue) in attributes {
print("\(attrName): \(attrValue)")
}
}
try main()

Note This isn’t a complete program. At the end of this post I have more details on how I ran this test.

When I run this I see:

did import, identities: [<SecIdentity 0x600003823fc0 [0x20638e240]>]
extr: 0

extr is kSecAttrIsExtractable, and a value of 0 indicates that this private key is not extractable.

In contrast, if I comment out the line that sets parameters.keyAttributes and run it again, it prints:

did import, identities: [<SecIdentity 0x600000515280 [0x20638e240]>]
extr: 1

indicating that the key is extractable in that case.

I’m pretty sure that this is ‘case closed’, but if you have any follow-up questions please post them here.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


I ran my test on macOS 15.2 with Xcode 16.2.

I used Keychain Access to create a new keychain, called “Scratch”, for this test to target. That ensures that the imported digital identity doesn’t get lost in my main login keychain.

Between each test I used Keychain Access to delete the imported digital identity (well, the certificate and private key) from the “Scratch” keychain.

I’ve elided the PKCS#12 data in the above program because it’s quite long. To test this yourself:

  • Fill in benjyP12Bytes with the hex bytes of your PKCS#12 blob.

  • Replace test with its passphrase.

The program uses the helpers from Calling Security Framework from Swift

Finally, the code relies on one additional helper method:

extension SecKeychain {
var url: URL? {
var length: UInt32 = 1024
var buffer = [CChar](repeating: 0, count: Int(length))
let err = SecKeychainGetPath(self, &length, &buffer)
guard err == errSecSuccess else { return nil }
return URL(fileURLWithFileSystemRepresentation: buffer, isDirectory: false, relativeTo: nil)
}
}
Make PKCS12 not exportable from mac keychain
 
 
Q