Learn › Signing ›
Written for Rasputin 2026.08.5
Why a second signing key needs its own purpose
Intermediate · about 35 minutes · hands-on lab in a folder you create
Before this: What a signature proves that a checksum does not.
In the track: DevOps.
What you need
- The beginner lesson, What a signature proves that a checksum does not.
It covers key pairs, signing a file and verifying it with
openssl. - A terminal on macOS or Linux. On Windows, WSL (Windows Subsystem for Linux) gives you a
Linux terminal. The lab was run on macOS 15 with its built-in
openssl(LibreSSL 3.3.6) and with OpenSSL 3.6.3, and on Debian 13 with OpenSSL 3.5.7, with the same output on all three. Other Linux distributions and Windows were not tested. openssl, plussh,cd,mkdir,cat,printf,grepandrm. Typeopenssl versionand press Return. Every Mac has it, as LibreSSL. Most Linux systems have it; if not, install the packageopenssl, which needs administrator rights. Nothing else needs them.
You do not need Rasputin hardware, Rasputin code, or an account anywhere.
The situation
A signing hierarchy is a chain of certificates used to sign software. At the top is a root, which every machine carries. Below it, an intermediate: a CA the root signed. Below that, leaves: the certificates whose keys actually sign releases. A machine accepts a release when the leaf that signed it chains up to the root.
Rasputin runs a small group of computers at home. Its machines carry one root, and its release signatures embed the leaf and the intermediate, so the root alone completes the check. The records describe that shape; none of them argues for it or weighs another.
Then a second thing needed signing. The app catalog, the list of apps a cluster can install, moved out of the software into its own repository, built by its own pipeline. Whatever key signs the catalog lives in that pipeline.
The decision, and what lost
The catalog got its own leaf under the same intermediate, and that leaf carries a purpose. A certificate can list what its key may be used for, in a field called extended key usage. Each purpose is an object identifier (OID), a dotted string of numbers naming something in a certificate. Rasputin defines two: a release purpose and a catalog purpose.
The decision record, ADR-0006, gives the goal: “a compromise of catalog-signing CI must not imply
the ability to sign” a system update. CI (continuous integration) is the pipeline that builds and
signs. The manual states what a cluster checks before it loads a catalog: “that the signing
certificate chains to the cluster’s trust root, and that it carries Rasputin’s catalog-signing
purpose.” A code comment in
eku.go
states the other half: a leaf carrying only the catalog purpose “must never satisfy” the update
path.
| Alternative | Why it lost |
|---|---|
| Sign the catalog with the release leaf | The catalog’s pipeline would hold a key that signs system updates. |
| A separate leaf, verifiers unchanged | This was the decision as first accepted. An amendment to the record found that the check looked only at the chain, so the new leaf was “cryptographically equivalent to the release leaf”. |
| A separate root for the catalog | The amendment calls it one that “would also work and is arguably stronger”, and rejects the “permanent human overhead for a property an enforced check already delivers”: a second root to create, ship and look after. |
What it cost
The separation lives in every verifier. The decision as first accepted, in the amendment’s words, “asserted a property the system does not have.” An automated security review of a code change found that before any catalog leaf existed. The amendment also fixed the order of the work: the check changes first, because “minting first leaves a window in which the catalog key is a universal signing key.”
A number nobody else uses. A purpose needs an OID. The code comment records two dead ends. The
UUID branch, 2.25, needs no registration, but Go’s usual OID type cannot hold a number that large.
Inventing a plausible number “is squatting on whoever holds it.” Geekdojo waited for a registered
number from IANA, the body that assigns them.
One root still holds both. A leaf’s purpose is inside a certificate the intermediate signed, so “a holder of the catalog leaf’s private key cannot grant themselves the release purpose without the intermediate key.” Both purposes therefore rest on that one intermediate key. The record does not say why a separate root is stronger; plausibly, it would not share that key.
What Rasputin does not do here
A catalog you sign yourself is refused. The manual says running your own means “running your own signing chain and installing your own trust root on the node”, and calls that “a PKI project”.
Try it
You will build a root and an intermediate, then try three designs: one leaf, two unmarked leaves, and two marked leaves.
1. Make a sandbox, a root and an intermediate.
mkdir purpose-lab
cd purpose-lab
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout root.key -out root.pem -days 30 -subj "/CN=Lab Root" 2>/dev/null
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout mid.key -out mid.csr -subj "/CN=Lab Intermediate" 2>/dev/null
printf 'basicConstraints=critical,CA:TRUE\nkeyUsage=critical,keyCertSign\n' > mid.ext
openssl x509 -req -sha256 -in mid.csr -CA root.pem -CAkey root.key -CAcreateserial -days 30 -extfile mid.ext -out mid.pem 2>/dev/null
The first openssl line makes a root that signs itself; the second, a key and signing request
for the intermediate. mid.ext makes it a CA (CA:TRUE) allowed to sign certificates
(keyCertSign), and the last line has the root sign it. 2>/dev/null hides progress lines, which
differ between LibreSSL and OpenSSL 3, and errors too: if a later step fails, check your typing.
2. Write a script that makes leaves.
cat > leaf.sh <<'EOF'
case $2 in
any) mark=codeSigning ;;
release) mark=2.25.7751002667111313490255348551610639636 ;;
catalog) mark=2.25.103120496973671545358755251608534931499 ;;
esac
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout "$1.key" -out "$1.csr" -subj "/CN=$1" 2>/dev/null
printf 'basicConstraints=critical,CA:FALSE\nextendedKeyUsage=%s\n' "$mark" > "$1.ext"
openssl x509 -req -sha256 -in "$1.csr" -CA mid.pem -CAkey mid.key -CAcreateserial -days 7 -extfile "$1.ext" -out "$1.pem" 2>/dev/null
echo "made $1, marked $2"
EOF
sh leaf.sh <name> <mark> has the intermediate sign a leaf. any is the standard, generic
code-signing mark. release and catalog are purposes under 2.25, made from random UUIDs; fine
in a sandbox, where nothing escapes. CA:FALSE means a leaf cannot sign certificates.
3. Write the verifier.
cat > install.sh <<'EOF'
file=$1
signer=$2
need=$3
if ! openssl verify -CAfile root.pem -untrusted mid.pem "$signer.pem" > /dev/null 2>&1; then
echo "$file: refused, $signer does not chain to the root"
exit
fi
openssl x509 -in "$signer.pem" -pubkey -noout > signer.pub
if ! openssl dgst -sha256 -verify signer.pub -signature "$file.sig" "$file" > /dev/null 2>&1; then
echo "$file: refused, bad signature"
exit
fi
case $need in
release) mark=2.25.7751002667111313490255348551610639636 ;;
catalog) mark=2.25.103120496973671545358755251608534931499 ;;
*) echo "$file: installed, signed by $signer"; exit ;;
esac
if openssl x509 -in "$signer.pem" -noout -text | grep -q "$mark"; then
echo "$file: installed, $signer is marked $need"
else
echo "$file: refused, $signer is not marked $need"
fi
EOF
sh install.sh <file> <signer> [purpose] checks in order. verify -untrusted mid.pem builds the
chain through the intermediate to root.pem. x509 -pubkey extracts the leaf’s public key for
dgst -verify. Given a purpose, grep -q looks for it in the certificate’s text.
4. One leaf for everything.
printf 'system image v2\n' > system.img
printf 'app list v7\n' > apps.json
sh leaf.sh one any
openssl dgst -sha256 -sign one.key -out system.img.sig system.img
openssl dgst -sha256 -sign one.key -out apps.json.sig apps.json
sh install.sh system.img one
sh install.sh apps.json one
made one, marked any
system.img: installed, signed by one
apps.json: installed, signed by one
It works. The catalog’s pipeline now holds the key that signs system images.
5. Two leaves, same verifier. Predict: the catalog’s own leaf signs a system image. Installed or refused?
sh leaf.sh release-leaf any
sh leaf.sh catalog-leaf any
openssl dgst -sha256 -sign catalog-leaf.key -out system.img.sig system.img
sh install.sh system.img catalog-leaf
made release-leaf, marked any
made catalog-leaf, marked any
system.img: installed, signed by catalog-leaf
Installed. Two keys, one power. This is the design the amendment corrected.
6. Mark the purposes, and check them. Running the script again replaces both leaves, with new keys.
sh leaf.sh release-leaf release
sh leaf.sh catalog-leaf catalog
openssl dgst -sha256 -sign catalog-leaf.key -out system.img.sig system.img
sh install.sh system.img catalog-leaf release
openssl dgst -sha256 -sign release-leaf.key -out system.img.sig system.img
sh install.sh system.img release-leaf release
made release-leaf, marked release
made catalog-leaf, marked catalog
system.img: refused, catalog-leaf is not marked release
system.img: installed, release-leaf is marked release
The catalog leaf chains to the root and its signature is good. It is refused only because it is not marked for this job.
7. Forget to ask. Sign with the catalog leaf again, and leave the purpose off:
openssl dgst -sha256 -sign catalog-leaf.key -out system.img.sig system.img
sh install.sh system.img catalog-leaf
system.img: installed, signed by catalog-leaf
The mark is still in the certificate, and nothing read it. That is the first cost above: the separation exists only in a verifier that checks.
8. Clean up.
cd ..
rm -rf purpose-lab
rm -rf deletes the folder and everything in it, keys included. It cannot be undone, so check
you typed purpose-lab.
Check yourself
- In step 5, the catalog leaf was a different key from the release leaf. Why was the system image still installed?
- What breaks if the catalog leaf is created, and handed to its pipeline, before the verifiers check purposes?
- Why can the holder of the catalog leaf’s key not simply add the release purpose to it?
- You sign both firmware and plug-ins for a product. You rule out one shared leaf. What does each remaining option, marked purposes or a separate root, cost you?
Answers
- The verifier asked only whether the signer chained to the root, and both leaves did.
- For that window, the catalog key signs anything any machine accepts, which the record calls a universal signing key.
- The purpose is inside the certificate, which the intermediate signed. Changing it breaks that signature, and only the intermediate’s key can sign a new one.
- Marked purposes cost a purpose check in every verifier, in place before the second leaf exists, and both purposes still rest on one intermediate key. A separate root costs a second root on every machine, and someone to look after it for as long as the product ships.
Where to go next
- The catalog’s signature check: Running your own catalog, in Rasputin’s manual.
- What a release signature protects: Signing and the trust root.
- Two kinds of certificate authority: Why a browser warns about a private certificate authority.
- The code: the purposes and their history in
eku.go.
Reviewed 2026-09-12. Something wrong or unclear in this lesson? Tell us.