ED25519 Digital Signatures In Go

When a sender transmits a message to a receiver, to ensure that the message is truly from the sender and has not been altered in transit, cryptographic signatures can be used for verification.

The whole flow works like this:

From The Sender

  • the sender of the message has his own public and private key.
  • the sender instead of just sending the message, it also sends the message signature with the message.
  • the message signature is generated by signing the message with the senders private key.

At The Receiver

  • the receiver receives the message and signature, and should already have the senders public key.
  • the receiver before processing the message, verifies if it was truly sent by the intended sender.
  • the receiver verifies the signature of the message from the senders public key.
  • if the signature is verified, the message will be processed else an error will be returned to the sender of the message.

Let's Code

In this article, we will use the Elliptic Curve algorithm, the very famous ED25519 curve. As we will write our code in Go, let us first create a module, we will name it as infinity (name can be anything).

$ go mod init infinity

Create a directory named certs in the project to store our ED25519 public and private keys. Next, let's generate the key pair. On Unix systems, we can use openssl, while on Windows, keygen can be used.

$ openssl genpkey -algorithm Ed25519 -out private_key.pem
$ openssl pkey -in private_key.pem -pubout -out public_key.pem

Next create the main.go file in the project, and the project directory structure should look like this:

infinity/
├── main.go
└── certs/
    ├── private_key.pem
    └── public_key.pem

Now that everything is set up, let's generate a signature for our message using the newly created private key. We will generate the signature as a hex string, making it easy to send in an API request.

Next, we will verify the signature against the message via the ED25519 public key, from the same pair of private key.

Generate ED25519 Signature

A signature of a message is always generated using the private key. So we will write a function to read the private key from the file, and convert it into a private key object that is required for signing the message. And let's sign the message and print the generated signature as a hex string.

package main

import (
	"crypto/ed25519"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"os"
	"path/filepath"
)

func loadPrivateKey(filename string) (ed25519.PrivateKey, error) {
	keyBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	block, _ := pem.Decode(keyBytes)
	if block == nil {
		return nil, fmt.Errorf("failed to decode PEM block")
	}

	privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	return privateKey.(ed25519.PrivateKey), nil
}

func main() {
	// load private key
	privateKey, err := loadPrivateKey(filepath.Join("certs", "private_key.pem"))
	if err != nil {
		fmt.Println("error loading private key:", err)
		return
	}

	// message to sign
	message := []byte("hey, I am coming for movies tonight!")

	// create signature
	signature := ed25519.Sign(privateKey, message)

	// convert signature to hex string
	signatureHex := hex.EncodeToString(signature)

	fmt.Println("signature (hex):", signatureHex)
}

Now lets run the code:

$ go run main.go
signature (hex): 47537ff12eb71f0c0624859d7dcee64b61d619b16a9793ca254f72fa8c1fecb4b7136f07109e3be042351eb454dac9b4a0ef40cfa71af791018f8c41fd55820a

So our signature is generated, next lets see how can we verify the signature with the public key.

Verify The Signature

The recipient of the message will get the message and the signature of the message. And the recipient should already have the senders public key.

Now, with the message signature and the public key, lets verify if the signature is correct before trusting the message.

So, let's add the below code in our main.go file to load the public key and verify the signature.

// ... keep the existing code as it is

func loadPublicKey(filename string) (ed25519.PublicKey, error) {
	keyBytes, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}

	block, _ := pem.Decode(keyBytes)
	if block == nil {
		return nil, fmt.Errorf("failed to decode PEM block")
	}

	publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}

	return publicKey.(ed25519.PublicKey), nil
}

func main() {
	// ... keep the existing code as it is
	publicKey, err := loadPublicKey(filepath.Join("certs", "public_key.pem"))
	if err != nil {
		fmt.Println("error loading public key:", err)
		return
	}
	
	// convert hex string back to bytes
	signatureBytes, err := hex.DecodeString(signatureHex)
	if err != nil {
		fmt.Println("error decoding signature hex:", err)
		return
	}

	// verify signature
	isValid := ed25519.Verify(publicKey, message, signatureBytes)

	fmt.Println("signature verification:", isValid)	
}

So, in the above code, we are loading the public key and verifying the signature of the message. The ed25519.Verify function returns true or false, based on which we can trust and process the incoming message.

Let us run the code and see if the signature can be verified:

$ go run main.go
signature (hex): e752c601d65ac0fe39939d1419a41bb60fd7630d6be5590b9d248d505f012e1d78c68d5c89dd760d135e6da7d5ad7020bfa0f17a4dcf8af3c4ec4e38d35c090e
signature verification: true

We have successfully verified the message signature, ensuring a secure way to validate incoming messages for critical applications.

Finally, it's important to note that signature generation and verification can be performed in different programming languages, as long as the key pairs are correct.


My First Go Book

My First Go Book

Learn Go from scratch with hands-on examples! Whether you're a beginner or an experienced developer, this book makes mastering Go easy and fun.

🚀 Get it Now