How do P-256 and Ed25519 signatures differ in CryptoKit: sizes, encodings, determinism, and interoperating with other tools?
Predict first
Are the signatures stable, and does OpenSSL accept the DER form?
import Foundation
import CryptoKit
let msg = Data("pay 10".utf8)
let p = P256.Signing.PrivateKey()
let s1 = try p.signature(for: msg), s2 = try p.signature(for: msg)
print("P-256 raw:", s1.rawRepresentation.count, "| two signatures differ:", s1.rawRepresentation != s2.rawRepresentation,
"| both verify:", p.publicKey.isValidSignature(s1, for: msg) && p.publicKey.isValidSignature(s2, for: msg))
let e = Curve25519.Signing.PrivateKey()
let e1 = try e.signature(for: msg), e2 = try e.signature(for: msg)
print("Ed25519:", e1.count, "| deterministic:", e1 == e2, "| verifies:", e.publicKey.isValidSignature(e1, for: msg))
print("public key sizes raw/x963/compressed:", p.publicKey.rawRepresentation.count, p.publicKey.x963Representation.count, p.publicKey.compressedRepresentation.count)
print("changed message verifies:", p.publicKey.isValidSignature(s1, for: Data("pay 99".utf8)))
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("sig\(getpid())")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let pub = dir.appendingPathComponent("pub.pem"), sig = dir.appendingPathComponent("sig.der"), m = dir.appendingPathComponent("m.bin")
try p.publicKey.pemRepresentation.write(to: pub, atomically: true, encoding: .utf8)
try s1.derRepresentation.write(to: sig)
try msg.write(to: m)
let pr = Process(); pr.executableURL = URL(fileURLWithPath: "/usr/bin/openssl")
pr.arguments = ["dgst", "-sha256", "-verify", pub.path, "-signature", sig.path, m.path]
let pipe = Pipe(); pr.standardOutput = pipe; try pr.run(); pr.waitUntilExit()
print("openssl:", String(decoding: pipe.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines))
try? FileManager.default.removeItem(at: dir)
- P-256 raw: 64 | two signatures differ: true | both verify: true / Ed25519: 64 | deterministic: false | verifies: true / public key sizes raw/x963/compressed: 64 65 33 / changed message verifies: false / openssl: Verified OK
- two signatures differ: false / deterministic: true / openssl: Verification Failure
- P-256 raw: 72 / public key sizes raw/x963/compressed: 32 33 65
Mental model. A signature is a handwritten signature that is slightly different each time you sign but is always recognised by the bank; the DER and raw forms are the same signature written on two different forms.
Both algorithms produce a 64-byte signature in CryptoKit, but the encodings and the plumbing differ. A P-256 signature has a rawRepresentation (r then s, 64 bytes, the form JWS/ES256 uses) and a derRepresentation (ASN.1, a variable 70 to 72 bytes, the form OpenSSL, X.509 and most servers expect); mixing them up is the commonest interop bug. A P-256 public key has a raw form (64 bytes), an ANSI X9.63 form (65 bytes with a leading 0x04), a compressed form (33 bytes) and a PEM/DER form; an Ed25519 key is 32 bytes. I checked interoperability by exporting the PEM public key and the DER signature and having /usr/bin/openssl dgst -sha256 -verify report Verified OK. Changing one byte of the message makes verification return false, and the signature is over the message’s digest, so signing SHA256.hash of the message verifies against the message.
Determinism surprises people. ECDSA is randomised, so two signatures of the same message differ and both verify (checked). Ed25519 is deterministic in its specification, but CryptoKit’s Ed25519 signatures differed between two calls on the same message in the check (the implementation adds randomness), so do not use a signature as a stable identifier, a cache key or a deduplication id, and do not write tests that expect a fixed signature. If you need one, hash the signed content. Choose P-256 when you need the Secure Enclave (only P-256 is supported there) or compatibility with ES256, and Ed25519 when you control both ends and want small keys and fast verification. Always verify with the public key you obtained through a trusted channel, never one embedded in the same message.
ECDSA and CryptoKit’s Ed25519 both gave different valid signatures; DER interoperates with OpenSSL.
Common traps
- Sending a raw 64-byte P-256 signature to a server expecting DER (or the reverse). Verification fails with no useful message.
- Assuming Ed25519 signatures are reproducible. In CryptoKit they were not; hash the content if you need a stable id.
- Taking the verification key from the signed message itself. That proves only that someone signed with some key.
They might follow up with
- Which form does JWT ES256 use? The raw r||s form, not DER.
- Why can an Ed25519 signature differ yet still verify? Verification checks a mathematical relation, not equality with a previous signature.
Related questions
- 14.6.2 How do two parties agree on a key with CryptoKit, why must the shared secret go through HKDF, and what does…
- 14.6.6 What should code that reads a JWT check, why is a client usually not the one to trust it, and what is the…
- 14.7.4 How does a Secure Enclave key behave in code: what is stored, what can be restored, and what can you not do…
- 14.1.1 What is the difference between hashing, a MAC, encryption and a digital signature, and when do you use…
← How do two parties agree on a key with CryptoKit, why must the shared… How does a time-based one-time password (TOTP) work, and how would… →