Skip to content
ecdsa.com
Swift

Apple CryptoKit

Apple CryptoKit, the Swift crypto framework built into iOS and macOS: ECDSA signatures that are both raw and DER, Ed25519 signatures that are randomized, ML-KEM, ML-DSA and X-Wing since the 26 releases, and Secure Enclave keys that the API exposes only as opaque handles.
License
Proprietary (part of Apple's operating systems)
Checked version
macOS 27.0.1 ·
Source
developer.apple.com/documentation/cryptokit
Documentation
developer.apple.com/documentation/cryptokit

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-256P-384P-521
EdDSA
Ed25519
Post-quantum algorithms
ML-KEMML-DSAX-Wing
Public key formats
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32
Private key formats
pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32hardware-handle

CryptoKit is the framework Swift apps on Apple platforms use for hashing, signatures, key agreement, AEAD and HPKE. It ships with the operating system, so its version is the OS version; the SDK marks P-256 as available from iOS 13 and macOS 10.15 and ML-KEM, ML-DSA and X-Wing from iOS 26 and macOS 26. The facts here were checked on macOS 27.0.1 with Swift 6.4 on an Apple-silicon Mac, and both samples were compiled and run there; iOS was not checked. On Linux the same API comes from swift-crypto.

A P-256 signature is an ECDSASignature value with two forms: rawRepresentation, the 64-byte r‖s used by JWS (ES256) and WebCrypto, and derRepresentation, the DER form X.509, OpenSSL and Java expect. Sending the wrong form is a common interop bug; the sample converts explicitly, and the DER ⇄ raw converter shows both. Parsing is strict and throws: raw bytes given to init(derRepresentation:) throw a CryptoKitASN1Error — truncatedASN1Field for the RFC 6979 test signature, though the case depends on the bytes — and DER given to init(rawRepresentation:) throws incorrectParameterSize, while isValidSignature simply returns false.

Nonces are random — two signatures of one message differed, and the RFC 6979 test key did not produce the RFC's r — and s is not normalized: high-S values occur and a signature with s replaced by n − s verified. signature(for: data) hashes with the hash that matches the curve (SHA-256, SHA-384, SHA-512), but signature(for: digest) accepts any digest type: a SHA-256 digest signed and verified on a P-384 key, and a SHA-512 digest on P-256 was truncated to its first 32 bytes, which Security.framework confirmed. Pass the matching digest yourself when you prehash.

Ed25519 (Curve25519.Signing) signatures are randomized: with the RFC 8032 test key, CryptoKit's signature of the test message differed from the RFC's and from its own previous one, yet both verified, also with Node.js. Tests that compare signature bytes with RFC 8032 vectors therefore fail with CryptoKit on macOS; verify signatures instead. Non-canonical S values are rejected, but the identity point was accepted as a public key, and a signature with R = identity and S = 0 verified for an arbitrary message. Reject small-order keys yourself when a signature must bind to one specific signer. Ed25519 keys exist only as 32 raw bytes; there is no PEM or DER form for them.

The post-quantum types are ML-KEM-768 and ML-KEM-1024, ML-DSA-65 and ML-DSA-87 — there is no ML-KEM-512, ML-DSA-44 or SLH-DSA — and the hybrid X-Wing KEM (XWingMLKEM768X25519), also offered as an HPKE ciphersuite. ML-DSA signing is hedged and accepts a FIPS 204 context string, which the verifier must repeat. ML-KEM decapsulation of a damaged ciphertext returns a different key instead of throwing. Private keys are stored only as seeds — 64 bytes for ML-KEM, 32 for ML-DSA — or the seed with an integrity check, so an expanded private key exported by another library cannot be imported. Try the schemes in the ML-KEM tool and the post-quantum signature tool.

SecureEnclave.P256.Signing.PrivateKey() creates a key inside the Secure Enclave, and on this Mac so did the ML-KEM and ML-DSA types under SecureEnclave. These types expose no raw, DER or PEM private key — only dataRepresentation, a handle that reopened the key — while the public key is an ordinary P256.Signing.PublicKey or MLDSA65.PublicKey. Software P-256 keys export as PKCS#8 (derRepresentation, pemRepresentation) and import from PKCS#8 and SEC1, PEM or DER, but not from encrypted PKCS#8, and CryptoKit has no JWK support.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/apple-cryptokit/run.sh ran Apple CryptoKit macOS 27.0.1 on macOS 27.0.1 (Swift 6.4) on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    structured

    ECDSASignature has a 64-byte rawRepresentation and a derRepresentation; its bytes (withUnsafeBytes) are the raw form.

    Recorded by the check
    fact ecdsa-signature-encoding = structured
    evidence ecdsa-signature-encoding: signature(for:) returns P256.Signing.ECDSASignature: rawRepresentation 64 bytes, derRepresentation starts with 0x30 and converts back: yes, withUnsafeBytes yields the raw form: yes
  • ECDSA nonce

    random

    Two signatures of one message differed, and the RFC 6979 A.2.5 key and message did not produce the RFC's r.

    Recorded by the check
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two signatures of one message are different; r for the RFC 6979 A.2.5 key and message equals the RFC's r: no
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: signatures with s > n/2 among 64: yes
  • High-S when verifying

    accepts-high-s

    A signature with s replaced by n − s verified.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: isValidSignature with s replaced by n - s: true
  • What the signing call hashes

    hashes-message-or-digest

    signature(for: data) hashes; signature(for: SHA256.hash(data: message)) verified against the message.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: signature(for: SHA256.hash(data: message)) verifies with isValidSignature(_:for: message): yes
  • Hash applied to messages

    curve-matched

    Signatures over data verified as signatures over SHA-256 on P-256, SHA-384 on P-384 and SHA-512 on P-521.

    Recorded by the check
    fact ecdsa-message-hash = curve-matched
    evidence ecdsa-message-hash: signature(for: data) verifies as a signature over SHA-256 on P-256: yes, SHA-384 on P-384: yes, SHA-512 on P-521: yes
  • Hash bound to the curve

    none

    A P-384 key signed a SHA-256 digest, and the signature verified.

    Recorded by the check
    fact ecdsa-hash-curve-binding = none
    evidence ecdsa-hash-curve-binding: a P-384 key signed a SHA-256 digest, and the signature verifies: yes
  • Digest longer than the curve order

    truncated

    A P-256 key signed a SHA-512 digest; Security.framework verified the signature over the digest's first 32 bytes.

    Recorded by the check
    fact ecdsa-long-input = truncated
    evidence ecdsa-long-input: a P-256 key signed a SHA-512 digest; Security.framework verifies the signature over the digest's first 32 bytes: yes
  • Malformed ECDSA signature

    throws

    init(derRepresentation:) threw truncatedASN1Field for a raw r‖s signature and for 3 bytes; init(rawRepresentation:) threw incorrectParameterSize for DER and for 3 bytes.

    Recorded by the check
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: ECDSASignature(derRepresentation:) with the RFC 6979 A.2.5 signature as raw r||s: threw CryptoKitASN1Error.truncatedASN1Field; with 3 bytes: threw CryptoKitASN1Error.truncatedASN1Field
    evidence ecdsa-malformed-signature: ECDSASignature(rawRepresentation:) with DER: threw CryptoKitError.incorrectParameterSize; with 3 bytes: threw CryptoKitError.incorrectParameterSize
    evidence ecdsa-malformed-signature: isValidSignature for a valid signature over another message: false
  • ECDSA curves

    P-256P-384P-521

    P-256, P-384 and P-521 signed and verified; the SDK declares no other ECDSA curve.

    Recorded by the check
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: SDK 27.0 CryptoKit.swiftinterface top-level key types: Curve25519 MLDSA65 MLDSA87 MLKEM1024 MLKEM768 P256 P384 P521 XWingMLKEM768X25519
  • EdDSA

    Ed25519

    Curve25519.Signing (Ed25519); the SDK declares no Ed448 type.

    Recorded by the check
    fact eddsa = Ed25519
  • Deterministic Ed25519 signatures

    no

    Two signatures of one message with one key differed; both verified, also with node:crypto.

    Recorded by the check
    fact ed25519-deterministic = no
    evidence ed25519-deterministic: RFC 8032 test 1: the derived public key matches: yes, the RFC signature verifies: yes
    evidence ed25519-deterministic: CryptoKit's signature with the same key and message equals the RFC signature: no; two CryptoKit signatures are identical: no; both verify: yes
    evidence ed25519-deterministic: node:crypto (OpenSSL) verifies the CryptoKit signature: yes
  • Ed25519 signature with non-canonical S

    rejected

    The RFC 8032 test 1 signature with S replaced by S + L was rejected.

    Recorded by the check
    fact ed25519-non-canonical-s = rejected
    evidence ed25519-non-canonical-s: the RFC 8032 test 1 signature with S replaced by S + L
  • Ed25519 small-order public key

    accepted

    The identity point imported as a public key, and R = identity with S = 0 verified.

    Recorded by the check
    fact ed25519-small-order-key = accepted
    evidence ed25519-small-order-key: the identity point as public key: import accepted; signature R = identity, S = 0 verifies: yes
  • Malformed Ed25519 signature

    rejected

    64 bytes of 0xab and a 63-byte signature returned false.

    Recorded by the check
    fact ed25519-malformed-signature = rejected
    evidence ed25519-malformed-signature: 64 bytes of 0xab: false; 63 bytes of the RFC signature: false
  • Post-quantum algorithms

    ML-KEMML-DSAX-Wing

    ML-KEM and X-Wing encapsulations round-tripped and ML-DSA signatures verified; X-Wing also worked through HPKE.

    Recorded by the check
    fact pqc = ML-KEM, ML-DSA, X-Wing
    evidence pqc: ML-KEM-768: public key 1184 bytes, ciphertext 1088 bytes, shared secrets match: yes
    evidence pqc: ML-KEM-1024: public key 1568 bytes, ciphertext 1568 bytes, shared secrets match: yes
    evidence pqc: ML-DSA-65: public key 1952 bytes, signature 3309 bytes, verifies: yes; ML-DSA-87: signature 4627 bytes, verifies: yes
    evidence pqc: X-Wing (XWingMLKEM768X25519): public key 1216 bytes, ciphertext 1120 bytes, shared secrets match: yes
    evidence pqc: HPKE with XWingMLKEM768X25519_SHA256_AES_GCM_256 opens its own message: yes
  • ML-KEM parameter sets

    ML-KEM-768ML-KEM-1024

    MLKEM768 and MLKEM1024 round-tripped; the SDK declares no ML-KEM-512 type.

    Recorded by the check
    fact ml-kem-parameter-sets = ML-KEM-768, ML-KEM-1024
  • ML-DSA parameter sets

    ML-DSA-65ML-DSA-87

    MLDSA65 and MLDSA87 signed and verified; the SDK declares no ML-DSA-44 type.

    Recorded by the check
    fact ml-dsa-parameter-sets = ML-DSA-65, ML-DSA-87
  • ML-DSA signing mode

    hedged

    Two ML-DSA-65 signatures of one message differed (hedged signing).

    Recorded by the check
    fact ml-dsa-signing = hedged
    evidence ml-dsa-signing: two ML-DSA-65 signatures of one message are different
  • ML-DSA context string

    supported

    A signature with the context invoice-v1 verified with it and failed with invoice-v2 and without a context.

    Recorded by the check
    fact ml-dsa-context-string = supported
    evidence ml-dsa-context-string: signed with context "invoice-v1": verifies with it yes, with "invoice-v2" no, without a context no
  • Malformed ML-DSA signature

    returns-false

    A truncated signature, 3 bytes and a wrong message returned false.

    Recorded by the check
    fact ml-dsa-malformed-signature = returns-false
    evidence ml-dsa-malformed-signature: isValidSignature: truncated signature false, 3 bytes false, wrong message false
  • Modified ML-KEM ciphertext

    implicit-rejection

    With one ciphertext bit flipped, decapsulate returned a key different from the sender's.

    Recorded by the check
    fact ml-kem-modified-ciphertext = implicit-rejection
    evidence ml-kem-modified-ciphertext: ML-KEM-768 with one ciphertext bit flipped: decapsulate returned a key that differs from the sender's
  • Post-quantum private keys

    seed-only

    seedRepresentation is 64 bytes for ML-KEM-768 and 32 for ML-DSA-65, and keys rebuilt from it matched; the only other private-key initializer takes the integrity-checked seed.

    Recorded by the check
    fact pq-private-keys = seed-only
    evidence pq-private-keys: ML-KEM-768 seedRepresentation 64 bytes, integrityCheckedRepresentation 96 bytes; ML-DSA-65 seedRepresentation 32 bytes, integrityCheckedRepresentation 64 bytes
    evidence pq-private-keys: keys rebuilt from the seed alone have the same public key: ML-KEM-768 yes, ML-DSA-65 yes
    evidence pq-private-keys: MLKEM768.PrivateKey initializers in the SDK: init() init<D>(integrityCheckedRepresentation: init<D>(seedRepresentation:
    evidence pq-private-keys: MLDSA65.PrivateKey initializers in the SDK: init() init<D>(integrityCheckedRepresentation: init<D>(seedRepresentation:
  • Secure Enclave algorithms

    P-256ML-KEM-768ML-KEM-1024ML-DSA-65ML-DSA-87

    Keys of each type were generated in the Secure Enclave and signed, verified or decapsulated on this Mac.

    Recorded by the check
    fact secure-enclave-algorithms = P-256, ML-KEM-768, ML-KEM-1024, ML-DSA-65, ML-DSA-87
    evidence secure-enclave-algorithms: SecureEnclave.isAvailable: true
    evidence secure-enclave-algorithms: P-256: signature verifies with the public key: yes, two signatures differ: yes, a key re-created from dataRepresentation signs for the same public key: yes
    evidence secure-enclave-algorithms: SecureEnclave key types in the SDK: MLDSA65 MLDSA87 MLKEM1024 MLKEM768 P256
  • Public key formats

    spki-pemspki-dersec1-uncompressedsec1-compressedraw-xyraw-32

    SPKI PEM and DER, SEC1 uncompressed and compressed, and raw x‖y for P-256, plus 32 raw bytes for Ed25519, all written and read back.

    Recorded by the check
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed, raw-xy, raw-32
    evidence public-key-formats: P-256 public key sizes: raw 64, x963 65, compressed 33, DER 91; PEM header -----BEGIN PUBLIC KEY-----; Ed25519 raw 32
    evidence public-key-formats: written and read back: spki-pem yes, spki-der yes, sec1-uncompressed yes, sec1-compressed yes, raw-xy yes, raw-32 yes
    evidence public-key-formats: key representations declared on the Curve25519 types: rawRepresentation; lines mentioning JWK in the SDK interface: 0
  • Private key formats

    pkcs8-pempkcs8-dersec1-pemsec1-derraw-scalarseed-32hardware-handle

    Reads PKCS#8 and SEC1, PEM or DER, the raw P-256 scalar and the Ed25519 seed; writes PKCS#8; Secure Enclave keys are handles.

    Recorded by the check
    fact private-key-formats = pkcs8-pem, pkcs8-der, sec1-pem, sec1-der, raw-scalar, seed-32, hardware-handle
    evidence private-key-formats: P-256 private key written as: derRepresentation PKCS#8 (138 bytes), pemRepresentation -----BEGIN PRIVATE KEY-----, rawRepresentation 32 bytes, x963Representation 97 bytes
    evidence private-key-formats: OpenSSL-written P-256 keys read: pkcs8-pem yes, pkcs8-der yes, encrypted-pkcs8 no, sec1-pem yes, sec1-der yes; encrypted PKCS#8: threw CryptoKitASN1Error.invalidPEMDocument
    evidence private-key-formats: key representations declared inside the SecureEnclave types: dataRepresentation

Usage

Sign with a P-256 key, take the raw form for JOSE and the DER form for X.509 peers, and verify a signature rebuilt from DER.
import CryptoKit
import Foundation

let key = P256.Signing.PrivateKey()
let message = Data("order #1042 approved".utf8)
let signature = try key.signature(for: message) // SHA-256 for P-256, random nonce

// JWS (ES256) and WebCrypto use the 64-byte r||s form; X.509, OpenSSL and Java use DER.
let jose = signature.rawRepresentation
let der = signature.derRepresentation

// Rebuild the signature from the form that arrived, then verify it.
let received = try P256.Signing.ECDSASignature(derRepresentation: der)
let valid = key.publicKey.isValidSignature(received, for: message)
print("raw bytes: \(jose.count), DER tag: 0x\(String(der[0], radix: 16)), verified: \(valid)")
// Other platforms import the public key as SPKI PEM.
print(key.publicKey.pemRepresentation.split(separator: "\n")[0])
Create a P-256 key in the Secure Enclave, reopen it from its handle, sign, and verify with the ordinary public key.
import CryptoKit
import Foundation

guard SecureEnclave.isAvailable else { fatalError("this device has no Secure Enclave") }
// The private key is generated inside the Secure Enclave; dataRepresentation is
// an opaque handle to store and reopen it, not the key itself.
let key = try SecureEnclave.P256.Signing.PrivateKey()
let reopened = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: key.dataRepresentation)

let message = Data("order #1042 approved".utf8)
let signature = try reopened.signature(for: message)
// Verifiers need only the ordinary P-256 public key.
let verifier = try P256.Signing.PublicKey(derRepresentation: key.publicKey.derRepresentation)
print("Secure Enclave signature verified: \(verifier.isValidSignature(signature, for: message))")

No container image provides this platform; the samples were run on macOS 27.0.1 (Swift 6.4) on October 20, 2026.

Libraries