[root@localhost /]# cyb3r.sh

Intro to Modern Cryptography

Brandon James

2026-06-20

This post is an expanded version of a talk I gave at NoCo Hackers in August 2025. The goal is to provide:

  1. A basic overview of how modern cryptography works
  2. Intuition for the math behind RSA
  3. Working knowledge of how cryptographic protocols are used today
  4. Basic knowledge about what TLS certificates are and how certificate chaining works

Many details will be left out in order to focus on broader concepts and the examples I use are “classic” concepts like RSA and DES as opposed to more recent innovations like ECDSA and AES. I also completely ignore post-quantum cryptography. I am not a cryptographer, but I apply many of these concepts in my day to day.

Each section includes hands-on exercises you can follow along with using standard linux utilities like openssl.

Some exercises look like this. Click to expand.

Click again to collapse.

Symmetric Encryption

Symmetric encryption uses a single key for both encryption and decryption. The implementation can vary quite a bit, but common algorithms include 3DES, Blowfish, AES and ChaCha20. If you work in tech, you’ve likely heard of some of these. Since you’re on my website, you either used ChaCha20 or AES to load this page1.

While the concept sounds similar to substitution ciphers2 many of us used on the playground as kids, substitution ciphers are vulnerable to techniques like frequency analysis and known plaintext attacks. Modern symmetric encryption prevents these methods from working by scrambling the output in addition to performing substitution.

Just how secure are these algorithms? Using today’s technology, 128-bit AES would take more than the current age of the universe to crack3. 256-bit AES is likely quantum safe, assuming no new quantum algorithms produce speed-ups beyond that of Grover’s algorithm4. They are quite good, assuming you can securely manage your keys.

Feistel Networks

Symmetric block ciphers such as DES and Blowfish are built on a structure called a Feistel network. An input block is split into left and right halves, and a round function F is applied repeatedly with different subkeys derived from the main key. Each round mixes the data further, and after enough rounds, the output is effectively indistinguishable from random noise.

Feistel network diagram

In the DES round function, the right half of the block is XORed with a subkey, then passed through S-boxes that substitute chunks of bits (providing confusion - obscuring the relationship between key and ciphertext), followed by a permutation that rearranges the bit positions (providing diffusion - spreading each input bit’s influence across the output).

DES inner function showing the s-boxes and bitwise permutation

Not all block ciphers are built on Feistel networks. For instance AES uses a much different architecture featuring a substitution-permutation network. The underlying idea is still the same. Take a block, substitute, permute and repeat.

Symmetric Encryption in Practice

In practice, Alice and Bob share a secret key. When Alice wants to send a message, she encrypts it with the shared key. The ciphertext travels across the network where Eve can see it, but without the key, it’s just gibberish. Bob decrypts it with his copy of the same key.

Alice and Bob communicating with symmetric encryption while Eve observes the ciphertext
Exercise: Symmetric Encryption with GPG

Let’s encrypt and decrypt a file using GPG with AES-256. First, create a file to encrypt:

┌─(you@workshop)-[~/tls_workshop]
└─$ echo "In the realm of cryptographic lore,
Live three souls we all adore,
Alice sends her secrets bright,
Bob receives them in the night." > poem.txt

Encrypt the file with AES-256. You’ll be prompted for a passphrase:

┌─(you@workshop)-[~/tls_workshop]
└─$ gpg --output poem.txt.enc --symmetric --cipher-algo AES256 poem.txt

Take a look at the encrypted file - it’s unreadable:

┌─(you@workshop)-[~/tls_workshop]
└─$ more poem.txt.enc
c^L)poO^LM"6!=U,(o_WoMYG\uibz/?sg[ö_B21'OH_7|#@?)D2

We can verify it’s actually AES-256 encrypted data:

┌─(you@workshop)-[~/tls_workshop]
└─$ file poem.txt.enc
poem.txt.enc: PGP symmetric key encrypted data - AES with 256-bit key salted & iterated - SHA512 .

Now decrypt it into a new file and verify the contents match:

┌─(you@workshop)-[~/tls_workshop]
└─$ gpg --output poem2.txt --decrypt poem.txt.enc
gpg: AES256.CFB encrypted data
gpg: encrypted with 1 passphrase
┌─(you@workshop)-[~/tls_workshop]
└─$ sha256sum poem.txt poem2.txt
421fa5b43f8cf383c66966c033343adc9c4cbba4119664d8c552713e806e7476  poem.txt
421fa5b43f8cf383c66966c033343adc9c4cbba4119664d8c552713e806e7476  poem2.txt

The SHA-256 hashes match - the decrypted file is identical to the original.

The Key Distribution Problem

There’s a fundamental issue with symmetric encryption: how do Alice and Bob agree on a shared key in the first place? If they can meet in person and exchange a key, great. But what if they’re on opposite sides of the internet and have never communicated before?

Alice handing Bob a pre-shared key while Eve watches from afar

They can’t just send the key over the network in plaintext - Eve would intercept it. They need a way to agree on a shared secret over an insecure channel. Enter Diffie-Hellman.

Diffie-Hellman Key Exchange

Diffie-Hellman allows two parties to establish a shared secret over an insecure channel. Here’s how it works:

  1. Alice and Bob agree on two public parameters: a large prime p and a generator g
  2. Alice picks a secret number a and computes A = ga mod p
  3. Bob picks a secret number b and computes B = gb mod p
  4. They exchange A and B publicly
  5. Alice computes the shared secret: s = Ba mod p
  6. Bob computes the shared secret: s = Ab mod p
  7. Both arrive at the same value of s
Diffie-Hellman key exchange between Alice and Bob, with Eve unable to derive the shared secret

A small example: with p = 23 and g = 5, if Alice picks a = 4 and Bob picks b = 3:

Both arrive at 18 without ever transmitting their secret values. Eve sees p, g, A and B, but cannot efficiently compute s.

Some important notes on parameter selection: p must be a prime number (2048-bit is the current best practice). g should be a primitive root of p (aka a generator). The choice of a and b are less important, but there are “weak” keys that should be avoided.

Exercise: Generate DH Parameters

Use OpenSSL to generate Diffie-Hellman parameters. This will take a moment as it needs to find a suitable large prime:

┌─(you@workshop)-[~]
└─$ openssl genpkey -algorithm dh -genparam -text

This outputs the DH parameters including the prime p and generator g. In practice, these parameters are negotiated as part of the TLS handshake.

Number Theory Notes - Why This Works

Diffie-Hellman works due to the Discrete Logarithm Problem:

Modular Arithmetic

Modular arithmetic is not intuitive. A few examples:

Try it yourself:

Primitive Roots

The selection of g is important. g is a primitive root modulo n if for every integer a coprime to n, there is some integer k for which gk ≡ a (mod p). While this is complicated to the point of nonsense without defining multiple terms, these numbers have an interesting property:

Explore this yourself. Try g = 2, p = 11 (a primitive root), then try g = 3, p = 11 (not a primitive root) and notice how 3 fails to produce all remainders:

Rows = k (exponent), Columns = remainders 1 to p-1. A lit cell means gk mod p equals that remainder. Primitive roots light every column.

The Scale of the Problem

If p is a 2048-bit integer such as:

300396648896056517967822789849851342061039737840497444694298029812868780991612
616546929927248598372661584089789602159266747365345985697362943273589428015143
016773149946315701041271641014512529437108063589612811583585838245047403956855
795246762650874918515999483338962432177317675184887766781763712231372305303916
152762734067057282167451220484749501274315183531761085778634261192080104872288
357240136773198003137851673885785239532397645643423686563947798011817173222358
195851978881109660891980662046175013305288876827537293648722280166850584485979
179022443434756151783609647880324673922252042888634076559224189862075342037

And g is a generator, there is no efficient way to solve for k.

Interactive Diffie-Hellman

Step through a Diffie-Hellman key exchange. Pick your parameters and secrets, then watch Alice and Bob arrive at the same shared secret while Eve is left in the dark.

RSA

RSA is an asymmetric encryption algorithm. Unlike symmetric encryption, it uses a pair of keys: one public, one private. It’s named after Rivest, Shamir and Adleman who published it in 19775.

Key Generation

The important parts:

  1. Alice chooses two prime numbers p and q
  2. She multiplies them together to yield N
  3. She selects another number e
  4. She finds d such that e × d = 1 mod (p - 1)(q - 1)

e and N should be made publicly available. These make up the public key.

p, q and d should be kept private. These make up the private key.

RSA key generation with Alice sharing her public key

RSA Encryption

Anyone can encrypt a message to Alice using her public key. Only Alice can decrypt it with her private key:

Bob encrypts a message with Alice’s public key, Alice decrypts with her private key

RSA Signing

RSA can also provide authentication and integrity. Alice can sign a message with her private key, and anyone with her public key can verify the signature:

Alice signs a message with her private key, Bob verifies with Alice’s public key

Notice that Eve can see the message (signing doesn’t encrypt it), but she can’t forge Alice’s signature without Alice’s private key. This gives Bob confidence the message actually came from Alice and hasn’t been tampered with.

Why RSA Works

RSA works because there is no known algorithm to efficiently find the prime factors of large composite numbers. When N is the product of two large primes (each 1024+ bits), factoring N to recover p and q is computationally infeasible with current technology.

Exercise: RSA with OpenSSL

Generate a 2048-bit RSA key pair:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl genrsa -out private.pem 2048

Extract the public key:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl rsa -in private.pem -pubout -out public.pem
writing RSA key

Encrypt a message with the public key:

┌─(you@workshop)-[~/tls_workshop]
└─$ echo "noco hackers" | openssl pkeyutl -encrypt -pubin -inkey public.pem -out ciphertext.out

Examine the ciphertext - it’s binary garbage:

┌─(you@workshop)-[~/tls_workshop]
└─$ xxd ciphertext.out | head -5
00000000: 6722 383e 758a 0788 f0ec d4e8 dfd6 5a55  g"8>u.........ZU
00000010: 1a47 3926 22bd ae79 952b 5719 9419 2b64  .G9&"..y.+W...+d
00000020: bd1e a72b 8eb1 b8bb f816 ea55 d63a 747e  ...+.......U.:t~
00000030: 2b00 c05b e245 140c 3023 63f4 0189 e522  +..[.E..0#c...."
00000040: 6fd4 c1ad ebf3 ee38 065d 626c b56a 6ad3  o......8.]bl.jj.

Decrypt it with the private key:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl pkeyutl -decrypt -inkey private.pem -in ciphertext.out
noco hackers

Hashing

Hash algorithms should be:

Let’s see this in action. Notice how changing a single character (“hackess” vs “hackers”) completely changes the hash:

┌─(bjames@llpt03)-[~/tls_workshop]
└─$ echo -n "noco hackess" | xxd -b
00000000: 01101110 01101111 01100011 01101111 00100000 01101000  noco h
00000006: 01100001 01100011 01101011 01100101 01110011 01110011  ackess
┌─(bjames@llpt03)-[~/tls_workshop]
└─$ echo -n "noco hackers" | xxd -b
00000000: 01101110 01101111 01100011 01101111 00100000 01101000  noco h
00000006: 01100001 01100011 01101011 01100101 01110010 01110011  ackers

Only a single bit differs between the two inputs (the s vs r), but the SHA-256 output is completely different:

┌─(bjames@llpt03)-[~/tls_workshop]
└─$ echo "noco hackers" | sha256sum
85a29cdad0e9979136366475928434e879fcdfd5f609d74d16852da053ced22c  -
┌─(bjames@llpt03)-[~/tls_workshop]
└─$ echo "noco hackess" | sha256sum
517163d51822495d2fcb7f163c2449284a5ed52ae3dfb2fc1ed4c818535f7232  -

This is the avalanche effect in action.

Merkle-Damgård construction showing how blocks are chained through a compression function

Many hash algorithms (including SHA-256) use the Merkle-Damgård construction. The message is broken into blocks, and each block is fed through a compression function F along with the output of the previous block. The first block uses an initialization vector (IV). This chaining ensures that every bit of the input affects the final hash.

Certificates

Anatomy of a Certificate

Cryptographic certificates are effectively:

  1. A container for a device’s public key
  2. A collection of metadata (subject name, issuer, validity period, etc.)
  3. A signature from a trusted authority validating:
    1. The metadata
    2. That the identity actually belongs to whoever requested the certificate be signed
Certificate anatomy showing subject, issuer, public key, and signature fields with certificate chain

Typically certificates are chained. Intermediate and leaf certificates are exchanged during the TLS handshake, while root certificates are deployed via OS/web browser updates or via some other automated mechanism (Active Directory, devops tools, SCEP, etc).

Certificate chain showing leaf cert signed by intermediate CA, signed by root CA

Ensuring Integrity

When a Certificate Authority (CA) processes a certificate signing request (CSR), it takes the public key and metadata from the CSR, then adds in some metadata of its own. Once complete, the CA uses its private key to sign the contents of the certificate.

The CA’s public key can then be used to validate that the certificate was A) signed by the CA in question and B) has not been tampered with.

Putting It All Together

TLS and by extension x.509 combine symmetric encryption, asymmetric encryption, key exchange protocols and hash algorithms. Here’s what a real TLS cipher suite looks like, scanned from cyb3r.sh:

┌─(bjames@llpt03)-[~]
└─$ nmap -sV --script ssl-enum-ciphers -p 443 cyb3r.sh
Starting Nmap 7.92 ( https://nmap.org ) at 2025-08-21 10:36 MDT
Nmap scan report for cyb3r.sh (165.227.88.41)
Host is up (0.0080s latency).

PORT    STATE SERVICE  VERSION
443/tcp open  ssl/http nginx 1.20.1
|_http-server-header: nginx/1.20.1
| ssl-enum-ciphers:
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|   TLSv1.3:
|     ciphers:
|       TLS_AKE_WITH_AES_128_CCM_SHA256 (secp256r1) - A
|       TLS_AKE_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_AKE_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_AKE_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
|_  least strength: A

Let’s break down TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:

Every concept from this post comes together in a single cipher suite name.

Exercise: Build a Certificate Chain

In this exercise, we’ll create our own certificate authority, an intermediate CA, and a server certificate - the same chain of trust your browser validates every time you visit an HTTPS site.

Step 1: Create a Root CA

Generate the root CA private key and self-signed certificate:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl req -x509 -newkey rsa:4096 -keyout root-key.pem -out root-cert.pem -days 365 \
    -subj "/CN=Workshop Root CA" -nodes

Step 2: Create an Intermediate CA

Generate the intermediate CA key and a certificate signing request (CSR):

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl req -newkey rsa:4096 -keyout intermediate-key.pem -out intermediate.csr \
    -subj "/CN=Workshop Intermediate CA" -nodes

Sign the intermediate CSR with the root CA:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl x509 -req -in intermediate.csr -CA root-cert.pem -CAkey root-key.pem \
    -CAcreateserial -out intermediate-cert.pem -days 180

Step 3: Create a Server Certificate

Generate a server key and CSR:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl req -newkey rsa:2048 -keyout server-key.pem -out server.csr \
    -subj "/CN=workshop.local" -nodes

Sign the server CSR with the intermediate CA:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl x509 -req -in server.csr -CA intermediate-cert.pem -CAkey intermediate-key.pem \
    -CAcreateserial -out server-cert.pem -days 90

Step 4: Verify the Chain

Now verify that the entire chain is valid:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl verify -CAfile root-cert.pem -untrusted intermediate-cert.pem server-cert.pem
server-cert.pem: OK

You can also inspect any certificate in the chain:

┌─(you@workshop)-[~/tls_workshop]
└─$ openssl x509 -in server-cert.pem -text -noout | head -15

This is fundamentally the same process that Let’s Encrypt, DigiCert and every other CA uses to issue certificates - just at a much larger scale with more rigorous identity verification.

Conclusion

We’ve covered symmetric encryption, key exchange, hashing, asymmetric encryption, and how they all come together in TLS. This post originated as a talk at NoCo Hackers, a meetup for anyone interested in technology, security and hacking in Northern Colorado. If you’re in the area, come hang out with us.


  1. Unless my cipher suites have changed since the time of writing, you used either AES-128-GCM, AES-256-GCM or ChaCha20-Poly1305 to load this page.↩︎

  2. GRPQ FK ZXPB FQP KLQ LYSFLRP COLJ QEB KXJB, QEFP FP X PRYPFQRQFLK ZFMEBO↩︎

  3. Stack exchange is a reliable source right?↩︎

  4. Seriously, do I cite anything other than stack exchange?↩︎

  5. The algorithm was independently discovered by Clifford Cocks at GCHQ in 1973, but was classified until 1997.↩︎