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, then ff bytes up to 00: 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.key made 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 shows 1f551b40
83ac inside 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 on mallory.sig to 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 ff bytes, and is the choice for new designs. Add -pkeyopt rsa_padding_mode:pss to both the -sign and the -verify command; each signature then differs, and step 3 shows no readable block.

  • Ed25519 hashes inside the algorithm, so it signs the message itself. -rawin passes 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 Successfully
    
  • Ubuntu's SHA256SUMS.gpg, a Git commit signed with -S and a JWT marked RS256 or ES256 are all signatures over a digest, made this way.