How to Generate and Use Ed25519 Keys in Go
You built a service that sends configuration updates to edge devices. You need to prove the update came from your server, not a hacker injecting traffic on the network. Or you're signing a git commit to prove you wrote the code. Or you're issuing a token that a third party must verify without sharing a secret password. You need a digital signature.
Ed25519 is the modern standard for digital signatures. It's based on elliptic curve cryptography, specifically the Curve25519 family. It produces small keys, tiny signatures, and runs fast. More importantly, it's hard to misuse. Unlike older algorithms, Ed25519 doesn't require you to generate a fresh random number for every signature. That removes a whole class of catastrophic security bugs.
Go's standard library includes crypto/ed25519. It gives you everything you need to generate keys, sign data, and verify signatures with zero external dependencies.
How Ed25519 Works
Asymmetric cryptography uses a pair of keys. The private key stays secret. You use it to sign data. The public key is shared openly. Anyone can use it to verify that the signature came from the holder of the private key. If the data changes even slightly, verification fails.
Ed25519 improves on older schemes like RSA and ECDSA in two major ways. First, the key sizes are fixed and small. A private key is 32 bytes. A public key is 32 bytes. A signature is 64 bytes. Compare that to RSA, where keys are often 2048 or 4096 bytes and signatures are even larger. Smaller keys mean faster transmission and less storage.
Second, Ed25519 is deterministic. Most signature algorithms need a random number generator to produce a unique "nonce" for every signature. If the random number generator is weak, predictable, or reused, an attacker can mathematically recover your private key. This has happened in the wild with ECDSA. Ed25519 derives the nonce from the message hash and the private key. The same message always produces the same signature. You never need a random number generator at signing time. The security relies entirely on the secrecy of the private key.
Minimal Example
Here's the simplest way to generate a key pair, sign a message, and verify the result. The code runs entirely in memory and demonstrates the core API.
package main
import (
"crypto/ed25519"
"fmt"
)
func main() {
// GenerateKey returns a public key, a private key, and an error.
// Passing nil for the reader uses crypto/rand automatically.
public, private, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
// The message to sign. Can be any byte slice.
message := []byte("Hello, World!")
// Sign returns a 64-byte signature.
// It panics if the private key is invalid, so trust the key generation.
signature := ed25519.Sign(private, message)
// Verify checks if the signature matches the message and public key.
// Returns true if valid, false otherwise.
if ed25519.Verify(public, message, signature) {
fmt.Println("Signature is valid")
} else {
fmt.Println("Signature is invalid")
}
}
Generate keys once. Sign often. Verify everywhere.
What Happens Under the Hood
When you call ed25519.GenerateKey(nil), Go generates 32 random bytes. These bytes become the seed for the key pair. The function derives the public key from the seed using elliptic curve multiplication.
Go stores the private key as a 64-byte slice. This is a convention specific to the Go implementation. The first 32 bytes are the seed. The last 32 bytes are the public key bytes. This means the private key contains the public key. If you have the private key, you can extract the public key by slicing: publicKey := privateKey[32:]. This saves storage in some applications, though you should still treat the two types distinctly in your code.
The ed25519.Sign function takes the private key and the message. It hashes the message, combines it with the private key seed, and performs curve arithmetic to produce a 64-byte signature. The function panics if the private key length is not exactly 64 bytes. Since GenerateKey always returns a valid key, this panic only happens if you construct a key manually and get the length wrong.
The ed25519.Verify function takes the public key, the message, and the signature. It repeats the curve arithmetic using the public key and checks if the result matches the signature. It returns a boolean. It panics if the public key length is not 32 bytes.
Realistic Usage
In production, you rarely sign a hardcoded string. You sign structured data, like a JSON payload or a file hash. You also need to handle errors properly and separate concerns.
Here's a function that signs a message and returns the signature. It follows Go conventions for error handling and receiver naming.
package main
import (
"crypto/ed25519"
"fmt"
)
// SignMessage signs the input message with the provided private key.
// It returns the signature bytes or an error if the key is invalid.
func SignMessage(privateKey ed25519.PrivateKey, message []byte) ([]byte, error) {
// Validate key length before signing.
// Sign panics on bad keys, so check explicitly for clean errors.
if len(privateKey) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key length: %d", len(privateKey))
}
// Sign the message.
// This returns 64 bytes of signature data.
signature := ed25519.Sign(privateKey, message)
// Return the signature for the caller to store or transmit.
return signature, nil
}
func main() {
// Generate a key pair.
// Always check the error, even if it rarely fails.
public, private, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
// Sign a realistic payload.
payload := []byte(`{"action":"deploy","version":"1.0.0"}`)
signature, err := SignMessage(private, payload)
if err != nil {
panic(err)
}
// Verify the signature.
// In a real app, this runs on a different machine with only the public key.
if ed25519.Verify(public, payload, signature) {
fmt.Println("Payload verified")
}
}
Store keys securely. Never embed private keys in source code. Use environment variables, secret managers, or hardware security modules.
Pitfalls and Compiler Errors
Ed25519 is safe, but Go's type system catches mistakes early. The types ed25519.PublicKey and ed25519.PrivateKey are defined as type PublicKey []byte and type PrivateKey []byte. They are distinct types, even though both are byte slices.
If you try to pass a raw []byte to Sign, the compiler rejects it. You get an error like cannot use rawBytes (variable of type []byte) as ed25519.PrivateKey value in argument. You must cast the slice: ed25519.PrivateKey(rawBytes). This cast tells the compiler you understand the bytes represent a key.
// BAD: Compiler error
// ed25519.Sign(rawBytes, message)
// GOOD: Explicit cast
ed25519.Sign(ed25519.PrivateKey(rawBytes), message)
If you forget to import the package, the compiler says undefined: ed25519. If you import it but don't use it, you get imported and not used. Go requires all imports to be used.
Runtime panics happen if you pass keys with the wrong length. Sign panics if the private key is not 64 bytes. Verify panics if the public key is not 32 bytes. Always validate key lengths if you load keys from external sources.
Another pitfall is message malleability. Ed25519 signatures are canonical, but some implementations accept multiple valid signatures for the same message. Go's ed25519.Verify rejects non-canonical signatures. This prevents attackers from modifying signatures to bypass checks. Trust the library. Don't write your own verification logic.
When to Use Ed25519
Choose the right tool for the job. Ed25519 excels in specific scenarios.
Use Ed25519 when you need a fast, safe signature with no configuration choices. It's the default for SSH keys, TLS certificates, and modern APIs.
Use RSA when you must interoperate with legacy systems that only support 2048-bit or 4096-bit keys. RSA is slower and produces larger signatures, but it's universally supported.
Use ECDSA when you need compatibility with systems that require NIST curves like P-256. ECDSA is similar to Ed25519 but requires careful handling of random number generation.
Use HMAC when both parties share a secret and you don't need public verification. HMAC is faster for symmetric scenarios, but anyone with the secret can sign. Ed25519 allows public verification without sharing the secret.
Use Ed25519 for new projects. It's the standard for a reason.