Skip to content

Instantly share code, notes, and snippets.

@grantr
Last active July 19, 2018 15:16
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save grantr/4757832 to your computer and use it in GitHub Desktop.
Save grantr/4757832 to your computer and use it in GitHub Desktop.
CurveCP handshake protocol in Ruby
# A demonstration of the CurveCP handshake protocol. This protocol has many
# favorable security properties described at http://curvecp.org.
#
# In addition to its security advantages, it has the following favorable properties:
# * Needs only 2 messages (1 from client, 1 from server) before application
# messages can be exchanged (3 before the server can send application messages)
# * Does not require the server to keep protocol state between handshake messages.
#
# An overview of the protocol:
#
# Definitions:
# S : Server long term public key
# S': Server short term public key
# s': Server short term private key
# C : Client long term public key
# C': Client short term public key
# V : Vouch:
# 16 byte nonce + Box to S from C containing C'
# K : Cookie:
# 16 byte nonce + SecretBox under minute-key containing C' and s'
# minute-key: A 32-byte random string rotated every minute (the current and
# previous key are both valid)
#
# Prerequisites:
# Client knows S and the domain name of the server
#
# The protocol flow:
#
# (Note some elements of messages are omitted here for clarity, see the CurveCP
# site for details)
#
# 1. Client sends HelloMessage
# - C'
# - 8 byte nonce
# - 64 null bytes encrypted with a Box to S from C'
#
# 2. Server sends CookieMessage
# - 16 byte nonce
# - Box to C' from S containing S' and K
#
# 3. Client sends InitiateMessage
# - C'
# - K
# - 8-byte nonce
# - Box to S' from C' containing:
# - C
# - V
# - server's domain name
# - a message (optional)
#
# The handshake has concluded at this point, and both server and client are
# free to send messages.
#
# 4. Server sends Message
# - 8 byte nonce
# - Box to C' from S' containing a message
#
# 5. Client sends Message
# - C'
# - 8 byte nonce
# - Box to S' from C' containing a message
#
#
# ALERT This implementation has not been inspected or verified by cryptography
# experts. Additionally, the CurveCP protocol itself is a work in progress. While
# the handshake protocol uses only proven primitives from the NaCL library, it
# is possible that weaknesses will be discovered. See the CurveCP website above
# for more information.
#
# Finally, ALERT comments throughout the code denote parts of the implementation
# that are noncompliant or not ready for production use. Read these carefully
# before basing an implementation on this code.
require 'celluloid'
require 'rbnacl'
module CurveCPHandshake
class Client
include Celluloid
attr_accessor :long_term_key
def initialize(long_term_key=Crypto::PrivateKey.generate)
@long_term_key = long_term_key
end
# returns a Connection
def connect(server, options={})
# ALERT Normally the server long term key would be pre-distributed
server_long_term_pubkey = options[:server_key] || server.long_term_public_key
# ALERT Normally the domain name would be pre-distributed
domain_name = options[:domain_name] || server.domain_name
# The initial message is optional
initial_message = options[:initial_message]
# Generate a client short term key
short_term_key = Crypto::PrivateKey.generate
# Generate a hello message
hello_message = HelloMessage.new(server_long_term_pubkey, short_term_key)
# add our mailbox so the server can reply
# ALERT Normally this would be handled by the transport layer
hello_message.reply_mailbox = Actor.current.mailbox
# send the hello message to the server
server.mailbox << hello_message
# Wait for a cookie from the server
cookie_message = receive { |msg| msg.is_a?(CookieMessage) }
# Extract the server's short term pubkey and cookie
server_short_term_pubkey, cookie = cookie_message.open(server_long_term_pubkey, short_term_key)
# Generate a vouch so the server knows we are authentic
vouch = Vouch.generate(server_long_term_pubkey, long_term_key, short_term_key.public_key)
# Generate an initiate message and send it to the server
# This contains the initial message
initiate_message = InitiateMessage.new(server_short_term_pubkey, short_term_key, cookie, long_term_key.public_key, vouch, domain_name, initial_message)
server.mailbox << initiate_message
# Now the connection can be used to send further messages
Connection.new(server_short_term_pubkey, short_term_key, :client)
end
end
class Server
include Celluloid
attr_accessor :long_term_key
attr_accessor :minute_key, :prev_minute_key
attr_accessor :domain_name
def initialize(long_term_key=Crypto::PrivateKey.generate)
@long_term_key = long_term_key
@client_connections = {}
# generate minute keys and rotate them
rotate_minute_key
every(60) { rotate_minute_key }
end
def domain_name
@domain_name ||= "example.com"
end
def long_term_public_key
long_term_key.public_key
end
def rotate_minute_key
self.prev_minute_key = minute_key || Crypto::Random.random_bytes(32)
self.minute_key = Crypto::Random.random_bytes(32)
end
def accept
accept_hello
# ALERT For testing
if block_given?
yield
end
accept_initiate
end
# returns a connection and initial message
def accept_hello
# Wait for a hello from a client
hello_message = receive { |msg| msg.is_a?(HelloMessage) }
# The client short term public key is sent in the clear
client_short_term_pubkey = hello_message.client_short_term_pubkey
# Ensure the hello message is valid, that is, the sender has access to
# the client short term private key and the server long term public key
raise "invalid hello message" unless hello_message.valid?(long_term_key)
# Generate a server short term key
short_term_key = Crypto::PrivateKey.generate
# Generate a cookie for the client to authenticate
# The cookie is also a state storage mechanism. It allows the handshake
# protocol to be stateless so that different threads can handle hello and
# initiate messages.
cookie = Cookie.generate(client_short_term_pubkey, short_term_key, minute_key)
# Generate a cookie message and send it to the client
cookie_message = CookieMessage.new(client_short_term_pubkey, long_term_key, short_term_key.public_key, cookie.to_bytes)
hello_message.reply_mailbox << cookie_message
end
def accept_initiate
# Wait for an initiate from a client
initiate_message = receive { |msg| msg.is_a?(InitiateMessage) }
# The client short term public key is sent in the clear
client_short_term_pubkey = initiate_message.client_short_term_pubkey
# The cookie is also sent in the clear
# This was sent to the server by the client and is returned unchanged
cookie = Cookie.new(initiate_message.cookie)
# Open the cookie to retrieve the boxed client short term public key and
# the server short term private key
# If the current minute key doesn't work, try the previous one
boxed_client_short_term_pubkey, short_term_key = begin
cookie.open(minute_key)
rescue Crypto::CryptoError
cookie.open(prev_minute_key)
end
# Ensure the boxed public key matches the one sent in the clear
# This is safe because Crypto::PublicKey implements constant-time
# equality
raise "boxed client key does not match" unless client_short_term_pubkey == boxed_client_short_term_pubkey
# Extract the client's long term public key, vouch, domain name, and initial message
client_long_term_pubkey, vouch, sent_domain_name, initial_message = initiate_message.open(short_term_key)
# Ensure the sent domain name matches our domain name
# ALERT This is potentially vulnerable to timing attacks. Constant-time
# comparison would probably be more secure.
raise "domain names do not match (#{sent_domain_name}, #{domain_name})" unless sent_domain_name == domain_name
# Open the vouch to retrieve the boxed client short term public key
vouched_client_short_term_pubkey = vouch.open(client_long_term_pubkey, long_term_key)
# Ensure the vouched public key matches the one sent in the clear
raise "vouched client key does not match" unless client_short_term_pubkey == vouched_client_short_term_pubkey
# ALERT Any application-specific logic for authorizing the client long
# term key would go here.
# The initiate message is valid, return a new Connection and the initial message
[Connection.new(client_short_term_pubkey, short_term_key, :server), initial_message]
end
end
class Connection
attr_accessor :public_key, :private_key, :type
def initialize(public_key, private_key, type)
raise "invalid type" unless [:server, :client].include?(type)
@box = Crypto::Box.new(public_key, private_key)
@type = type
end
def box(bytes)
Message.new(@box, @type, bytes)
end
def open(message)
message.open(@box)
end
end
module NonceGenerator
# Nonces can never be used more than once for a particular key!
#
# ALERT In real life, you would use a generator for each key so that
# information about the number of clients is not leaked.
# Rules for short term nonces:
# Must be 8 bytes
# Nonces must strictly increase for a particular short term key
# Not required to start at 0
# Not required to increase by 1
def short_term_nonce
@counter = (@counter ? @counter + 1 : 0)
# CurveCP specifies little-endian
[@counter].pack("Q<")
end
# Rules for long term nonces:
# Must be 16 bytes
# Not required to start at 0
# Not required to strictly increase
# Must not be used more than once, even if the process restarts. The docs
# mention two possible strategies for dealing with this: persistent counters
# and timestamps.
# ALERT In real life, long term nonce generators must be persisted. Even if
# the timestamp strategy is used, the timestamp must be persisted to ensure
# the clock never runs backwards.
# counter strategy
def long_term_nonce_counter
short_term_nonce + Crypto::Random.random_bytes(8)
end
# timestamp strategy
def long_term_nonce_timestamp
# microseconds since epoch
timestamp = (Time.now.to_f*1_000_000).to_i
# CurveCP specifies little-endian
timestamp.pack("Q<") + Crypto::Random_bytes(8)
end
end
class HelloMessage
extend NonceGenerator
attr_accessor :client_short_term_pubkey
attr_accessor :nonce, :ciphertext
attr_accessor :reply_mailbox
def initialize(server_long_term_pubkey, client_short_term_privkey)
@client_short_term_pubkey = client_short_term_privkey.public_key
@nonce = self.class.short_term_nonce
@ciphertext = Crypto::Box.new(server_long_term_pubkey, client_short_term_privkey).box(nonce_string, Crypto::Util.zeros(64))
end
def nonce_string
"CurveCP-client-H" + @nonce
end
# ALERT In real life, Hello messages should be constructed so their length
# is greater than or equal to the length of Cookie messages. This is to
# avoid an amplification attack whereby a client can use small bandwidth to
# overwhelm a server with larger bandwidth.
def valid?(server_long_term_privkey)
string = Crypto::Box.new(@client_short_term_pubkey, server_long_term_privkey).open(nonce_string, @ciphertext)
zeros = Crypto::Util.zeros(32)
Crypto::Util.verify32(string[0, 32], zeros) && Crypto::Util.verify32(string[32, 32], zeros)
end
end
class CookieMessage
extend NonceGenerator
attr_accessor :nonce, :ciphertext
def initialize(client_short_term_pubkey, server_long_term_privkey, server_short_term_pubkey, cookie)
@nonce = self.class.long_term_nonce_counter
@ciphertext = Crypto::Box.new(client_short_term_pubkey, server_long_term_privkey).box(nonce_string, server_short_term_pubkey.to_bytes + cookie)
end
def nonce_string
"CurveCPK" + @nonce
end
def open(server_long_term_pubkey, client_short_term_privkey)
plaintext = Crypto::Box.new(server_long_term_pubkey, client_short_term_privkey).open(nonce_string, @ciphertext)
server_short_term_pubkey, cookie = plaintext.unpack("a32a96")
[Crypto::PublicKey.new(server_short_term_pubkey), cookie]
end
end
class Cookie
extend NonceGenerator
NONCE_PREFIX = "minute-k"
def self.generate(client_short_term_pubkey, server_short_term_privkey, minute_key)
nonce = long_term_nonce_counter
nonce_string = NONCE_PREFIX + nonce
ciphertext = Crypto::SecretBox.new(minute_key).box(nonce_string, client_short_term_pubkey.to_bytes + server_short_term_privkey.to_bytes)
new(nonce + ciphertext)
end
def initialize(bytes)
@cookie = bytes
end
def open(minute_key)
nonce, ciphertext = @cookie.unpack("a16a80")
nonce_string = NONCE_PREFIX + nonce
plaintext = Crypto::SecretBox.new(minute_key).open(nonce_string, ciphertext)
client_short_term_pubkey, server_short_term_privkey = plaintext.unpack("a32a32")
[Crypto::PublicKey.new(client_short_term_pubkey), Crypto::PrivateKey.new(server_short_term_privkey)]
end
def to_bytes
@cookie
end
end
class InitiateMessage
extend NonceGenerator
attr_accessor :client_short_term_pubkey
attr_accessor :cookie
attr_accessor :nonce, :ciphertext
def initialize(server_short_term_pubkey, client_short_term_privkey, cookie, client_long_term_pubkey, vouch, domain_name, message)
@client_short_term_pubkey = client_short_term_privkey.public_key
@cookie = cookie
@nonce = self.class.short_term_nonce
@ciphertext = Crypto::Box.new(server_short_term_pubkey, client_short_term_privkey).box(nonce_string, [client_long_term_pubkey.to_bytes, vouch.to_bytes, domain_name, message].pack("a32a64a256a*"))
end
def nonce_string
"CurveCP-client-I" + @nonce
end
def open(server_short_term_privkey)
plaintext = Crypto::Box.new(@client_short_term_pubkey, server_short_term_privkey).open(nonce_string, ciphertext)
# Use A256 to unpack the domain name so null padding is not retained
client_long_term_pubkey, vouch, domain_name, message = plaintext.unpack("a32a64A256a*")
[Crypto::PublicKey.new(client_long_term_pubkey), Vouch.new(vouch), domain_name, message]
end
end
class Vouch
extend NonceGenerator
NONCE_PREFIX = "CurveCPV"
def self.generate(server_long_term_pubkey, client_long_term_privkey, client_short_term_pubkey)
nonce = long_term_nonce_counter
nonce_string = NONCE_PREFIX + nonce
ciphertext = Crypto::Box.new(server_long_term_pubkey, client_long_term_privkey).box(nonce_string, client_short_term_pubkey.to_bytes)
new(nonce + ciphertext)
end
def initialize(bytes)
@vouch = bytes
end
def open(client_long_term_pubkey, server_long_term_privkey)
nonce, ciphertext = @vouch.unpack("a16a48")
nonce_string = NONCE_PREFIX + nonce
client_short_term_pubkey = Crypto::Box.new(client_long_term_pubkey, server_long_term_privkey).open(nonce_string, ciphertext)
Crypto::PublicKey.new(client_short_term_pubkey)
end
def to_bytes
@vouch
end
end
class Message
extend NonceGenerator
attr_accessor :nonce, :box
def initialize(box, type, bytes)
raise "invalid type" unless [:server, :client].include?(type)
@nonce = self.class.short_term_nonce
@type = type
@ciphertext = box.box(nonce_string, bytes)
end
def nonce_string
"CurveCP-#{@type}-M" + @nonce
end
def open(box)
box.open(nonce_string, @ciphertext)
end
end
end
if $0 == __FILE__
require 'minitest/spec'
require 'minitest/autorun'
include CurveCPHandshake
Celluloid.logger = nil
def connect(options={})
client = Client.new
server = Server.new
connected = client.future.connect(server, options)
accepted = server.future.accept
client_connection = connected.value(0.1)
server_connection, initial_message = accepted.value(0.1)
[client_connection, server_connection, initial_message]
end
describe CurveCPHandshake do
it 'should transmit an initial message' do
_, _, initial_message = connect(initial_message: "hello!")
initial_message.must_equal "hello!"
end
it 'should exchange further messages' do
client_conn, server_conn = connect
m1 = client_conn.box("message 1")
m2 = server_conn.box("message 2")
server_conn.open(m1).must_equal "message 1"
client_conn.open(m2).must_equal "message 2"
end
it 'should raise if the server long term key is incorrect' do
lambda {
client = Client.new
server = Server.new
connected = client.future.connect(server, server_key: Crypto::PrivateKey.generate)
server.accept
}.must_raise(Crypto::CryptoError)
end
it 'should raise if the server domain is incorrect' do
client = Client.new
server = Server.new
server.domain_name = "foobar.com"
connected = client.future.connect(server, domain_name: "foobaz.com")
lambda {
server.accept
}.must_raise(RuntimeError)
end
it 'should not raise if the minute key has rotated once' do
client = Client.new
server = Server.new
connected = client.future.connect(server)
server.accept do
server.rotate_minute_key
end
end
it 'should raise if the minute key has rotated twice' do
client = Client.new
server = Server.new
connected = client.future.connect(server)
lambda {
server.accept do
server.rotate_minute_key
server.rotate_minute_key
end
}.must_raise(Crypto::CryptoError)
end
end
end
@tarcieri
Copy link

tarcieri commented Mar 3, 2013

You might also mention forward secrecy (even in the presence of a MitM) in your overview of the protocol's desirable properties. It's fairly unique to transport encryption protocols like this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment