I'm trying to export and re-import a P-256 private key that was originally generated via SecKeyCreateRandomKey(), but I keep running into roadblocks. The key is simply exported via SecItemExport() with format formatWrappedPKCS8, and I did set a password just to be sure.
Do note that I must use the file-based keychain, as the data protection keychain requires a restricted entitlement and I'm not going to pay a yearly fee just to securely store some private keys for a personal project. The 7-day limit for unsigned/self-signed binaries isn't feasible either.
Here's pretty much everything I could think of trying:
-
Simply using
SecItemImport()does import the key, but I cannot setkSecAttrLabeland more importantly:kSecAttrApplicationTag. There just isn't any way to pass these attributes upfront, so it's always imported asImported Private Keywith an empty comment. Keys don't support many attributes to begin with and I need something that's unique to my program but shared across all the relevant key entries, otherwise it's impossible to query for only my program's keys.kSecAttrLabelis already used for something else and is always unique, which really only leaveskSecAttrApplicationTag. I've already accepted that this can be changed via Keychain Access, as this attribute should end up as the entry's comment. At least, that's how it works withSecKeyCreateRandomKey()andSecItemCopyMatching(). I'm trying to get that same behaviour for imports. -
Running
SecItemUpdate()afterwards to set these 2 attributes doesn't work either, as now thekSecAttrApplicationTagis suddenly used for the entry's label instead of the comment. Even settingkSecAttrComment(just to be certain) doesn't change the comment. I thinkkSecAttrApplicationTagmight be a creation-time attribute only, and sinceSecItemImport()already created a SecKey I will never be able to set this. It likely falls back to updating the label because it needs to target something that is still mutable? -
Using
SecItemImport()with a nil keychain (i.e. create a transient key), then persisting that withSecItemAdd()viakSecValueRefdoes allow me to set the 2 attributes, but now the ACL is lost. Or more precise: the ACL does seem to exist as any OS prompts do show the label I originally set for the ACL, but in Keychain Access it shows asAllow all applications to access this item. I'm looking to enableConfirm before allowing accessand add my own program to theAlways allow access by these applicationslist. Private keys outright being open to all programs is of course not acceptable, and I can indeed access them from other programs without any prompts. -
Changing the ACL via
SecKeychainItemSetAccess()afterSecItemAdd()doesn't seem to do anything. It apparently succeeds but nothing changes. I also reopened Keychain Access to make sure it's not a UI "caching" issue. -
Creating a transient key first, then getting the raw key via
SecKeyCopyExternalRepresentation()and passing that toSecItemAdd()viakSecValueDataresults in The specified attribute does not exist. This error only disappears if I remove almost all of the attributes. I can pass onlykSecValueData,kSecClassandkSecAttrApplicationTag, but then I getThe specified item already exists in the keychainerrors. I found a doc that explains what determines uniqueness, so here are the rest of the attributes I'm using forSecItemAdd():kSecClass: not mentioned as part of the primary key but still required, otherwise you'll getOne or more parameters passed to a function were not valid.kSecAttrLabel: needed for my use case and not part of the primary key either, but as I said this results inThe specified attribute does not exist.kSecAttrApplicationLabel:The specified attribute does not exist. As I understand it this should be the SHA1 hash of the public key, passed asData. Just omitting it would certainly be an option if the other attributes actually worked, but right now I'm passing it to try and construct a truly unique primary key.kSecAttrApplicationTag:The specified item already exists in the keychain.kSecAttrKeySizeInBits:The specified attribute does not exist.kSecAttrEffectiveKeySize:The specified attribute does not exist.kSecAttrKeyClass:The specified attribute does not exist.kSecAttrKeyType:The specified attribute does not exist.
It looks like only
kSecAttrApplicationTagis accepted, but still ignored for the primary key. Even entering something that is guaranteed to be unique still results inThe specified item already exists in the keychain, so I think might actually be targeting literally any key. I decided to create a completely new keychain and import it there (which does succeed), but the key is completely broken. There's noKindandUsageat the top of Keychain Access and the table view just below it showssymmetric keyinstead ofprivate. ThekSecAttrApplicationTagI'm passing is still being used as the label instead of the comment and there's no ACL. I can't even delete this key because Keychain Access complains thatA missing value was detected. It seems like the key doesn't really contain anything unique for its primary key, so it will always match any existing key. -
Using
SecKeyCreateWithData()and then using that key as thekSecValueRefforSecItemAdd()results inA required entitlement isn't present. I also have to addkSecUseDataProtectionKeychain: falsetoSecItemAdd()(even though that should already be the default) but then I getThe specified item is no longer valid. It may have been deleted from the keychain. This occurs even if I decrypt the PKCS8 manually instead of viaSecItemImport(), so it's at least not like it's detecting the transient key somehow. No combination ofkSecAttrIsPermanent,kSecUseDataProtectionKeychainandkSecUseKeychainon eitherSecKeyCreateWithData()orSecItemAdd()changes anything. -
I also tried PKCS12 despite that it always expects an "identity" (key + cert), while I only have (and need) a private key. Exporting as
formatPKCS12and importing it withitemTypeAggregate(oritemTypeUnknown) does import the key, and now it's only missing thekSecAttrApplicationTagas the original label is automatically included in the PKCS12. TheoutItemsparameter contains an empty list though, which sort of makes sense because I'm not importing a full "identity". I can at least target the key bykSecAttrLabelforSecItemUpdate(), but any attempt to update the comment once again changes the label so it's not really any better than before. -
SecPKCS12Import()doesn't even import anything at all, even though it does return errSecSuccess while also passingkSecImportExportKeychainexplicitly.
Is there literally no way?
First up, you’re correct that there’s a contradiction here:
- Apple has effectively deprecated the file-base keychain in favour of the data protection keychain.
- But the Personal Team limits, which come from iOS, run counter to the expectations of macOS developers.
I don’t have any answers for you on that front, but if you file a bug describing this contradiction and post the bug number here, I’ll make sure that the relevant folks see it.
Coming back to your real issue, there’s a lot to unpack there and I don’t have time to dig into it all. However, you seem to have bumped into a pretty fundamental limitation:
- You want the system to protect your item such that only your program can access it.
- But you’re not giving it any code-signing credentials to do that.
Something has to give.
The file-based keychain has a bunch of legacy access control centred around code-signing requirements — see TN3127 Inside Code Signing: Requirements — so that’s one thing you might explore. But that still requires you to have a stable code-signing identity [1]. Historically it was feasible to use a code-signing identity whose certificate was issued by a non-Apple CA, but that’s not really an options these days.
I think Personal Team will actually help here. While its provisioning profiles are limited to 7 days, its certificates actually have a lifetime of a year [2]. The profile limit means you’re still stuck on the file-based keychain, but the long-lived certificate should help with the keychain because:
- It gives your program an Apple-issued stable code-signing identity.
- It also means your program is signed with a Team ID, which is critical for the keychain partition mechanism.
So my initial recommendation is that you try to get some basic file-base keychain code working:
- Create an app that reads and writes a keychain item.
- Sign it using your Personal Team.
- Check that the resulting keychain item has a reasonable ACL.
- Check that, after updating your app [3], it can still access the keychain item created by the previous version.
- Check that other third-party programs can’t access it.
A good way to do that last bit is to create a second Apple Account, and hence a second Personal Team, and clone your test app but this time use the second Personal Team.
I think that’ll all work, but please let me know if it doesn’t.
And if it does work, you can return to your key import/export issue.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] The file-based keychain is old enough that it has an even older code identity mechanism, one that predates code signing. I think that’s part of the reason why you’re tests ran into so many problems, because you’re either not signing, or ad hoc signing, your program.
[1] Indeed, my testing today suggests that macOS doesn’t actually check for certificate expiration unless there’s a profile involved. Here’s what I did:
- I ran Xcode 26.1 in a macOS 15.6 in a VM hosted by macOS 15.7.1.
- I logged in with my Personal Team.
- I created a test app, configured it to use my Personal Team for signing, and built and ran the app.
- I turned off the VM’s network interface.
- I restarted the VM, to clear any in-memory state.
- Once the VM was back up, I used System Settings to set the clock far into the future.
- In the Finder, I launched the built copy of my test app. It ran just fine, even though its code-signing certificate had long expired.
Note that this test app has no restricted entitlement claims. When I repeated this test with a new app that had a capability backed by a restricted entitlement — based on Developer Account Help > Reference > Supported capabilities (macOS), the only option is Maps — the app’s execution was blocked by the trusted execution system in step 6.
[3] Such that its cdhash changes.