Apple’s CryptoKit framework provides an easy-to-use interface to common cryptographic operations, including working with Secure Enclave keys. Signing data is easy enough, but turns out verifying signatures on the other side of the walled garden is easier said than done.
Continuing my recent obsession with iOS and its numerous frameworks, I’m working on another iOS app; this time, it’s interacting with a web service which requires devices to authenticate by signing challenges with a device key pair: a perfect excuse to play with Apple’s latest cryptographic library.
Say Hello to CryptoKit
CryptoKit has support for a great variety of algorithms: various hash functions, MACs, ciphers, public key cryptography, shared secret derivation, and key derivation. All of these are implemented in the most performant and energy-efficient form for the underlying hardware. It even provides an interface for generating wrapped keys, tied to the device’s Secure Enclave greatly enhancing security.
Best of all, the framework takes care of most of the hard stuff, such as ensuring memory is zeroed on deallocation to avoid leaking secrets, securely implementing crypto primitives, and so forth. And unlike the Security framework, all that comes with a wonderful Swift-y API.
For the sake of the example, we’ll stick with the P256 algorithm. Generating a private key is trivial, even if we want to take advantage of the enhanced security offered by the Secure Enclave:
1 2 3 4 // create an ephemeral key on the CPU for this operation let key = P256.Signing.PrivateKey() // or generate a key backed by the Secure Enclave (only supports P256 keys) let sepKey = SecureEnclave.P256.Signing.PrivateKey()
At the time of writing, Secure Enclave keys are limited to the P256 algorithm.
Regardless of the type of key, you can export the private key in various representations for later use. Apple has a handy guide on Storing CryptoKit Keys in the Keychain, which is probably the most likely option for key storage.
All private key types also feature a
publicKey property, which allows easy access to the corresponding public key. That public key can then be encoded and shared with a web service for authentication, for example.
An important thing to note is that the representation provided for Secure Enclave keys is not actually the key itself, but rather data that can be used by the enclave’s hardware to derive the same key again. If you rely on being able to export the key for use with other cryptographic libraries, including off-device. this is probably not what you want.
However, this type of key has several security benefits: for one, the actual raw key is never visible to application software. Additionally, the encoded data is useless on any other device, or this device after the user resets their data, as the key derivation algorithm includes per-device data.
With our generated private key, signing data is easy enough:
1 2 3 4 let stringToSign = "sign me daddy" let signBytes = stringToSign.data(using: .utf8) let signature = try key.signature(for: signBytes)
Unlike most other crypto APIs, where the result of a signature is usually just a blob, here we get back an
ECDSASignature struct. That allows us to either get the raw byte value of the signature or a DER1-encoded version, via the
I think the DER representations should be used whenever possible: they’re as compact as possible, while still containing enough information to allow unambiguous parsing. DER and ASN.1 parsers are probably one of the most well-tested pieces of software on the planet, so it seems silly to not want to benefit from that by using raw representations to save a few bytes.
The Dark Side of the Wall
On the server, we’re leaving the warm, cozy confines of the Apple ecosystem behind for ~ boring ~ server-side stuff like PHP. Which surely have well-established crypto libraries, where validating a signature is a single function call, since crypto is crypto, and it should work exactly the same everywhere… right?
Unfortunately, that is not the case.
For the code samples below, I’ll be using phpseclib since it exposes some features that the OpenSSL bindings don’t, or make difficult to get to.
As usual, error checks are omitted for clarity. You’ll want to add error checking in real-world use of this code.
First things first, you need to load a public key to use in verifying signatures. Sadly, the troubles start here already. Most key loading functions expect to read in PEM2 formatted keys, rather than a binary DER blob.
Thankfully, the conversion is simple:
1 2 3 4 5 // where $der is a binary string containing the public key $pem = chunk_split(base64_encode($der), 64, '\n'); $pem = "-----BEGIN PUBLIC KEY-----\n" . $pem . "-----END PUBLIC KEY-----\n"; // load key: may throw \phpseclib3\Exception\NoKeyLoadedException $key = PublicKeyLoader::load($pem);
This should be easy enough to translate to other languages; alternatively, your app can provide keys as PEM encoded (via the
pemRepresentation property on a key) string rather than DER blobs. However, if your underlying data store can handle binary data, there’s no reason to prefer PEM over DER.
Given a binary string DER-encoded signature, verifying it is very simple:
1 2 3 4 // specialize the key for the signature format CryptoKit uses $key = $key->withSignatureFormat('ASN1')->withHash('sha256'); // verify the signature ($inSignature, binary string) over $inData $isValid = $key->verify($inData, $inSignature);
Easy enough! But getting here was an immense pain in the ass, not only because documentation on anything crypto online seems to be… extremely lacking. The best description I found of how you verify a CryptoKit signature outside of CryptoKit was a StackOverflow question, which seems a little ridiculous for Apple to not have documented.
If you’re trying to implement this in another language, the key (pun definitely intended) details of the DER representation of a signature are as follows:
- It’s (obviously) DER-encoded ASN.1 data.
- CryptoKit will hash your input data using SHA-256 (or SHA-384 or SHA-512 for larger curves) unless you specifically pass a precomputed digest in.
Thankfully, that’s all there is to it.
I almost went insane trying to work out why I couldn’t get CryptoKit signatures to verify anywhere else: that certainly wasn’t helped by the resources out there (as of the time of writing) being nothing more than “this code snippet kind of worked.” Part of this is on Apple’s docs for not being clearer on how data is encoded, but it also seems that there’s a lot of incorrectly done crypto out in the wild.
Hopefully, despite being one of the shorter posts here, this helps someone avoid the same issues with CryptoKit that most people seem to be facing. It is a great framework that takes care of many of the tricky parts of writing crypto code for you and is definitely what developers ought to be using for any sort of cryptographic operations on any Apple platform in 2022 and beyond.
DER, or the Distinguished Encoding Rules are a way to represent ASN.1 structures as binary data. It’s widely used in X.509 (TLS certificates) and other crypto and crypto-adjacent fields. Check out this article for more details. ↩
PEM, or Privacy Enhanced Mail is a standard defining ASCII-compatible encoding for cryptographic keys, certificates, and more. It’s not much more than a pair of lines bracketing the base64-encoded DER blob. ↩