Public key cryptography is the foundation of modern digital security, enabling secure communication over insecure channels. This guide explores the fundamental concepts through practical examples and real-world implementations.
The Security Challenge
Let’s establish a scenario to understand the core problems public key cryptography solves:
My name is Clifford Smith and your name is Reginald Noble.
We need to solve four fundamental security problems:
- I want to communicate with you securely
- I want to make sure that the intended target of a message is the only one able to read it
- I want to make sure that you can tell if the message I sent you was altered
- I want to make sure that you can tell if I sent you the message
I want to communicate with you securely.
Each player will generate a public/private key pair.
Each player will keep their private key private, and share
their public key with everyone.
Public keys can be exchanged using the PEM format which is a format that can be distributed as text.
E.g.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAye5l5dBxJsKLYU99ZYhT
cO68MCl3WjZE7LEazOFFPWr5Rvy//hKQr1qwqZXAvOONlQ6Ua6HIqYT8da8SgNZr
HpPKv604SSL4PAFA0/zMv8ItJN688C2vt0UdYROPiMSHwAGQI6Lt+bIVQAY+l0ti
M9vor+pXKnLcm+bBqY2KWtOzgiLmGuz33T6R9uetA7ZORcmvd2cPZGemmvTMz55O
FOQnqNyoEbe/2g+yOF4DtjBeF9voxCkT8xfENOjNOsAPdALZE4tHRCUnU/Q6h4Ro
aYp5D/jmh2nFEgJnfCCuIGkau6aVyxYByzQXz22i8Fw99MhRCJBw5Sd2DxVIkbXo
2QIDAQAB
-----END PUBLIC KEY-----
Important terms:
- plaintext: this is the text that is sent before any form of crypto is applied.
- ciphertext: this is the encrypted form of the plaintext.
To communicate securely we must only transfer ciphertext over
the wire. This means the transport layer may or may not be a
secure channel.
A challenging part of this is to securely exchange key material. .i.e, when you receive my public key can you be sure that it is actually mine.
I want to make sure that the intended target of a message is the only one able to read it.
In order for Clifford to send an encrypted message to Reginald he
must encrypt the plaintext message with Reginald’s public key.
Only Reginald, will be able to decrypt the ciphertext with his private
key. In theory, this means that we can share the message with anyone
and only Reginald should be able to decrypt it.
#!/bin/env ruby
require 'openssl'
require 'base64'
class Player
attr_reader :public_key
def initialize(private_key = OpenSSL::PKey::RSA.new(2048))
@private_key = private_key
@public_key = private_key.public_key
end
def send_to(player, plaintext)
ciphertext = player.public_key.public_encrypt(plaintext)
puts "Sending: #{Base64.strict_encode64(ciphertext)}"
player.receive_from(self, ciphertext)
end
def receive_from(player, ciphertext)
plaintext = @private_key.private_decrypt(ciphertext)
puts "Received: #{plaintext}\n"
end
end
clifford = Player.new
reginald = Player.new
clifford.send_to(reginald, "Will there be a `How high 2'?")
reginald.send_to(clifford, "Maybe!")
When we run this small program we can see that only the ciphertext is
ever transmitted from one player to another. Each player has access to
the other players public key. Each player’s private key remains private.
モ ruby _includes/pk-secure-transfer.rb
Sending: C97jOIMeTFcDtlrPvAtu9OxfQK8QbI4uw+GzdYkb89PZb4YrAOFAgOehaeFSLIZ/u/7Higkt5ovzwJs1+OIwwFTlXAVVYbBawCSKn0qXISYEqVfGg6gHVgHNB20Khm2Tn5mntBIKgVOT/D/CUsbKQEpyiaeqkYHsK9ZvRurdmWzUBpIOAW1/EtA57Sbu6zPpcvALNp2mnsLTcuWGc5LNkDroRmmFjYTM4lPy7yza1rNyHsE5tZ/HUdIMt9Zw9DCKQlAqlHW2mW93Y6GKa7ObuA3S3hS4U1fLUq2F43L794GMAQyYmVNEgOPZuX6CuCAi0YoqigWcEOclJT2hzWixjA==
Received: Will there be a `How high 2'?
Sending: kUjvTSlW48mJk7X8GAb5x0Dm7r0WKlA0z0VJ6LmdqqKJyzJgX7FVKZrXnh5l9mm5b4CQjS+wFJtjJF0B7IJKjDUq78bo6Hkq7C/iXgklHwgFet+969p/2N4za+SOrt2OmkaQSi9ElJUFVh1PA509zJWXxbMIq+uVKfYhsv0urOg1AeEzgPI4JnIX+SppAQz5wNx1YS5csNPkkyqU0Bp/i1GNyFK8wcuD1ZuSnqkrRPIjkF3hfAKhTRHALZwC4SPoMhsVyoRa6ZPNKI6DXJxobgiIRfG+VNvfktfEXjxp4v5KXhDqZrH2MjOUiNyWVexOx441BBhYxiIWz6lt/TXMAg==
Received: Maybe!
I want to make sure that you can tell if the message I sent you was altered.
To make sure that the plaintext was not altered, we can
include a signature. A signature is usually a hash of something that
can be verified. The hash is then encrypted with a player’s private
key so that the other player can decrypt it with the sending players
public key.
Let’s look at an example of how we can attack the communication without a signature.
In the following example an attacker has hijacked the plaintext message and reversed it before forwarding to the recipient.
#!/bin/env ruby
require 'openssl'
class Player
attr_reader :public_key
def initialize(private_key = OpenSSL::PKey::RSA.new(2048))
@private_key = private_key
@public_key = private_key.public_key
end
def send_to(player, plaintext)
puts "Sent: #{plaintext}"
player.receive_from(self, plaintext)
end
def receive_from(player, plaintext)
puts "Received: #{plaintext}\n"
end
end
class Attacker
attr_reader :player
def initialize(player)
@player = player
end
def receive_from(player, plaintext)
# reverse the original plaintext message
player.receive_from(player, plaintext.reverse)
end
end
clifford = Player.new
reginald = Player.new
clifford.send_to(Attacker.new(reginald), "Will there be a `How high 2'?")
も ruby _includes/pk-altered-transfer.rb
Sent: Will there be a `How high 2'?
Received: ?'2 hgih woH` a eb ereht lliW
For Reginald to know that the message wasn’t altered, we can include a signature of the original message. Reginald can validate the signature and detect when a message has been tampered with.
#!/bin/env ruby
require 'openssl'
class Player
attr_reader :public_key
def initialize(private_key = OpenSSL::PKey::RSA.new(2048))
@private_key = private_key
@public_key = private_key.public_key
end
def send_to(player, plaintext)
puts "Sent: #{plaintext}"
signature = @private_key.private_encrypt(Digest::SHA1.hexdigest(plaintext))
player.receive_from(self, "#{plaintext}:#{signature}")
end
def receive_from(player, message)
plaintext, signature = message.split(':', 2)
expected_sha1 = Digest::SHA1.hexdigest(plaintext)
actual_sha1 = player.public_key.public_decrypt(signature)
if actual_sha1 == expected_sha1
puts "Received: #{plaintext}\n"
else
puts "ERROR: This message has been altered"
end
end
end
class Attacker
attr_reader :player
def initialize(player)
@player = player
end
def receive_from(player, message)
_, signature = message.split(':', 2)
player.receive_from(player, "Gimme the loot:#{signature}")
end
end
clifford = Player.new
reginald = Player.new
clifford.send_to(reginald, "Hi, this is Clifford.")
clifford.send_to(Attacker.new(reginald), "Will there be a `How high 2'?")
も ruby _includes/pk-signed-transfer.rb
Sent: Hi, this is Clifford.
Received: Hi, this is Clifford.
Sent: Will there be a `How high 2'?
ERROR: This message has been altered
I want to make sure that you can tell if I sent you the message.
This part was solved when we added a signature. We need to ensure that the public key for each player was exchanged ahead of time using a secure transport. Each player can then verify the signature of each message using the public key was provided via the previously secured public key exchange. So if an attacker modifies a message and generates a new signature it wont matter unless they were able to generate the signature using the attacked players private key.
Now putting it all together, we can encrypt the message so that if the message is intercepted it becomes difficult to decrypt. We can add a signature so that each party can verify the message hasn’t been tampered with and came from the expected player.
#!/bin/env ruby
require 'openssl'
require 'base64'
class Player
attr_reader :public_key
def initialize(private_key = OpenSSL::PKey::RSA.new(2048))
@private_key = private_key
@public_key = private_key.public_key
end
def send_to(player, plaintext)
ciphertext = player.public_key.public_encrypt(plaintext)
signature = @private_key.private_encrypt(Digest::SHA1.hexdigest(plaintext))
player.receive_from(self, "#{Base64.encode64(ciphertext)}:#{signature}")
end
def receive_from(player, message)
encodedtext, signature = message.split(':', 2)
ciphertext = Base64.decode64(encodedtext)
plaintext = @private_key.private_decrypt(ciphertext)
expected_sha1 = Digest::SHA1.hexdigest(plaintext)
actual_sha1 = player.public_key.public_decrypt(signature)
if actual_sha1 == expected_sha1
puts "Received: #{plaintext}\n"
else
puts "ERROR: This message has been altered"
end
end
end
clifford = Player.new
reginald = Player.new
clifford.send_to(reginald, "Hi, this is Clifford.")
reginald.send_to(clifford, "This is Reginald.")
も ruby _includes/pk-secure-signed-transfer.rb
Received: Hi, this is Clifford.
Received: This is Reginald.
Real-World Application: JWT Token Signing
Here’s how these public key cryptography principles apply in modern authentication systems. This example is from my OAuth2 server implementation:
Token Signing and Verification
# app/models/token.rb - JWT token generation with RSA signing
class Token < ApplicationRecord
def self.generate_jwt(payload)
# Sign JWT with private key
JWT.encode(payload, private_key, 'RS256', kid: key_id)
end
def self.verify_jwt(token)
# Verify JWT with public key
payload, = JWT.decode(
token,
public_key,
true,
{
algorithm: 'RS256',
verify_aud: true,
verify_iss: true,
verify_expiration: true
}
)
payload
rescue JWT::DecodeError => e
Rails.logger.error "JWT verification failed: #{e.message}"
nil
end
private
def self.private_key
@private_key ||= OpenSSL::PKey::RSA.new(ENV.fetch('JWT_PRIVATE_KEY'))
end
def self.public_key
@public_key ||= OpenSSL::PKey::RSA.new(ENV.fetch('JWT_PUBLIC_KEY'))
end
def self.key_id
# Generate key ID from public key fingerprint
Digest::SHA256.hexdigest(public_key.to_pem)[0..7]
end
end
JWKS (JSON Web Key Set) Endpoint
# app/controllers/jwks_controller.rb - Public key distribution
class JwksController < ApplicationController
def index
render json: {
keys: [
{
kty: 'RSA',
use: 'sig',
kid: Token.key_id,
n: Base64.urlsafe_encode64(public_key.n.to_s(2), padding: false),
e: Base64.urlsafe_encode64(public_key.e.to_s(2), padding: false),
alg: 'RS256'
}
]
}
end
private
def public_key
@public_key ||= OpenSSL::PKey::RSA.new(ENV.fetch('JWT_PUBLIC_KEY'))
end
end
Microservice Token Validation
# Resource server validating tokens from authorization server
class TokenValidator
def initialize(jwks_url)
@jwks_url = jwks_url
@public_keys = {}
end
def validate_token(token)
header = JWT.decode(token, nil, false)[1]
key_id = header['kid']
public_key = fetch_public_key(key_id)
return { valid: false, error: 'Unknown key' } unless public_key
payload = JWT.decode(
token,
public_key,
true,
{
algorithm: 'RS256',
verify_expiration: true
}
).first
{
valid: true,
user_id: payload['sub'],
scopes: payload['scope']&.split(' ') || [],
expires_at: Time.at(payload['exp'])
}
rescue JWT::DecodeError => e
{ valid: false, error: e.message }
end
private
def fetch_public_key(key_id)
return @public_keys[key_id] if @public_keys[key_id]
# Fetch JWKS from authorization server
response = HTTP.get(@jwks_url)
jwks = JSON.parse(response.body)
# Find the specific key
key_data = jwks['keys'].find { |key| key['kid'] == key_id }
return nil unless key_data
# Convert JWK to OpenSSL key
@public_keys[key_id] = jwk_to_rsa(key_data)
end
def jwk_to_rsa(jwk)
# Convert JSON Web Key to RSA public key
n = Base64.urlsafe_decode64(jwk['n'])
e = Base64.urlsafe_decode64(jwk['e'])
key = OpenSSL::PKey::RSA.new
key.set_key(
OpenSSL::BN.new(n, 2),
OpenSSL::BN.new(e, 2),
nil
)
key
end
end
Advanced Cryptographic Patterns
Key Rotation Strategy
# Automated key rotation for zero-downtime security updates
class KeyRotationService
def rotate_keys!
# Generate new key pair
new_key = OpenSSL::PKey::RSA.generate(2048)
new_key_id = generate_key_id(new_key)
# Store new keys
Rails.application.credentials.jwt[:private_key] = new_key.to_pem
Rails.application.credentials.jwt[:public_key] = new_key.public_key.to_pem
Rails.application.credentials.jwt[:key_id] = new_key_id
# Update JWKS endpoint to include both old and new keys
update_jwks_with_new_key(new_key, new_key_id)
# Schedule old key removal after grace period
RemoveOldKeyJob.set(wait: 24.hours).perform_later
end
private
def generate_key_id(key)
Digest::SHA256.hexdigest(key.public_key.to_pem)[0..7]
end
def update_jwks_with_new_key(key, key_id)
# Implementation would update the JWKS endpoint
# to serve both old and new public keys during transition
end
end
Certificate Pinning for Enhanced Security
# Certificate pinning to prevent man-in-the-middle attacks
class SecureHttpClient
def initialize(expected_cert_fingerprint)
@expected_fingerprint = expected_cert_fingerprint
end
def get(url)
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# Verify certificate fingerprint
http.verify_callback = proc do |verify_ok, store_context|
cert = store_context.current_cert
fingerprint = Digest::SHA256.hexdigest(cert.to_der)
if fingerprint == @expected_fingerprint
true
else
Rails.logger.error "Certificate pinning failed: expected #{@expected_fingerprint}, got #{fingerprint}"
false
end
end
request = Net::HTTP::Get.new(uri)
http.request(request)
end
end
Production Implementation Examples
The cryptographic concepts demonstrated above are used in real-world applications. Here’s a complete implementation you can study:
GitHub Repository: https://github.com/xlgmokha/proof
Key cryptographic implementation files:
- Token Model - JWT signing and verification
- JWKS Controller - Public key distribution
- OAuth Token Controller - Secure token exchange
- Certificate Management - X.509 certificate handling
Run the complete implementation:
git clone https://github.com/xlgmokha/proof.git
cd proof
bundle install
rails db:setup
rails server
# Cryptographic endpoints available at:
# GET /.well-known/jwks.json - Public keys for JWT verification
# POST /oauth/token - Signed JWT token generation
# POST /oauth/introspect - Token signature validation
Test cryptographic operations:
# Get public keys for token verification
curl -X GET "http://localhost:3000/.well-known/jwks.json"
# Generate signed JWT token
curl -X POST "http://localhost:3000/oauth/token" \
-H "Authorization: Basic $(echo -n 'client_id:client_secret' | base64)" \
-d "grant_type=client_credentials&scope=read"
# Verify token signature
curl -X POST "http://localhost:3000/oauth/introspect" \
-H "Authorization: Basic $(echo -n 'client_id:client_secret' | base64)" \
-d "token=your_jwt_token"
Key Security Principles
- Never transmit private keys - Only public keys should be distributed
- Use strong key sizes - Minimum 2048-bit RSA or 256-bit ECC
- Implement key rotation - Regular key updates maintain security
- Validate everything - Always verify signatures and certificates
- Use secure random generators - Cryptographically secure randomness is essential
Modern Applications
Public key cryptography enables:
- TLS/SSL - Secure web communication
- SSH - Secure remote access
- JWT tokens - Stateless authentication
- SAML assertions - Federated identity
- Code signing - Software integrity verification
- Blockchain - Cryptocurrency and smart contracts
Conclusion
Using public key cryptography we can encrypt plaintext into ciphertext and create signatures that can be used to verify that messages have not been tampered with and came from a trusted party.
These cryptographic primitives form the foundation of modern digital security, enabling everything from secure web browsing to cryptocurrency transactions. Understanding these concepts is essential for implementing robust authentication and authorization systems.