package main import ( "crypto/ed25519" "crypto/x509" "encoding/hex" "encoding/pem" "fmt" "log" "os") func main() { pemBytes, err := os.ReadFile("key.pem") if err != nil { log.Fatal(err) } block, _ := pem.Decode(pemBytes) // "PRIVATE KEY" — PKCS#8 if block == nil { log.Fatal("key.pem: no PEM block") } parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { log.Fatal(err) } privateKey, ok := parsed.(ed25519.PrivateKey) // a value type, not a pointer if !ok { log.Fatalf("expected an Ed25519 key, got %T", parsed) } message, err := os.ReadFile("message.txt") if err != nil { log.Fatal(err) } signature := ed25519.Sign(privateKey, message) // the message itself, not a digest if err := os.WriteFile("sig.bin", signature, 0o644); err != nil { log.Fatal(err) } fmt.Println("signature:", hex.EncodeToString(signature))} package main import ( "crypto/ed25519" "crypto/x509" "encoding/pem" "fmt" "log" "os") func main() { pemBytes, err := os.ReadFile("pub.pem") if err != nil { log.Fatal(err) } block, _ := pem.Decode(pemBytes) // "PUBLIC KEY" — SPKI if block == nil { log.Fatal("pub.pem: no PEM block") } parsed, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { log.Fatal(err) } publicKey, ok := parsed.(ed25519.PublicKey) if !ok { log.Fatalf("expected an Ed25519 key, got %T", parsed) } message, err := os.ReadFile("message.txt") if err != nil { log.Fatal(err) } signature, err := os.ReadFile("sig.bin") if err != nil { log.Fatal(err) } fmt.Println("valid:", ed25519.Verify(publicKey, message, signature)) fmt.Println("tampered message valid:", ed25519.Verify(publicKey, []byte("hello ed25520"), signature))} How it works
x509.ParsePKCS8PrivateKeyhandles every PKCS#8 key type and returnsany; for Ed25519 the concrete type ised25519.PrivateKey, a 64-byte slice holding the seed followed by the public key.ed25519.Sign(privateKey, message)hashes internally with SHA-512 — do not hash the message first.x509.ParsePKIXPublicKeyreturns aned25519.PublicKey(32 bytes) for aBEGIN PUBLIC KEYblock, anded25519.Verifyreports the verdict as abool.- Both programs share one module and run with
go run ./signandgo run ./verify; the signature matches every other recipe of this task.
Gotchas
- The type assertion is to a value, not a pointer:
parsed.(*ed25519.PrivateKey)fails, while ECDSA keys come back as*ecdsa.PrivateKey. Code ported from ECDSA trips over this first. - Through the generic
crypto.Signerinterface, passingcrypto.SHA256as options — as ECDSA code does — returnsed25519: expected opts.HashFunc() zero (unhashed message, for standard Ed25519) or SHA-512 (for Ed25519ph). Passcrypto.Hash(0)and the unhashed message. - Wrong key lengths panic instead of returning an error:
ed25519.Verifywith a 31-byte key panics withed25519: bad public key length: 31. Checklen(key) == ed25519.PublicKeySizewhen keys come from outside. - A 32-byte seed from another system is not an
ed25519.PrivateKey; signing with it panics withed25519: bad private key: ed25519: bad private key length: 32. Expand it withed25519.NewKeyFromSeed(seed)— andprivateKey.Seed()goes the other way.