How To Sign Content with an ed25519 Keypair
Lets go over some bash scripts to help you understand how to sign and verify content with an ed25519 keypair
The idea with this post is to go over how signing content works with an ed25519 keypair (even if you don’t know what that means).
At a high level here is what we will do:
- Stub out project files / sub-directories
- Generate an ed25519 keypair for signing content
- Generate content to sign
- Sign the content
- Verify the content
Prerequisites
- Terminal or Terminal Emulator
- OpenSSH Client
Check out this tutorial on how to install OpenSSH client on your system (mac, linux, windows). It is covered on the first part of the Lazy Article titled How to Generate an ed25519 SSH Key Pair
Project Setup
Lets run a script that stubs out our project structure.
First, begin with a new directory to work in for this tutorial.
mkdir ed25519-signing && cd ed25519-signing
touch init-project.sh
Next, lets update the empty init-project.sh
with the following.
#!/usr/bin/env bash
# Function to display error message and exit
error_exit() {
echo "Error: $1" >&2
exit 1
}
# Function to create project structure
create_project_structure() {
local project_root="$1"
# Create directories
mkdir -p "${project_root}"/{content,keys,scripts} || error_exit "Failed to create directories"
# Stub out script files
local script_files=("gen-content.sh" "gen-keypair.sh" "init-keys-dir.sh" "sign-content.sh" "verify-content.sh")
for file in "${script_files[@]}"; do
touch "${project_root}/scripts/${file}" || error_exit "Failed to create ${file}"
done
}
# Check if a project root path is provided
if [ $# -eq 0 ]; then
error_exit "No project root path provided.\nUsage: ${0##*/} <project_root_path>"
fi
# Set the project root path
PROJECT_ROOT="$1"
# Check if the provided path is absolute
if [[ "${PROJECT_ROOT}" != /* ]]; then
error_exit "Please provide an absolute path for the project root"
fi
# Check if the provided path exists
if [ ! -d "${PROJECT_ROOT}" ]; then
error_exit "The provided project root path does not exist"
fi
# Change to the project root directory
cd "${PROJECT_ROOT}" || error_exit "Failed to change to project directory"
# Create project structure
create_project_structure "${PROJECT_ROOT}"
# Display success message
success_message="Project structure created successfully in:"
echo "${success_message} $(echo "${PROJECT_ROOT}" | sed "s|^${HOME}|~|")"
# Log the operation
log_file="${PROJECT_ROOT}/.project_init_log"
relative_project_root=$(echo "${PROJECT_ROOT}" | sed "s|^${HOME}|~|")
echo "$(date): Project structure created in ${relative_project_root}" > "${log_file}"
init-project-files.sh
requires one argument: The file path to where we want to generate files / folders. In our case since we are already in the folder we want to work in we will use $(pwd)
as the argument.
bash +x init-project.sh $(pwd)
If the script runs successfully you should see a success message and a bunch of new files and folders in your working directory.
Project structure created successfully in: ~/dev/lazydevschool.com/ed25519-signing
Congratulations! You have successfully setup your project. Now we will add code to each script file for the different steps outlined at the beginning of this post.
Generate a keypair
We need to create a keypair if we want to sign and verify content.
The process I am about to show you is hyper specific to this tutorial on signing and verifying content. If you want a more generic resource for understanding keypair generation check out this Lazy Article on How To Generate An ed25519 SSH Keypair.
Never share your private key.
#!/usr/bin/env bash
project_path=$1
key_name=$2
identity=$3
private_key=$project_path/keys/$key_name
allowed_signers=$project_path/keys/allowed_signers
# create keypair
ssh-keygen -t ed25519 \
-C "$identity" \
-N "" \
-f $private_key \
-q
# create allowed_signers file
echo "$identity $(cat $private_key.pub)" > $allowed_signers
Here is a usage example for the gen-keypair.sh
script.
bash +x scripts/gen-keypair.sh \
$(pwd) \
"id_ed25519" \
"user@domain"
This will create a keypair and add the public key to the allowed_signers
file.
If you open the allowed_signers
file you should see the user@domain
is associated with the public key.
Now if anyone wonders what identity is associated with certain private key you can start to discover that information by parsing the public key from the private key and then looking up the identity associated with the public key (Last section of this post talks about two ways to do this).
Generate Content (for signing)
Lets run the tree command on our project directory and redirect the output to a file.
Update scripts/gen-content.sh
with the following.
#!/usr/bin/env bash
project_path=$1
output_path="$project_path/content/tree.txt"
# Create content directory if it doesn't exist
mkdir -p "$(dirname $output_path)"
# Get the current user's home directory
user_home=$(eval echo ~$USER)
# Generate tree and replace the user's home directory with ~
tree $project_path --noreport | sed "s|$user_home|~|" > $output_path
Example usage of the gen-content.sh
script:
bash +x scripts/gen-content.sh $(pwd)
The script will create a file in content/tree.txt
with the output of the tree
command.
~/dev/lazydevschool.com/ed25519-signing
├── content
│ └── tree.txt
├── init-project.sh
├── keys
│ ├── allowed_signers
│ ├── id_ed25519
│ └── id_ed25519.pub
├── main.sh
└── scripts
├── gen-content.sh
├── gen-keypair.sh
├── init-keys-dir.sh
├── sign-content.sh
└── verify-content.sh
Sign Content
Ok we made it to the fun part.
Lets sign the content using our private key.
The expected output from this step is a signature file. The signature file will become one of the inputs in the verification process.
Update scripts/sign-content.sh
with the following.
#!/usr/bin/env bash
project_path=$1
key_name=$2
namespace=$3
content_path=$project_path/content/tree.txt
private_key_path=$project_path/keys/$key_name
cat $content_path | \
ssh-keygen -Y sign \
-f $private_key_path \
-n $namespace -q > $content_path.sig
The scripts/sign-content.sh
requires 3 arguments.
- The project path
$(pwd)
- The private key name
id_ed25519
- The namespace
author:content
Namespace
The namespace is the least intuitive part of this process (at least it was for me).
Think of the namespace as way to categorize the keys signing the content. For example, if we were running a content platform we might have authors and advertisers on the platform. We want to be able to prove content was signed by an author or signed by a advertiser.
The namespace is how we would accomplish that.
How do you know what namespace to use?
Well that is up to you and your team to agree on. It should be noted that the namespace will be part of the signed content so you will want to make sure it is descriptive and consistent.
Here is an example usage of the sign-content.sh
script.
bash +x scripts/sign-content.sh \
$(pwd) \
"id_ed25519" \
"author:content"
Verify Content
Up to this point you should have ed25519 keypair, content, and signature.
At a high level here is how we do the verification.
Invoke ssh-keygen -Y verify
with the following arguments:
allowed_signers
file path- The identity used to signed the content
user@domain
- Signature file
tree.txt.sig
- Namespace
author:content
(the role of the key) - Content we want to verify the signature against
tree.txt
Update scripts/verify-content.sh
with the following.
#!/usr/bin/env bash
project_path=$1
identity=$2
namespace=$3
allowed_signers=$project_path/keys/allowed_signers
content_path=$project_path/content/tree.txt
ssh-keygen -Y verify \
-f $allowed_signers \
-I $identity \
-n $namespace \
-s $content_path.sig < $content_path
<
is a more direct way to send file content to ssh-keygen
(Command Redirection). It is the same as running the following command:
cat $content_path | ssh-keygen -Y verify \
-f $allowed_signers \
-I $identity \
-n $namespace \
-s $content_path.sig
The scripts/verify-content.sh
script requires 3 arguments.
- The project path
$(pwd)
- The identity we are told who signed the content
user@domain
- The namespace
author:content
Example usage of the scripts/verify-content.sh
script.
bash +x scripts/verify-content.sh \
$(pwd) \
"user@domain" \
"author:content"
If all goes according to plan you should see similar output.
Good "author:content" signature for user@domain with ED25519 key SHA256:XP0TZ1Rcnfnf86+rlZEUTH6W9UBdzNUiYnmkABd9q5U
You can extract the public key from the private key with the following command. See how the public key matches the key in your allowed_signers
file?
ssh-keygen -y -f keys/id_ed25519
You can extract the fingerprint of the public key with the following command. See how the fingerprint matches the output from your verification command?
ssh-keygen -lf keys/id_ed25519.pub
Wrapping Up
You made it! You have now signed and verified content with an ed25519 keypair.
If you are still not clear on how the signing works here is what I recomend.
- Create a new keypair and add it to the
allowed_signers
file. Don’t forget to include a unique identity. - Sign the same content with your new keypair
- Try to verify the content using the signature from the second signing but using the identity of the first keypair.
Bonus Step
Try creating a second keypair and sign the same content.
Look at how different the signatures are given the same content.
diff -u content/tree.txt.sig1 content/tree.txt.sig2
Closing Remarks
I hope this post helped you understand a bit more about ed25519 keypairs and how to sign and verify content with them.
A few more ideas from where to go from here:
- How to handle key versioning
- How to handle revoked keys