Skip to content
ecdsa.com

Ed25519 sign & verify · Swift

How to sign and verify Ed25519 signatures in Swift

Swift signs and verifies Ed25519 signatures with the Curve25519.Signing API — from CryptoKit on Apple platforms, and on Linux from apple/swift-crypto, which provides the same API as the Crypto module. Curve25519 keys exist only in a raw 32-byte form, so this recipe unwraps the RFC 8410 PEM files first.

Tested with swift-crypto 4.5.2 (Swift 6.4.1, Linux (BoringSSL)) and CryptoKit macOS 27 (Swift 6.4, randomized Ed25519 signatures (checked separately)).

Package.swift — swift-crypto for Linux (on Apple platforms, import CryptoKit instead)
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "ed25519-demo",
dependencies: [
// Crypto is CryptoKit's API: CryptoKit itself on Apple platforms, BoringSSL-backed on Linux
.package(url: "https://github.com/apple/swift-crypto.git", exact: "4.5.2"),
],
targets: [
.executableTarget(name: "ed25519-demo", dependencies: [.product(name: "Crypto", package: "swift-crypto")]),
]
)
main.swift — read the PEM keys, sign and verify
import Crypto
import Foundation
struct KeyFormatError: Error {}
/// The raw 32-byte key inside an Ed25519 PEM. RFC 8410 fixes the DER around it,
/// so for these two structures a prefix check is a complete parser.
func rawKey(fromPEM path: String, derPrefix: [UInt8]) throws -> Data {
let body = try String(contentsOfFile: path, 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 pkcs8Prefix: [UInt8] = [0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]
let spkiPrefix: [UInt8] = [0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: rawKey(fromPEM: "key.pem", derPrefix: pkcs8Prefix))
let publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: rawKey(fromPEM: "pub.pem", derPrefix: spkiPrefix))
let message = try Data(contentsOf: URL(fileURLWithPath: "message.txt"))
let signature = try privateKey.signature(for: message) // 64 bytes
print("signature: \(signature.map { String(format: "%02x", $0) }.joined())")
print("valid: \(publicKey.isValidSignature(signature, for: message))")
print("tampered message valid: \(publicKey.isValidSignature(signature, for: Data("hello ed25520".utf8)))")

How it works

  1. Curve25519.Signing.PrivateKey(rawRepresentation:) takes the 32-byte seed, and PublicKey(rawRepresentation:) the 32-byte public key.
  2. An Ed25519 PKCS#8 key is always the 16-byte header 302e020100300506032b657004220420 followed by the seed, and an SPKI key the 12-byte header 302a300506032b6570032100 followed by the key (RFC 8410). Checking the prefix and the length is therefore a complete parser for these two structures.
  3. signature(for:) returns 64 bytes, and isValidSignature(_:for:) returns a Bool.
  4. swift build -c release resolves swift-crypto 4.5.2 and builds the executable the run starts; on Linux it prints the same signature as every other recipe of this task.

Gotchas

  • CryptoKit on Apple platforms randomizes Ed25519 signatures: signing the same message twice gives different, equally valid bytes. swift-crypto on Linux returns the deterministic RFC 8032 signature shown above. Tests must verify signatures, not compare them.
  • Curve25519 keys have no pemRepresentation or derRepresentation (P-256 keys have both) — Curve25519.Signing.PrivateKey(pemRepresentation:) does not compile — hence the explicit prefix handling.
  • import Crypto works on Linux and, through swift-crypto, on Apple platforms too, where swift-crypto compiles down to a re-export of CryptoKit (so signatures are randomized there as well). Code that imports CryptoKit directly does not build on Linux.
  • Keep the private key's rawRepresentation out of logs and print calls: it is the complete secret, and anyone holding those 32 bytes can sign.

Related recipes