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.