// swift-tools-version:6.0import 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")]), ])import Cryptoimport 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 bytesprint("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
Curve25519.Signing.PrivateKey(rawRepresentation:)takes the 32-byte seed, andPublicKey(rawRepresentation:)the 32-byte public key.- An Ed25519 PKCS#8 key is always the 16-byte header
302e020100300506032b657004220420followed by the seed, and an SPKI key the 12-byte header302a300506032b6570032100followed by the key (RFC 8410). Checking the prefix and the length is therefore a complete parser for these two structures. signature(for:)returns 64 bytes, andisValidSignature(_:for:)returns aBool.swift build -c releaseresolves 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
pemRepresentationorderRepresentation(P-256 keys have both) —Curve25519.Signing.PrivateKey(pemRepresentation:)does not compile — hence the explicit prefix handling. import Cryptoworks 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 importsCryptoKitdirectly does not build on Linux.- Keep the private key's
rawRepresentationout of logs andprintcalls: it is the complete secret, and anyone holding those 32 bytes can sign.