SecItemImport with PEM data

I'm trying to interact with a remote server that requires a client certificate. I have obtained the required info from a separate login tool from the same vendor. However, I am having no end of trouble connecting the dots.

Specifically I can't manage to get the needed SecIdentity from the raw PEM data.

Here is the code (with actual data copy/pasted from vendor, but truncated here):

Code Block
let pemString =
"""
-----BEGIN PRIVATE KEY-----
MIGHAg(key contents truncated)QS1osPzBH8
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDkT(cert contents trucated)1yIMCYx2E=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDWD(cert contents trucated)e19Jv799c=
-----END CERTIFICATE-----
"""
let pemData = pemString.data(using: .utf8)!
var inputFormat :SecExternalFormat = .formatUnknown
var itemType :SecExternalItemType = .itemTypeUnknown
var itemsCFArray :CFArray? = nil
let error = SecItemImport(pemData as CFData, nil, &inputFormat, &itemType, [], nil, nil, &itemsCFArray)
let errorString = SecCopyErrorMessageString(error, nil)


The error is always "Unknown format in import."

Any ideas why this is returning "Unknown format"?
I’m not sure whether SecItemImport can actually do this — it is a very cantankerous routine — but, if it’s going to work at all, you’re going to need to give it some more context. Specifically:
  • Pass a value like import.pem to the fileNameOrExtension parameter

  • Set inputFormat to .formatPEMSequence

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Hi Quinn, thanks so much for your relentless help with networking stuffs.

I have updated the code per your suggestions and am still getting the same result. The updated code is below.

So, perhaps I should back up and ask a bigger picture question (and please excuse my still learning the terms and meanings), the vendor's tool gives the application a private key as a base64 string and 2x certs also as base64 strings, which seem to be primed to become PEM data. In the end, I just need a URLCredential to pass to the URLSession through the delegate. What would be your recommendation as to the best way to connect the dots?

(Incidently, if I construct a info.pem file with the same contents as in the code and try to use openSSL to make a pkcs12 file, it borks with an error "unable to load private key".

I also believe these certs (?plus private key?) are not valid locally (using the Paw application, which does successfully make a URL call when configured to use URLSession and using this data, was showing the cert(s) as invalid) - does SecItemImport try to do any kind of validation and might that affect its result?

Code Block Swift
let pemString =
"""-----BEGIN PRIVATE KEY-----
MIGHAg(key contents truncated)QS1osPzBH8
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDkT(cert contents trucated)1yIMCYx2E=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDWD(cert contents trucated)e19Jv799c=
-----END CERTIFICATE-----
"""
let pemData = pemString.data(using: .utf8)!
var inputFormat :SecExternalFormat = .formatPEMSequence
var itemType :SecExternalItemType = .itemTypeUnknown
var itemsCFArray :CFArray? = nil
let error = SecItemImport(pemData as CFData, "import.pem" as CFString, &inputFormat, &itemType, [], nil, nil, &itemsCFArray)
let errorString = SecCopyErrorMessageString(error, nil)


does SecItemImport try to do any kind of validation and might that affect its result?

No. It validate the structure of what it imports but it does not trust evaluation.

What would be your recommendation as to the best way to connect the dots?

By far the easiest way to import a digital identity is as a PKCS\#12. If you have the option to post-process the data you get from the vendor, I’d convert it to PKCS\#12 and pass that to your app to import.

Apropos that:

if I construct a info.pem file with the same contents as in the code and try to use openSSL to make a pkcs12 file, it borks with an error "unable to load private key".

Well, that’s not good. It’s hard to say what’s going on without seeing the Base64 data in full. Can you post a throwaway example?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Why it just so happens I'm working on this from the Rust side and encountering the same error with the cert generated by mk-ca-bundle.pl

These certs don't even import via they keychain import tool.

However, they will import when in DER format; which format is useless for my remote app.
It looks like it doesn't like keychains, which I bet is an aspect of this Rust library. It will import PEM just fine: I imported a PEM file generated by letsencrypt.
I'm not an expert. It looks like the OP is using a keychain?
Quinn: I have some additional info from the vendor on the values being returned:

The private key is base64-encoded DER-encoded PKCS8 key.
The certificates are base64-encoded DER-encoded X.509 certs.

Does this help our SecItemImport situation at all? Or does the fact that they are different formats just throw us off a cliff?

I've subsequently updated the code to be able to create a SecKeyRef from the private key data and 2x SecCertificateRef from the 2x certificate datas, but now I'm at a loss as to how to get from there to a SecIdentity or URLCredential. Any thoughts?


Jchimene: No keychains involved actually. The whole system is being developed to not store anything on the machine at all.
Not sure this even has a hope of working, but I tried the following, but get the error "The specified item is no longer valid. It may have been deleted from the keychain.":

Code Block Swift
let passphrase = "abc" as CFString
var keyParams = SecItemImportExportKeyParameters()
keyParams.version = UInt32(SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION)
keyParams.passphrase = Unmanaged.passUnretained(passphrase)
var pkcs12DData :CFData? = nil
var status = SecItemExport([privateKey, certificateRef0, certificateRef1] as CFArray, .formatPKCS12, [], &keyParams, &pkcs12DData)


Do you have to deal with this data in this format? Can you munge it server side into something that’s easier to import? That’s going to be the easiest way forward.

If not, there’s some things to note:
  • Trying to important these items as a single unit is not going to work. You will need to important them separately and then form an identity from the results. This means that the private key will need to end up in the keychain.

  • Do you know if the PKCS#8 is wrapped or not? SecItemImport can deal with a wrapped PKCS#8 via kSecFormatWrappedPKCS8.

  • If the PKCS#8 is not wrapped, you should try kSecFormatBSAFE.

The last two points assume it’s an RSA key. The situation with EC keys is much muddier (r. 59424536).

ps I’d still like to see an example of the actual data you’re trying to import. Right now I’m just guessing )-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
I've sent sample data to you via email.

I can circle back to the vendor to see if they can adjust their tool to export a different format. It is a command line tool that just writes the data out to stdout, which is captured and subsequently handled by a macOS GUI application. Is there a particular format that would be best?

I have been able to successfully create a SecKeyRef and 2x SecCertificateRef with the following code:

Code Block Swift
let priKeyECData = Data(base64Encoded: privateKey, options: Data.Base64DecodingOptions.ignoreUnknownCharacters)!
let priKeyECStriped = priKeyECData[priKeyECData.count-65..<priKeyECData.count] + priKeyECData[36..<68]
let attributes :[String : Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
kSecAttrKeySizeInBits as String: 256,
kSecAttrIsPermanent as String: false
]
var error :Unmanaged<CFError>? = nil
let privateKey = SecKeyCreateWithData(Data(bytes: priKeyECStriped) as CFData, attributes as CFDictionary, &error)
let certificateRef0 =
SecCertificateCreateWithData(kCFAllocatorDefault,
Data(base64Encoded: certificateChain[0])! as CFData)
let certificateRef1 =
SecCertificateCreateWithData(kCFAllocatorDefault,
Data(base64Encoded: certificateChain[1])! as CFData)

I was hoping to avoid the keychain, but if it has to be involved in the process, I suppose can just remove from the keychain anything added once the SecIdentity/URLCredential has been created?

Their sample is a python script, which works great of course, and I've pasted the relevant portion here in case it is at all helpful.

Code Block python
with open(cert_file, "w") as f:
for cert in auth_info["CertificateChain"]:
f.write("-----BEGIN CERTIFICATE-----\n")
f.write(f"{cert}\n")
f.write("-----END CERTIFICATE-----\n")
with open(key_file, "w") as f:
f.write("-----BEGIN PRIVATE KEY-----\n")
f.write(f"{auth_info['PrivateKey']}\n")
f.write("-----END PRIVATE KEY-----\n")
r = requests.get(
"{HIDDEN}",
cert=(cert_file, key_file),
verify=False,
)


StevoGTA sent me a copy of the credentials in question. To start, I decoded the Base64 of the private key and this is what I got:

Code Block
% dumpasn1 -p private.asn1
SEQUENCE {
INTEGER 0
SEQUENCE {
OBJECT IDENTIFIER ecPublicKey (1 2 840 10045 2 1)
OBJECT IDENTIFIER prime256v1 (1 2 840 10045 3 1 7)
}
OCTET STRING, encapsulates {
SEQUENCE {
INTEGER 1
OCTET STRING
… 32 bytes of K …
[1] {
BIT STRING
04 … 32 bytes of X then 32 bytes of Y …
}
}
}
}


This is a PKCS#8 EC key. Unfortunately, SecItemImport is not able to deal with that (r. 59424536).

If you extract the 04, X, Y and K values from the key you can create a SecKey object from that data using SecKeyCreateWithData.

Note Slicing out byte ranges is generally not safe in the context of ASN.1 but in this case it’s probably OK.

In contrast, the certificates are just Base64 encoded binary certificates, so importing those is trivial.

The problem here is forming the identity. It’s not possible to construct a SecIdentity object from an in-memory private key and its matching certificate. Your options are:
  • Add the private key to the keychain and then use SecIdentityCreateWithCertificate (this is a Mac-only routine, FYI).

  • Add both to the keychain and then get the identity using SecItemCopyMatching.

  • Give up on this approach and use a PKCS#12, which you can import directly.



Is there a particular format that would be best?

Yes, PKCS#12.

I suppose can just remove from the keychain anything added once the SecIdentity/URLCredential has been created?

That’ll probably work if you use the iOS-style keychain. If you use the file-based keychain, I suspect that deleting the private key from the keychain will ‘break’ your identity.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Thanks again Quinn so much for your help.

Turns out we were able to get the client to modify their tool to export PKCS12 data which is importing easily enough.

Now we're on to other issues getting a basic transaction to go.

do you know why SecItemImport was removed from the API?

SecItemImport is as available as it’s ever been; see the docs here.

Are you trying to use it on an iOS-based platform? If so then, yeah, that won’t work. This call is not available on iOS.

If you’re working on the Mac then you should check that you’ve imported Security framework.

Share and Enjoy

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

SecItemImport with PEM data
 
 
Q