Crypto on the Command Line, Part 5: Signatures
A signature runs the key pair the other way from encryption: the private half makes it, the public half checks
it. This post runs the signature scene of the
Security Primitives
tool with
openssl, one step per command:
- hashing the message;
- signing the digest with Bob's private key;
- reading the digest back out of the signature with his public key;
- verifying, as Alice does;
- the one-command form, which gives the same signature;
- Mallory's two attempts: an edited message, and her own key.
The tools on Ubuntu 26.04
The commands are shown as a terminal session:
$starts a command: type what follows it;>marks a line that continues the command above it, for a command too long for one line;- a line with neither is output. A command with no lines under it prints nothing.
openssl (3.5 on 26.04) and xxd, and Bob's key pair from
Part 3
:
1$ sudo apt update
2$ sudo apt install openssl xxd
3$ mkdir -p ~/crypto-lab && cd ~/crypto-lab
4$ openssl genpkey -quiet -algorithm RSA -out bob.key # skip if Part 3 made it
5$ openssl pkey -in bob.key -pubout -out bob.pub
Step 1: the digest
Bob signs the digest, not the message: 32 bytes, whatever the message's size
(
Part 1
). -binary writes the 32 bytes themselves:
1$ printf 'ship order 42' > order.txt
2$ openssl dgst -sha256 -binary order.txt > order.sha256
3$ xxd -p -c 32 order.sha256
41f551b4022596ac7b08713276ad95f03bbc09592eb1f47185fe017b49cf483ac
sha256sum order.txt prints the same digest.
Step 2: signing the digest
pkeyutl -sign transforms the digest with the private key. digest:sha256 hashes nothing: it tells openssl
which hash made the input, a name that goes into the signature beside it.
1$ openssl pkeyutl -sign -inkey bob.key -pkeyopt digest:sha256 \
2> -in order.sha256 -out order.sig
3$ wc -c order.sig
4256 order.sig
5$ xxd order.sig | head -2
600000000: 9699 a707 8254 d858 d1a7 7cbe 8c76 a05a .....T.X..|..v.Z
700000010: 0976 3b39 4db2 6372 525d 794c 0251 3f6c .v;9M.crR]yL.Q?l
The signature is 256 bytes, the size of the key, and looks random. Bob sends order.txt and order.sig to
Alice; the message travels in the clear, since signing hides nothing.
Step 3: the digest inside the signature
The public key undoes what the private key did, so anyone can open a signature and look inside.
-verifyrecover with rsa_padding_mode:none prints the block exactly as it was before signing:
1$ openssl pkeyutl -verifyrecover -pubin -inkey bob.pub \
2> -pkeyopt rsa_padding_mode:none -in order.sig | xxd
300000000: 0001 ffff ffff ffff ffff ffff ffff ffff ................
400000010: ffff ffff ffff ffff ffff ffff ffff ffff ................
500000020: ffff ffff ffff ffff ffff ffff ffff ffff ................
6...
7000000b0: ffff ffff ffff ffff ffff ffff ffff ffff ................
8000000c0: ffff ffff ffff ffff ffff ffff 0030 3130 .............010
9000000d0: 0d06 0960 8648 0165 0304 0201 0500 0420 ...`.H.e.......
10000000e0: 1f55 1b40 2259 6ac7 b087 1327 6ad9 5f03 .U.@"Yj....'j._.
11000000f0: bbc0 9592 eb1f 4718 5fe0 17b4 9cf4 83ac ......G._.......
The 256 bytes are, in order:
00 01, thenffbytes up to00: the padding (PKCS#1 v1.5), filling the block to the key's size;30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20: "a SHA-256 digest, 32 bytes, follows";1f55 1b40 ⊠83ac: the digest from step 1.
So signing is the private key's arithmetic applied to this block, and only the private key could make a signature that the public key turns back into it.
Step 4: verifying
Alice repeats the work she can do herself: she hashes the message she received. pkeyutl -verify opens the
signature with bob.pub and compares the digest inside it with hers:
1$ openssl dgst -sha256 -binary order.txt > order.sha256
2$ openssl pkeyutl -verify -pubin -inkey bob.pub -pkeyopt digest:sha256 \
3> -in order.sha256 -sigfile order.sig
4Signature Verified Successfully
Alice now knows two things, from bob.pub alone:
- the holder of
bob.keymade the signature; - the message is the one he signed, to the bit.
The one-command form
openssl dgst -sign hashes and signs in one command. It runs steps 1 and 2, and the signature is the same
bytes:
1$ openssl dgst -sha256 -sign bob.key -out order2.sig order.txt
2$ cmp order.sig order2.sig && echo same
3same
4$ openssl dgst -sha256 -verify bob.pub -signature order.sig order.txt
5Verified OK
Mallory edits the message
Mallory catches the message, changes the order to 99 and keeps Bob's signature. Alice hashes what arrived:
1$ printf 'ship order 99' > order.txt
2$ openssl dgst -sha256 -binary order.txt > order.sha256
3$ openssl pkeyutl -verify -pubin -inkey bob.pub -pkeyopt digest:sha256 \
4> -in order.sha256 -sigfile order.sig
5...:error:02000068:rsa routines:ossl_rsa_verify:bad signature:...
6...:error:1C880004:Provider routines:rsa_verify_directly:RSA lib:...
7...:error:030000EA:digital envelope routines:EVP_PKEY_verify:provider signature failure:...
8Signature Verification Failure
- The signature still opens with
bob.pub, and step 3 still shows1f551b40âŠ83acinside it. - The message now hashes to
1f8daf8aâŠe544: the two digests differ. - Hashing alone let Mallory through in the tool's second scene, since she could hash her own document. Here she
would need a new signature over the new digest, and that takes
bob.key.
Mallory signs with her own key
So she makes a key pair and signs the edited message with it:
1$ openssl genpkey -quiet -algorithm RSA -out mallory.key
2$ openssl pkeyutl -sign -inkey mallory.key -pkeyopt digest:sha256 \
3> -in order.sha256 -out mallory.sig
4$ openssl pkeyutl -verify -pubin -inkey bob.pub -pkeyopt digest:sha256 \
5> -in order.sha256 -sigfile mallory.sig
6...:error:0200008A:rsa routines:RSA_padding_check_PKCS1_type_1:invalid padding:...
7...:error:02000072:rsa routines:rsa_ossl_public_decrypt:padding check failed:...
8...:error:1C880004:Provider routines:rsa_verify_directly:RSA lib:...
9...:error:030000EA:digital envelope routines:EVP_PKEY_verify:provider signature failure:...
10Signature Verification Failure
- Opened with Bob's public key, her signature gives random bytes, not
00 01 ff âŠ(run step 3 onmallory.sigto see them). - With her own public key it verifies: it is a correct signature, only not Bob's.
- So Alice's check is only as good as her copy of
bob.pub. If Mallory had swapped that copy when Bob published it, her signatures would pass. A certificate ties a public key to a name; TLS Concepts shows how.
Other signature schemes
RSA-PSS pads with a random salt instead of the fixed
ffbytes, and is the choice for new designs. Add-pkeyopt rsa_padding_mode:pssto both the-signand the-verifycommand; each signature then differs, and step 3 shows no readable block.Ed25519 hashes inside the algorithm, so it signs the message itself.
-rawinpasses it whole, and the signature is 64 bytes:1$ openssl genpkey -algorithm ED25519 -out ed.key 2$ openssl pkey -in ed.key -pubout -out ed.pub 3$ openssl pkeyutl -sign -rawin -inkey ed.key -in order.txt -out ed.sig 4$ openssl pkeyutl -verify -rawin -pubin -inkey ed.pub -in order.txt -sigfile ed.sig 5Signature Verified SuccessfullyUbuntu's
SHA256SUMS.gpg, a Git commit signed with-Sand a JWT markedRS256orES256are all signatures over a digest, made this way.