Crypto on the Command Line, Part 3: Key Pairs

A key pair is two numbers tied by a mathematical relation: what one half does, only the other undoes. This post makes Bob's key pair from the Security Primitives tool with openssl:

  • making the private key;
  • looking at the numbers inside it, and checking their relation;
  • computing the public half from it;
  • naming a public key by its fingerprint;
  • locking the private key with a passphrase.

Parts 4 and 5 use the pair made here.

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); python3 for one check, installed on every desktop. If either is missing:

1$ sudo apt update
2$ sudo apt install openssl python3
3$ mkdir -p ~/crypto-lab && cd ~/crypto-lab

Making the private key

genpkey makes a key of the algorithm named; 2048 bits is openssl's default size for RSA:

1$ openssl genpkey -quiet -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out bob.key
2$ ls -l bob.key
3-rw------- 1 yuval yuval 1700 Oct 12 14:17 bob.key
4$ head -3 bob.key
5-----BEGIN PRIVATE KEY-----
6MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDEIE+iLG2UeRTV
7QfRhipGiTfjFFvq7R44QqLdsFP4+nlRWbTq7dTRV8BFskYvcG+jxkJK3Ki5BEqdK
  • Only its owner can read it (-rw-------): openssl writes a private key that way.
  • The file is PEM: the key's binary form (DER), in base64, between two marker lines.
  • Without -quiet, genpkey prints dots and pluses while it searches for two large primes.

The numbers inside the key

-text prints the key's numbers in hex. Their names alone:

 1$ openssl pkey -in bob.key -text -noout | grep -v '^ '
 2Private-Key: (2048 bit, 2 primes)
 3modulus:
 4publicExponent: 65537 (0x10001)
 5privateExponent:
 6prime1:
 7prime2:
 8exponent1:
 9exponent2:
10coefficient:
  • prime1 and prime2 (p and q) are two random primes of 1024 bits each.
  • modulus (n) is their product, 2048 bits: the key's size.
  • publicExponent (e) is 65537 in almost every RSA key.
  • privateExponent (d) is computed from e, p and q, so that raising a number to the power e and then d (modulo n) gives the number back.
  • exponent1, exponent2 and coefficient are d split up by p and q: a shortcut that makes the private key's work about four times faster.

A short Python program reads the numbers back and checks the relation:

 1$ openssl pkey -in bob.key -text -noout > bob.txt
 2$ python3 - bob.txt <<'EOF'
 3> import re, sys
 4> text = open(sys.argv[1]).read()
 5> def num(name):
 6>     hexdigits = re.search(name + r":\n((?:    .*\n)+)", text).group(1)
 7>     return int(re.sub(r"[\s:]", "", hexdigits), 16)
 8> n, e, d = num("modulus"), 65537, num("privateExponent")
 9> p, q = num("prime1"), num("prime2")
10> print("p x q == n:", p * q == n)
11> print("digits in n:", len(str(n)))
12> c = pow(42, e, n)
13> print("42 locked with e:", str(c)[:20] + "...")
14> print("unlocked with d:", pow(c, d, n))
15> EOF
16p x q == n: True
17digits in n: 617
1842 locked with e: 20571338879645799541...
19unlocked with d: 42
20$ rm bob.txt
  • Computing d needs p and q. Anyone holding n alone would have to factor a 617-digit number to find them, and that is out of reach.
  • RSA used this way, on a bare number, is only the arithmetic. Real encryption and signing first pad the number, as Parts 4 and 5 show.

The public half

The public key is n and e, copied out of the private key:

 1$ openssl pkey -in bob.key -pubout -out bob.pub
 2$ cat bob.pub
 3-----BEGIN PUBLIC KEY-----
 4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCBPoixtlHkU1UH0YYqR
 5ok34xRb6u0eOEKi3bBT+Pp5UVm06u3U0VfARbJGL3Bvo8ZCStyouQRKnSlWi8zEO
 6w6JUAK6ayJtXkMFG3gFqFailpcPvnAde4toadxWIuTlGOv/aNMuXNvrqlP7IgTel
 7pP6wnSuEZxysJYCP3VzI9od0YSkH0HsMj2e9Mo/w3PkQLTy5h+DYguuti8x+IB1b
 8XKgf7583tw0KIYAA2x6GHQBohIlVyeOWbkFaR3xQueo/Xpdf9VFasNsICZel1uTs
 9lKWgh/LGUteULRL33OkCGYgtQShMMvztTYm3BFNWmxG8gvpNmnVWB4czRPQczLva
10JQIDAQAB
11-----END PUBLIC KEY-----
12$ openssl pkey -pubin -in bob.pub -text -noout | grep -v '^ '
13Public-Key: (2048 bit)
14Modulus:
15Exponent: 65537 (0x10001)
  • Computing it again gives the same bytes: openssl pkey -in bob.key -pubout | cmp - bob.pub prints nothing. Nothing random went in; it is a copy of two numbers.
  • Going back is out of reach: the public file holds neither prime.
  • bob.pub is the file Bob publishes. Alice, Eve and Mallory all get a copy.

Checking that two halves belong together

Both files hold the same modulus, and comparing it is the usual check that a public key matches a private one:

1$ openssl rsa -in bob.key -noout -modulus | sha256sum
2bbed2911adac8926d54171e89abaa6fe865b7692230ef003fdba7acd071d0432  -
3$ openssl rsa -pubin -in bob.pub -noout -modulus | sha256sum
4bbed2911adac8926d54171e89abaa6fe865b7692230ef003fdba7acd071d0432  -

The two digests are equal. A web server that refuses to start with "key values mismatch" failed this check between its certificate and its key.

A public key's fingerprint

A public key is 294 bytes in its binary form, too long to compare by eye. Its SHA-256 digest ( Part 1 ) names it in 32:

1$ openssl pkey -pubin -in bob.pub -outform DER | sha256sum
2239dcc89b331c570fe15d10efe031877ed6b2ccd090b9f8d06bb50108259b93c  -
  • SSH's "key fingerprint" and a certificate's key pinning are digests like this one.
  • Reading the fingerprint to Alice over the phone is one way for her to know bob.pub is Bob's and not Mallory's.

Locking the private key with a passphrase

A private key in a plain file is read by anyone who copies the file. genpkey can encrypt it with AES, the key coming from a passphrase:

1$ openssl genpkey -quiet -algorithm RSA -aes-256-cbc -out locked.key
2Enter PEM pass phrase:
3Verifying - Enter PEM pass phrase:
4$ head -1 locked.key
5-----BEGIN ENCRYPTED PRIVATE KEY-----
  • Every later use asks for the passphrase.
  • A wrong passphrase fails with bad decrypt: the AES padding check from Part 2 .

Other kinds of key pair

Algorithmgenpkey -algorithmUsed forKey size
RSARSAencryption and signatures2048–4096 bits
Ed25519ED25519signatures only32 bytes
X25519X25519key agreement only32 bytes

Ed25519 and X25519 are built on an elliptic curve, not on primes, and are much smaller and faster than RSA. Part 7 uses X25519.