Skip to content
ecdsa.com

ECDH key agreement · Swift

How to derive a shared key with ECDH and HKDF in Swift

Swift derives a shared key with ECDH through the CryptoKit API — CryptoKit itself on Apple platforms, swift-crypto on Linux: sharedSecretFromKeyAgreement computes the X25519 or P-256 secret, and hkdfDerivedSymmetricKey turns it into a SymmetricKey. P-256 keys read PEM directly; X25519 keys have only a raw form.

Tested with swift-crypto 4.5.2 (Swift 6.4.1, Linux (BoringSSL)).

Package.swift — swift-crypto for Linux (on Apple platforms, import CryptoKit instead)
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "ecdh-demo",
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", exact: "4.5.2"),
],
targets: [
.executableTarget(name: "ecdh-demo", dependencies: [.product(name: "Crypto", package: "swift-crypto")]),
]
)
main.swift — X25519 and P-256 agreement from PEM files, then HKDF-SHA256
import Crypto
import Foundation
struct KeyFormatError: Error {}
struct WeakPeerKeyError: Error {}
/// X25519 keys have only a raw form; RFC 8410 fixes the DER around the 32 bytes.
func rawX25519Key(pemFile: String, derPrefix: [UInt8]) throws -> Data {
let body = try String(contentsOfFile: pemFile, encoding: .utf8)
.split(separator: "\n").filter { !$0.hasPrefix("-----") }.joined()
guard let der = Data(base64Encoded: body), der.count == derPrefix.count + 32, der.starts(with: derPrefix) else {
throw KeyFormatError()
}
return der.suffix(32)
}
let x25519PKCS8Prefix: [UInt8] = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20]
let x25519SPKIPrefix: [UInt8] = [0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00]
let aliceX25519 = try Curve25519.KeyAgreement.PrivateKey(
rawRepresentation: rawX25519Key(pemFile: "alice-x25519.pem", derPrefix: x25519PKCS8Prefix))
let bobX25519 = try Curve25519.KeyAgreement.PublicKey(
rawRepresentation: rawX25519Key(pemFile: "bob-x25519.pub.pem", derPrefix: x25519SPKIPrefix))
// P-256 keys read PEM directly
let aliceP256 = try P256.KeyAgreement.PrivateKey(pemRepresentation: String(contentsOfFile: "alice-p256.pem", encoding: .utf8))
let bobP256 = try P256.KeyAgreement.PublicKey(pemRepresentation: String(contentsOfFile: "bob-p256.pub.pem", encoding: .utf8))
// swift-crypto accepts a low-order X25519 peer key and returns 32 zero bytes: reject that
let x25519Secret = try aliceX25519.sharedSecretFromKeyAgreement(with: bobX25519)
guard !x25519Secret.withUnsafeBytes({ $0.allSatisfy { $0 == 0 } }) else { throw WeakPeerKeyError() }
let secrets = [
("X25519", x25519Secret),
("P-256", try aliceP256.sharedSecretFromKeyAgreement(with: bobP256)),
]
for (curve, secret) in secrets {
let key = secret.hkdfDerivedSymmetricKey(
using: SHA256.self, salt: Data(), sharedInfo: Data("AES-256-GCM key v1".utf8), outputByteCount: 32)
// Printing secrets is for this demonstration only
let secretHex = secret.withUnsafeBytes { $0.map { String(format: "%02x", $0) }.joined() }
let keyHex = key.withUnsafeBytes { $0.map { String(format: "%02x", $0) }.joined() }
print("\(curve) shared secret: \(secretHex)")
print("\(curve) derived key: \(keyHex)")
}

How it works

  1. P256.KeyAgreement.PrivateKey(pemRepresentation:) and PublicKey(pemRepresentation:) read the PEM files; for X25519, the RFC 8410 DER prefix is checked and stripped to get the 32 raw bytes.
  2. sharedSecretFromKeyAgreement(with:) returns a SharedSecret, deliberately opaque: its bytes are reachable only through withUnsafeBytes.
  3. hkdfDerivedSymmetricKey(using: SHA256.self, salt:, sharedInfo:, outputByteCount: 32) is RFC 5869 HKDF; sharedInfo is HKDF's info, and an empty salt is the default.
  4. The run prints the RFC 7748 and RFC 5903 secrets and the same derived keys as every other recipe of this task.

Gotchas

  • swift-crypto on Linux accepts a low-order X25519 peer key and returns 32 zero bytes instead of throwing. The guard above rejects that result; without it, an attacker-chosen key gives a predictable secret.
  • x963DerivedSymmetricKey sits right next to the HKDF method and also produces a key — a different one (ANSI X9.63). Both sides must call the same KDF; HKDF is what the other platforms use.
  • Curve25519.KeyAgreement keys have no pemRepresentation or derRepresentation, unlike P256.KeyAgreement keys, so PEM handling differs by curve.
  • Printing the secret, as the demonstration does, defeats the point of SharedSecret being opaque. In production code, pass it straight to the KDF.

Related recipes