~/src/www.mokhan.ca/xlgmokha [main]
cat public-key-cryptography.md
public-key-cryptography.md 54431 bytes | 2018-11-21 00:00
symlink: /dev/eng/public-key-cryptography.md

Public Key Cryptography - Complete Guide to Secure Communication

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:

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

  1. Never transmit private keys - Only public keys should be distributed
  2. Use strong key sizes - Minimum 2048-bit RSA or 256-bit ECC
  3. Implement key rotation - Regular key updates maintain security
  4. Validate everything - Always verify signatures and certificates
  5. 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.