name: swift-post-quantum-security-ios26 description: >- Adopt iOS 26 post-quantum cryptography and transport hardening the canonical way — PQ-TLS 1.3 is automatic for URLSession/Network (no opt-in), HPKE app-layer encryption uses the XWingMLKEM768X25519_SHA256_AES_GCM_256 ciphersuite, ML-KEM / ML-DSA ship as first-class CryptoKit types, constant-time comparison uses HMAC.isValidAuthenticationCode (there is NO CryptoKit.timingSafeEqual), and certificate pinning uses NSPinnedDomains or a URLSessionDelegate with SecTrustEvaluateWithError. Use when implementing or reviewing encryption, key exchange, secret comparison, or TLS pinning on iOS 26+. version: "1.0.0" freshness: verified_against: - source: "Apple Developer — HPKE (enum) + HPKE.Ciphersuite.XWingMLKEM768X25519_SHA256_AES_GCM_256 + HPKE.Sender" url: "https://developer.apple.com/documentation/cryptokit/hpke" version: "iOS 26.0 (HPKE iOS 17.0+, XWing ciphersuite iOS 26.0+)" - source: "Apple Developer — XWingMLKEM768X25519 / MLKEM768 / MLDSA65 (post-quantum CryptoKit types)" url: "https://developer.apple.com/documentation/cryptokit/xwingmlkem768x25519" version: "iOS 26.0" - source: "Apple Developer — HMAC.isValidAuthenticationCode(:authenticating:using:) (constant-time MAC check)" url: "https://developer.apple.com/documentation/cryptokit/hmac/isvalidauthenticationcode(:authenticating:using:)-8ezmw" version: "iOS 13.0+" - source: "Apple Developer — NSPinnedDomains + SecTrustEvaluateWithError / SecCertificateCopyKey (pinning)" url: "https://developer.apple.com/documentation/bundleresources/information-property-list/nsapptransportsecurity/nspinneddomains" version: "iOS 14.0+ (chain copy iOS 15.0+)" - source: "Apple Platform Security — Quantum-secure cryptography in Apple operating systems (PQ-TLS automatic)" url: "https://support.apple.com/guide/security/quantum-secure-cryptography-apple-devices-secc7c82e533/web" version: "iOS 26.0" - source: "RFC 9180 — Hybrid Public Key Encryption (HPKE)" url: "https://datatracker.ietf.org/doc/html/rfc9180" version: "RFC 9180" verified_on: "2026-06-03" recheck_after: trigger: "WWDC26 keynote + any CryptoKit/Security release" or_date: "2026-12-01" decay_risk: medium
status: current
Post-quantum security (iOS 26 CryptoKit + transport)
iOS 26 ships post-quantum cryptography in two layers. Transport is handled for you: quantum-secure
TLS 1.3 key exchange (hybrid X25519 + ML-KEM-768) is enabled by default for URLSession and the
Network framework — there is no per-request opt-in. Application-layer encryption is available
through CryptoKit's HPKE with the new XWingMLKEM768X25519 ciphersuite, plus first-class MLKEM768,
MLKEM1024, MLDSA65, and MLDSA87 types (with SecureEnclave-backed variants). This skill applies
those primitives correctly — and corrects the single most common myth: CryptoKit.timingSafeEqual
does not exist.
When to invoke
- You're encrypting data at the application layer (stored payloads, sealed envelopes) and want a
quantum-resistant scheme — reach for HPKE with
XWingMLKEM768X25519_SHA256_AES_GCM_256. - You're comparing secrets (OAuth
state, CSRF tokens, MACs, capability strings) and need a constant-time comparison. - You're pinning a server certificate or public key for an auth/API host.
- You're reviewing crypto code and see
==onData/Stringfor a secret, a hand-rolled RSA box, or a reference to atimingSafeEqualsymbol that doesn't compile.
Announce on invoke: "Using swift-post-quantum-security-ios26 to apply iOS 26 PQ-CryptoKit and constant-time comparison per Apple's CryptoKit/Security docs."
Do not add an HPKE layer "to get post-quantum" on data that only travels over URLSession —
TLS 1.3 already gives you PQC in transit on iOS 26. Use HPKE for data at rest or end-to-end
payloads the server can't read, where defense-in-depth beyond transit actually buys something.
The canonical APIs (verified)
| API | Signature / form (verified) | Use |
|---|---|---|
| PQ-TLS 1.3 | (none — automatic) | Using URLSession/Network gives hybrid X25519+ML-KEM-768 key exchange by default on iOS 26. Legacy Secure Transport is excluded. |
HPKE.Ciphersuite.XWingMLKEM768X25519_SHA256_AES_GCM_256 |
static let … : HPKE.Ciphersuite |
The post-quantum HPKE ciphersuite (iOS 26.0+). |
HPKE.Sender |
init(recipientKey:ciphersuite:info:) then seal(_:) / seal(_:authenticating:); .encapsulatedKey |
Encrypt one or more messages to a recipient public key (iOS 17.0+; PQ via the XWing ciphersuite). |
MLKEM768 / MLKEM1024 |
MLKEM768.PrivateKey.generate() throws -> MLKEM768.PrivateKey; .publicKey |
Module-lattice KEM (NIST FIPS 203). SecureEnclave.MLKEM768 keeps the key in the SE. |
MLDSA65 / MLDSA87 |
MLDSA65.PrivateKey / .PublicKey |
Module-lattice digital signatures (NIST FIPS 204). SecureEnclave.MLDSA65 variant exists. |
HMAC.isValidAuthenticationCode(_:authenticating:using:) |
static func isValidAuthenticationCode<D>(_ authenticationCode: HMAC<H>.MAC, authenticating authenticatedData: D, using key: SymmetricKey) -> Bool where D: DataProtocol |
Constant-time MAC verification. The canonical secret-equality primitive. |
NSPinnedDomains (Info.plist) |
dict of domain → NSPinnedCAIdentities / NSPinnedLeafIdentities (+ NSIncludesSubdomains) |
Declarative cert pinning for URLSession (iOS 14+). |
SecTrustEvaluateWithError(_:_:) |
func SecTrustEvaluateWithError(_ trust: SecTrust, _ error: …) -> Bool |
Validate the server trust before SPKI pinning in a URLSessionDelegate. |
XWingMLKEM768X25519.PrivateKeyand itsPublicKeyare real iOS 26.0+ types, but the concrete type's no-argumentinit()/generate()are not documented at a stable URL — create the key via the initializer your CryptoKit version exposes (e.g. the documentedinit(seedRepresentation:publicKey:)) and treat the ciphersuite constant as the load-bearing public API to verify.MLKEM768.PrivateKey.generate()is documented and is the safe example.
The rules (break them and the crypto breaks)
1. There is no CryptoKit.timingSafeEqual — never compare secrets with ==
== on String/Data short-circuits on the first differing byte, leaking timing. CryptoKit ships
no timingSafeEqual symbol (a persistent myth). The canonical constant-time tools are:
- MAC verification:
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: data, using: key). - Equal-length byte comparison:
Data.elementsEqual(_:)/HashedAuthenticationCode.elementsEqual(_:). - Arbitrary equal-length secrets: XOR-accumulate (below), or HMAC both sides with an ephemeral key and compare the MACs.
2. TLS 1.3 PQC is automatic — don't hand-roll it, don't claim a per-request API
On iOS 26, URLSession and Network negotiate hybrid X25519 + ML-KEM-768 automatically and fall
back gracefully when the peer lacks PQC. There is no opt-in flag and no support in the legacy
Secure Transport API. Just use URLSession. Reserve HPKE for app-layer/at-rest payloads.
3. HPKE app-layer encryption uses the XWing ciphersuite — not RSA, not raw ECDH
For quantum-resistant app-layer encryption, use HPKE.Sender/HPKE.Recipient with
.XWingMLKEM768X25519_SHA256_AES_GCM_256. Send the encapsulatedKey alongside the ciphertext; the
recipient reconstructs the same key schedule with its private key. Do not pin app-layer crypto to
RSA-2048 or bare ECDH for new code.
4. Pin certificates — declaratively where you can, by delegate where you need control
NSPinnedDomains in Info.plist covers URLSession from iOS 14+ (it does not cover
SFSafariViewController, and only covers WKWebView from iOS 16+). For SPKI pinning, rotation, or
backup-pin logic, implement urlSession(_:didReceive:completionHandler:), call
SecTrustEvaluateWithError first, then hash the leaf's public key with SHA-256 and compare against
your pin set. SecTrustCopyCertificateChain requires iOS 15+.
5. Keep private keys in the Secure Enclave when the workload allows
For ML-KEM / ML-DSA keys that never need to leave the device, prefer SecureEnclave.MLKEM768 /
SecureEnclave.MLDSA65 so the private key material is generated and held in the SE; the Keychain
stores only a reference. Gate every PQ API behind #available(iOS 26, *).
Canonical example
Constant-time comparison plus an HPKE seal, using only verified APIs:
import CryptoKit
import Foundation
extension Data {
/// Constant-time equality. Safe ONLY for equal-length secrets.
/// There is no `CryptoKit.timingSafeEqual` — this is the fallback idiom.
func constantTimeEquals(_ other: Data) -> Bool {
guard count == other.count else { return false }
var diff: UInt8 = 0
for i in indices { diff |= self[i] ^ other[i] }
return diff == 0
}
}
/// Verify an OAuth `state` / CSRF token in constant time via HMAC (no `==` on secrets).
func stateMatches(received: Data, expected: Data, key: SymmetricKey) -> Bool {
let expectedMAC = HMAC<SHA256>.authenticationCode(for: expected, using: key)
return HMAC<SHA256>.isValidAuthenticationCode(expectedMAC, authenticating: received, using: key)
}
@available(iOS 26.0, macOS 26.0, *)
enum AppCrypto {
static let suite: HPKE.Ciphersuite = .XWingMLKEM768X25519_SHA256_AES_GCM_256
/// Encrypt a payload to a recipient's post-quantum public key.
static func seal(_ payload: Data,
to recipient: XWingMLKEM768X25519.PublicKey,
info: Data) throws -> (encapsulated: Data, ciphertext: Data) {
var sender = try HPKE.Sender(recipientKey: recipient, ciphersuite: suite, info: info)
let ciphertext = try sender.seal(payload)
return (sender.encapsulatedKey, ciphertext)
}
}
SPKI pinning in a delegate (the control path):
final class PinningDelegate: NSObject, URLSessionDelegate {
let pinnedSPKISHA256: Set<Data>
init(pinnedSPKISHA256: Set<Data>) { self.pinnedSPKISHA256 = pinnedSPKISHA256 }
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust,
SecTrustEvaluateWithError(trust, nil), // validate first
let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate], // iOS 15+
let leaf = chain.first,
let key = SecCertificateCopyKey(leaf),
let spki = SecKeyCopyExternalRepresentation(key, nil) as Data?
else { return completionHandler(.cancelAuthenticationChallenge, nil) }
let digest = Data(SHA256.hash(data: spki))
completionHandler(pinnedSPKISHA256.contains(digest) ? .useCredential : .cancelAuthenticationChallenge,
pinnedSPKISHA256.contains(digest) ? URLCredential(trust: trust) : nil)
}
}
Decision aid: which primitive
- In transit over HTTPS? Nothing to do —
URLSessionon iOS 26 is already post-quantum. - At rest / end-to-end the server can't read? HPKE +
XWingMLKEM768X25519_SHA256_AES_GCM_256. - Comparing a secret/token/MAC?
HMAC.isValidAuthenticationCode(or XOR-accumulate on equal length). Never==. - Signing on-device with a non-exportable key?
SecureEnclave.MLDSA65. - Simple host pinning?
NSPinnedDomains. Rotation / SPKI / backup pins? delegate +SecTrustEvaluateWithError.
Related skills
global-skills/apple-auth/swift-auth-security-checklist/SKILL.md— where these primitives slot into the full storage/transport/lifecycle defense-in-depth checklist.global-skills/apple-auth/swift-auth-security-audit-suite/SKILL.md— the tests that assert thestatecomparison is constant-time and ATS has no arbitrary loads.global-skills/apple/apple-anti-patterns/SKILL.md— registers "compare secrets with==" and "invent atimingSafeEqual" as anti-patterns this skill avoids.
Sources
- HPKE · HPKE.Sender · XWingMLKEM768X25519 · MLKEM768 · MLDSA65
- HMAC.isValidAuthenticationCode(_:authenticating:using:) · HMAC
- NSPinnedDomains · SecTrustEvaluateWithError · SecCertificateCopyKey
- Apple Platform Security — Quantum-secure cryptography in Apple operating systems · WWDC25 314 Get ahead with quantum-secure cryptography
- RFC 9180 HPKE · RFC 8446 TLS 1.3 · OWASP Pinning Cheat Sheet
Last verified: 2026-06-03 against CryptoKit + Security (Apple Developer docs, live) — HPKE XWing
ciphersuite, MLKEM768/MLDSA65, and HMAC.isValidAuthenticationCode signatures confirmed iOS 26.0/13.0+.
CryptoKit.timingSafeEqual was checked and does NOT exist — use HMAC.isValidAuthenticationCode
or equal-length XOR-accumulate. XWingMLKEM768X25519.PrivateKey() / .generate() are not documented
at a stable URL; the ciphersuite constant and MLKEM768.PrivateKey.generate() are the verified anchors.
Re-check after: WWDC26 + any CryptoKit/Security release, or by 2026-12-01. Decay risk: medium.
Found a drift? Run /skill-pattern-freshness-audit apple-auth.