This post is an extension to Importing Cryptographic Keys that covers one specific common case: importing a PEM-based RSA private key and its certificate to form a digital identity.
If you have questions or comments, start a new thread in Privacy & Security > General. Tag your thread with Security so that I see it.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Importing a PEM-based RSA Private Key and its Certificate
I regularly see folks struggle to import an RSA private key and its corresponding certificate. Importing Cryptographic Keys outlines various options for importing keys, but in this post I want to cover one specific case, namely, a PEM-based RSA private key and its corresponding certificate. Together these form a digital identity, represented as a SecIdentity
object.
IMPORTANT If you can repackage your digital identity as a PKCS#12, please do. It’s easy to import that using SecPKCS12Import
. If you can switch to an elliptic curve (EC) private key, please do. It’s generally better and Apple CryptoKit has direct support for importing an EC PEM.
Assuming that’s not the case, let’s explore how to import a PEM-base RSA private key and its corresponding certificate to form a digital identity.
Note The code below was built with Xcode 16.2 and tested on the iOS 18.2 simulator. It uses the helper routines from Calling Security Framework from Swift.
This code assumes the data protection keychain. If you’re targeting macOS, add kSecUseDataProtectionKeychain
to all the keychain calls. See TN3137 On Mac keychain APIs and implementations for more background to that.
Unwrap the PEM
To start, you need to get the data out of the PEM:
/// Extracts the data from a PEM.
///
/// As PEM files can contain a large range of data types, you must supply the
/// expected prefix and suffix strings. For example, for a certificate these
/// are `"-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`.
///
/// - important: This assumes the simplest possible PEM format. It does not
/// handle metadata at the top of the PEM or PEMs with multiple items in them.
func dataFromPEM(_ pem: String, _ expectedPrefix: String, _ expectedSuffix: String) -> Data? {
let lines = pem.split(separator: "\n")
guard
let first = lines.first,
first == expectedPrefix,
let last = lines.last,
last == expectedSuffix
else { return nil }
let base64 = lines.dropFirst().dropLast().joined()
guard let data = Data(base64Encoded: base64) else { return nil }
return data
}
IMPORTANT Read the doc comment to learn about some important limitations with this code.
Import a Certificate
When adding a digital identity to the keychain, it’s best to import the certificate and the key separately and then add them to the keychain. That makes it easier to track down problems you encounter.
To import a PEM-based certificate, extract the data from the PEM and call SecCertificateCreateWithData
:
/// Import a certificate in PEM format.
///
/// - important: See ``dataFromPEM(_:_:_:)`` for some important limitations.
func importCertificatePEM(_ pem: String) throws -> SecCertificate {
guard
let data = dataFromPEM(pem, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"),
let cert = SecCertificateCreateWithData(nil, data as NSData)
else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecParam), userInfo: nil) }
return cert
}
Here’s an example that shows this in action:
let benjyCertificatePEM = """
-----BEGIN CERTIFICATE-----
MIIC4TCCAcmgAwIBAgIBCzANBgkqhkiG9w0BAQsFADAfMRAwDgYDVQQDDAdNb3Vz
ZUNBMQswCQYDVQQGEwJHQjAeFw0xOTA5MzAxNDI0NDFaFw0yOTA5MjcxNDI0NDFa
MB0xDjAMBgNVBAMMBUJlbmp5MQswCQYDVQQGEwJHQjCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAOQe5ai68FQhTVIgpsDK+UOPIrgKzqJcW+wwLnJRp6GV
V9EmifJq7wjrXeqmP1XgcNtu7cVhDx+/ONKl/8hscak54HTQrgwE6mK628RThld9
BmZoOjaWWCkoU5bH7ZIYgrKF1tAO5uTAmVJB9v7DQQvKERwjQ10ZbFOW6v8j2gDL
esZQbFIC7f/viDXLsPq8dUZuyyb9BXrpEJpXpFDi/wzCV3C1wmtOUrU27xz4gBzi
3o9O6U4QmaF91xxaTk0Ot+/RLI70mR7TYa+u6q7UW/KK9q1+8LeTVs1x24VA5csx
HCAQf+xvMoKlocmUxCDBYkTFkmtyhmGRN52XucHgu0kCAwEAAaMqMCgwDgYDVR0P
AQH/BAQDAgWgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUA
A4IBAQAyrArH7+IyHTyEOrv/kZr3s3h4HWczSVeiO9qWD03/fVew84J524DiSBK4
mtAy3V/hqXrzrQEbsfyT7ZhQ6EqB/W0flpVYbku10cSVgoeSfjgBJLqgJRZKFonv
OQPjTf9HEDo5A1bQdnUF1y6SwdFaY16lH9mZ5B8AI57mduSg90c6Ao1GvtbAciNk
W8y4OTQp4drh18hpHegrgTIbuoWwgy8V4MX6W39XhkCUNhrQUUJk3mEfbC/yqfIG
YNds0NRI3QCTJCUbuXvDrLEn4iqRfbzq5cbulQBxBCUtLZFFjKE4M42fJh6D6oRR
yZSx4Ac3c+xYqTCjf0UdcUGxaxF/
-----END CERTIFICATE-----
"""
print(try? importCertificatePEM(benjyCertificatePEM))
If you run this it prints:
Optional(<cert(0x11e304c10) s: Benjy i: MouseCA>)
Import a Private Key
To import a PEM-base RSA private key, extract the data from the PEM and call SecKeyCreateWithData
:
/// Import an 2048-bit RSA private key in PEM format.
///
/// Don’t use this code if:
///
/// * If you can switch to an EC key. EC keys are generally better and, for
/// this specific case, there’s support for importing them in Apple CryptoKit.
///
/// * You can switch to using a PKCS#12. In that case, use the system’s
/// `SecPKCS12Import` routine instead.
///
/// - important: See ``dataFromPEM(_:_:_:)`` for some important limitations.
func importRSA2048PrivateKeyPEM(_ pem: String) throws -> SecKey {
// Most private key PEMs are in PKCS#8 format. There’s no way to import
// that directly. Instead you need to strip the header to get to the
// `RSAPrivateKey` data structure encapsulated within the PKCS#8. Doing that
// in the general case is hard. In the specific case of an 2048-bit RSA
// key, the following hack works.
let rsaPrefix: [UInt8] = [
0x30, 0x82, 0x04, 0xBE, 0x02, 0x01, 0x00, 0x30,
0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7,
0x0D, 0x01, 0x01, 0x01, 0x05, 0x00, 0x04, 0x82,
0x04, 0xA8,
]
guard
let pkcs8 = dataFromPEM(pem, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"),
pkcs8.starts(with: rsaPrefix)
else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecParam), userInfo: nil) }
let rsaPrivateKey = pkcs8.dropFirst(rsaPrefix.count)
return try secCall { SecKeyCreateWithData(rsaPrivateKey as NSData, [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
] as NSDictionary, $0) }
}
IMPORTANT This code only works with 2048-bit RSA private keys. The comments explain more about that limitation.
Here’s an example that shows this in action:
let benjyPrivateKeyPEM = """
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDkHuWouvBUIU1S
IKbAyvlDjyK4Cs6iXFvsMC5yUaehlVfRJonyau8I613qpj9V4HDbbu3FYQ8fvzjS
pf/IbHGpOeB00K4MBOpiutvEU4ZXfQZmaDo2llgpKFOWx+2SGIKyhdbQDubkwJlS
Qfb+w0ELyhEcI0NdGWxTlur/I9oAy3rGUGxSAu3/74g1y7D6vHVGbssm/QV66RCa
V6RQ4v8MwldwtcJrTlK1Nu8c+IAc4t6PTulOEJmhfdccWk5NDrfv0SyO9Jke02Gv
ruqu1FvyivatfvC3k1bNcduFQOXLMRwgEH/sbzKCpaHJlMQgwWJExZJrcoZhkTed
l7nB4LtJAgMBAAECggEBAKOPF6ED776SZgrliEog/dmXrhABB6jXybytyw+CRkuP
dXhrRmr+isZ9Y0gTzMN4+dILVgW4EozzoP0/sgZ04oWwDqQS30eU2qzRRzMbo+3k
oYsZXeu3nhxcYppwXIDsfAEd/ygMFzaadRPKYhrFykR2rA/dpLYCvW2tfm5SuULp
RxnKykFlVi8yVT64AovVm0XGOy/QTO5BBbUdftvZY9QCjGn/IEL8QFEz0rxZsb2L
s0HgVMUcB1My38RksZQRKLMWCtqLqWnez3oCnPka+dxFQj5RU//vNtRoVh1ExbmW
txHz48v00AKQvaudC4ujIspZlY8+UPdYQT0TNjhsfoUCgYEA+7yEvyCgRtYwUNm6
jHTg67LoSldHwENOry63qGZp3rCkWBkPXle7ulgRtuw+e11g4MoMMAgkIGyIGB/Z
6YvnQGmJCTMw+HHIyw3k/OvL1iz4DM+QlxDuD79Zu2j2UIL4maDG0ZDskiJujVAf
sFOy4r36TvYedmd7qgh9pgpsFl8CgYEA5/v8PZDs2I1wSDGllGfTr6aeQcxvw98I
p8l/8EV/lYpdKQMFndeFZI+dnJCcTeBbeXMmPNTAdL5gOTwDReXamIAdr93k7/x6
iKMHzBrpQZUMEhepSd8zdR1+vLvyszvUU6lvNXcfjwbu7gJQkwbA6kSoXRN+C1Cv
i5/w66t0f1cCgYBt02FWwTUrsmaB33uzq4o1SmhthoaXKsY5R3h4z7WAojAQ/13l
GwGb2rBfzdG0oJiTeZK3odWhD7iQTdUUPyU0xNY0XVEQExQ3AmjUr0rOte/CJww9
2/UAicrsKG7N0VYEMFCNPVz4pGz22e35T4rLwXZi3J2NqrgZBntK5WEioQKBgEyx
L4ii+sn0qGQVlankUUVGjhcuoNxeRZxCrzsdnrovTfEbAKZX88908yQpYqMUQul5
ufBuXVm6/lCtmF9pR8UWxbm4X9E+5Lt7Oj6tvuNhhOYOUHcNhRN4tsdqUygR5XXr
E8rXIOXF4wNoXH7ewrQwEoECyq6u8/ny3FDtE8xtAoGBALNFxRGikbQMXhUXj7FA
lLwWlNydCxCc7/YwlHfmekDaJRv59+z7SWAR15azhbjqS9oXWJUQ9uvpKF75opE7
MT0GzblkKAYu/3uhTENCjQg+9RFfu5w37E5RTWHD2hANV0YqXUlmH3d+f5uO0xN7
7bpqwYuYzSv1hBfU/yprDco6
-----END PRIVATE KEY-----
"""
print(try? importRSA2048PrivateKeyPEM(benjyPrivateKeyPEM))
If you run this it prints:
Optional(<SecKeyRef algorithm id: 1, key type: RSAPrivateKey, version: 4, 2048 bits (block size: 256), addr: 0x600000c5ce50>)
Form a Digital Identity
There are two common ways to form a digital identity:
-
SecPKCSImport
-
SecItemCopyMatching
SecPKCSImport
is the most flexible because it gives you an in-memory digital identity. You can then choose to add it to the keychain or not. However, it requires a PKCS#12 as input. If you’re starting out with separate private key and certificate PEMs, you have to use SecItemCopyMatching
.
Note macOS also has SecIdentityCreateWithCertificate
, but it has some seriously limitations. First, it’s only available on macOS. Second, it requires the key to be in the keychain. If you’re going to add the key to the keychain anyway, you might as well use SecItemCopyMatching
.
To form a digital identity from a separate private key and certificate:
-
Add the certificate to the keychain.
-
Add the private key to the keychain.
-
Call
SecItemCopyMatching
to get back a digital identity.
Here’s an example of that in action:
/// Imports a digital identity composed of separate certificate and private key PEMs.
///
/// - important: See ``dataFromPEM(_:_:_:)`` for some important limitations.
/// See ``importRSA2048PrivateKeyPEM(_:)`` for alternative strategies that are
/// much easier to deploy.
func addRSA2048DigitalIdentityPEMToKeychain(certificate: String, privateKey: String) throws -> SecIdentity {
// First import the certificate and private key. This has the advantage in
// that it triggers an early failure if the data is in the wrong format.
let certificate = try importCertificatePEM(certificate)
let privateKey = try importRSA2048PrivateKeyPEM(privateKey)
// Check that the private key matches the public key in the certificate. If
// not, someone has given you bogus credentials.
let certificatePublicKey = try secCall { SecCertificateCopyKey(certificate) }
let publicKey = try secCall { SecKeyCopyPublicKey(privateKey) }
guard CFEqual(certificatePublicKey, publicKey) else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecPublicKeyInconsistent))
}
// Add the certificate first. If that fails — and the most likely error is
// `errSecDuplicateItem` — we want to stop immediately.
try secCall { SecItemAdd([
kSecValueRef: certificate,
] as NSDictionary, nil) }
// The add the private key.
do {
try secCall { SecItemAdd([
kSecValueRef: privateKey,
] as NSDictionary, nil) }
} catch let error as NSError {
// We ignore a `errSecDuplicateItem` error when adding the key. It’s
// possible to have multiple digital identities that share the same key,
// so if you try to add the key and it’s already in the keychain then
// that’s fine.
guard error.domain == NSOSStatusErrorDomain, error.code == errSecDuplicateItem else {
throw error
}
}
// Finally, search for the resulting identity.
//
// I originally tried querying for the identity based on the certificate’s
// attributes — the ones that contribute to uniqueness, namely
// `kSecAttrCertificateType`, `kSecAttrIssuer`, and `kSecAttrSerialNumber` —
// but that failed for reasons I don't fully understand (r. 144152660). So
// now I get all digital identities and find the one with our certificate.
let identities = try secCall { SecItemCopyMatching([
kSecClass: kSecClassIdentity,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnRef: true,
] as NSDictionary, $0) } as! [SecIdentity]
let identityQ = try identities.first { i in
try secCall { SecIdentityCopyCertificate(i, $0) } == certificate
}
return try secCall(Int(errSecItemNotFound)) { identityQ }
}
IMPORTANT This code is quite subtle. Read the comments for an explanation as to why it works the way it does.
Further reading
For more information about the APIs and techniques used above, see:
Finally, for links to documentation and other resources, see Security Resources.
Revision History
-
2025-02-13 Added code to check for mismatched private key and certificate.
-
2025-02-04 First posted.