21 November 2018

public key cryptography

by mo


Let’s play a game.

My name is Clifford Smith and your name is Reginald Noble.

  • 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.

Fin

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.

Peace, 💖 and Ruby!

security 💎