Skip to content

Instantly share code, notes, and snippets.

@grantr
Last active July 19, 2018 15:16
Show Gist options
  • Select an option

  • Save grantr/4757832 to your computer and use it in GitHub Desktop.

Select an option

Save grantr/4757832 to your computer and use it in GitHub Desktop.

Revisions

  1. grantr revised this gist Feb 12, 2013. 1 changed file with 93 additions and 6 deletions.
    99 changes: 93 additions & 6 deletions curvecp_handshake.rb
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,61 @@
    # 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
    @@ -104,6 +159,12 @@ def rotate_minute_key

    def accept
    accept_hello

    # ALERT For testing
    if block_given?
    yield
    end

    accept_initiate
    end

    @@ -260,7 +321,8 @@ def nonce_string

    # 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.
    # 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)
    @@ -439,14 +501,39 @@ def connect(options={})
    end

    it 'should raise if the server domain is incorrect' do
    lambda {
    client = Client.new
    server = Server.new
    server.domain_name = "foobar.com"
    client = Client.new
    server = Server.new
    server.domain_name = "foobar.com"

    connected = client.future.connect(server, domain_name: "foobaz.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
  2. grantr revised this gist Feb 12, 2013. 1 changed file with 55 additions and 12 deletions.
    67 changes: 55 additions & 12 deletions curvecp_handshake.rb
    Original file line number Diff line number Diff line change
    @@ -30,9 +30,15 @@ def initialize(long_term_key=Crypto::PrivateKey.generate)
    end

    # returns a Connection
    def connect(server, initial_message=nil)
    # ALERT Normally this would be pre-distributed
    server_long_term_pubkey = server.long_term_public_key
    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
    @@ -57,9 +63,8 @@ def connect(server, initial_message=nil)
    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
    # ALERT Normally the domain name would be pre-distributed
    # This contains the initial message
    initiate_message = InitiateMessage.new(server_short_term_pubkey, short_term_key, cookie, long_term_key.public_key, vouch, server.domain_name, 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
    @@ -144,7 +149,7 @@ def accept_initiate
    # 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 CryptoError
    rescue Crypto::CryptoError
    cookie.open(prev_minute_key)
    end

    @@ -392,18 +397,56 @@ def open(box)
    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
    client = Client.new
    server = Server.new
    _, _, initial_message = connect(initial_message: "hello!")
    initial_message.must_equal "hello!"
    end

    accepted = server.future.accept
    client_connection = client.connect(server, "hello!")
    it 'should exchange further messages' do
    client_conn, server_conn = connect

    server_connection, initial_message = accepted.value
    m1 = client_conn.box("message 1")
    m2 = server_conn.box("message 2")

    initial_message.must_equal "hello!"
    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
    lambda {
    client = Client.new
    server = Server.new
    server.domain_name = "foobar.com"

    connected = client.future.connect(server, domain_name: "foobaz.com")
    server.accept
    }.must_raise(RuntimeError)
    end
    end
    end
  3. grantr revised this gist Feb 11, 2013. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions curvecp_handshake.rb
    Original file line number Diff line number Diff line change
    @@ -31,7 +31,7 @@ def initialize(long_term_key=Crypto::PrivateKey.generate)

    # returns a Connection
    def connect(server, initial_message=nil)
    # Normally this would be pre-distributed
    # ALERT Normally this would be pre-distributed
    server_long_term_pubkey = server.long_term_public_key

    # Generate a client short term key
    @@ -41,7 +41,7 @@ def connect(server, initial_message=nil)
    hello_message = HelloMessage.new(server_long_term_pubkey, short_term_key)

    # add our mailbox so the server can reply
    # Normally this would be handled by the transport layer
    # ALERT Normally this would be handled by the transport layer
    hello_message.reply_mailbox = Actor.current.mailbox

    # send the hello message to the server
    @@ -57,7 +57,7 @@ def connect(server, initial_message=nil)
    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
    # Normally the domain name would be pre-distributed
    # ALERT Normally the domain name would be pre-distributed
    # This contains the initial message
    initiate_message = InitiateMessage.new(server_short_term_pubkey, short_term_key, cookie, long_term_key.public_key, vouch, server.domain_name, initial_message)
    server.mailbox << initiate_message
  4. grantr revised this gist Feb 11, 2013. 1 changed file with 88 additions and 13 deletions.
    101 changes: 88 additions & 13 deletions curvecp_handshake.rb
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,21 @@
    # 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.
    #
    # 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'

    @@ -79,24 +97,38 @@ def rotate_minute_key
    self.minute_key = Crypto::Random.random_bytes(32)
    end

    # returns a connection and initial message
    def accept
    accept_hello
    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) }

    @@ -105,11 +137,40 @@ def accept

    # The cookie is also sent in the clear
    # This was sent to the server by the client and is returned unchanged
    received_cookie = Cookie.new(initiate_message.cookie)
    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 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, domain_name, initial_message = initiate_message.open(short_term_key)
    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
    @@ -134,6 +195,9 @@ def open(message)

    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
    @@ -154,6 +218,10 @@ def short_term_nonce
    # 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)
    @@ -185,6 +253,9 @@ 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.
    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)
    @@ -261,7 +332,8 @@ def nonce_string

    def open(server_short_term_privkey)
    plaintext = Crypto::Box.new(@client_short_term_pubkey, server_short_term_privkey).open(nonce_string, ciphertext)
    client_long_term_pubkey, vouch, domain_name, message = plaintext.unpack("a32a64a256a*")
    # 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
    @@ -316,19 +388,22 @@ def open(box)
    end

    if $0 == __FILE__
    #require 'minitest/spec'
    #require 'minitest/autorun'
    require 'minitest/spec'
    require 'minitest/autorun'

    include CurveCPHandshake

    client = Client.new
    server = Server.new
    describe CurveCPHandshake do
    it 'should transmit an initial message' do
    client = Client.new
    server = Server.new

    accepted = server.future.accept
    client_connection = client.connect(server, "hello!")

    server_connection, initial_message = accepted.value
    accepted = server.future.accept
    client_connection = client.connect(server, "hello!")

    puts "initial message received: #{initial_message}"
    server_connection, initial_message = accepted.value

    initial_message.must_equal "hello!"
    end
    end
    end
  5. grantr revised this gist Feb 11, 2013. 1 changed file with 61 additions and 29 deletions.
    90 changes: 61 additions & 29 deletions curvecp_handshake.rb
    Original file line number Diff line number Diff line change
    @@ -119,27 +119,41 @@ class Connection

    def initialize(public_key, private_key, type)
    raise "invalid type" unless [:server, :client].include?(type)
    @public_key = public_key
    @private_key = private_key
    @box = Crypto::Box.new(public_key, private_key)
    @type = type
    end

    def box(bytes)
    Message.new(@public_key, @private_key, @type, bytes)
    Message.new(@box, @type, bytes)
    end

    def open(message)
    message.open(@public_key, @private_key)
    message.open(@box)
    end
    end

    module NonceGenerator
    # Nonces can never be used more than once for a particular key!

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

    # counter strategy
    def long_term_nonce_counter
    short_term_nonce + Crypto::Random.random_bytes(8)
    @@ -157,22 +171,22 @@ def long_term_nonce_timestamp
    class HelloMessage
    extend NonceGenerator
    attr_accessor :client_short_term_pubkey
    attr_accessor :nonce, :box
    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
    @box = Crypto::Box.new(server_long_term_pubkey, client_short_term_privkey).box(nonce_string, Crypto::Util.zeros(64))
    @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

    def valid?(server_long_term_privkey)
    string = Crypto::Box.new(@client_short_term_pubkey, server_long_term_privkey).open(nonce_string, @box)
    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
    @@ -181,20 +195,20 @@ def valid?(server_long_term_privkey)
    class CookieMessage
    extend NonceGenerator

    attr_accessor :nonce, :box
    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
    @box = Crypto::Box.new(client_short_term_pubkey, server_long_term_privkey).box(nonce_string, server_short_term_pubkey.to_bytes + cookie)
    @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, @box)
    server_short_term_pubkey, cookie = plaintext.unpack("A32A96")
    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
    @@ -206,19 +220,19 @@ class Cookie
    def self.generate(client_short_term_pubkey, server_short_term_privkey, minute_key)
    nonce = long_term_nonce_counter
    nonce_string = NONCE_PREFIX + nonce
    box = Crypto::SecretBox.new(minute_key).box(nonce_string, client_short_term_pubkey.to_bytes + server_short_term_privkey.to_bytes)
    new(nonce + box)
    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, box = @cookie.unpack("A16A80")
    nonce, ciphertext = @cookie.unpack("a16a80")
    nonce_string = NONCE_PREFIX + nonce
    plaintext = Crypto::SecretBox.new(minute_key).open(nonce_string, box)
    client_short_term_pubkey, server_short_term_privkey = plaintext.unpack("A32A32")
    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

    @@ -232,22 +246,22 @@ class InitiateMessage

    attr_accessor :client_short_term_pubkey
    attr_accessor :cookie
    attr_accessor :nonce, :box
    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
    @box = 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*"))
    @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, box)
    client_long_term_pubkey, vouch, domain_name, message = plaintext.unpack("A32A64A256A*")
    plaintext = Crypto::Box.new(@client_short_term_pubkey, server_short_term_privkey).open(nonce_string, ciphertext)
    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
    @@ -259,18 +273,18 @@ class Vouch
    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
    box = Crypto::Box.new(server_long_term_pubkey, client_long_term_privkey).box(nonce_string, client_short_term_pubkey.to_bytes)
    new(nonce + box)
    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, box = @vouch.unpack("A16A48")
    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, box)
    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

    @@ -284,19 +298,37 @@ class Message

    attr_accessor :nonce, :box

    def initialize(public_key, private_key, type, bytes)
    def initialize(box, type, bytes)
    raise "invalid type" unless [:server, :client].include?(type)
    @nonce = self.class.short_term_nonce
    @type = type
    @box = Crypto::Box.new(public_key, private_key).box(nonce_string, bytes)
    @ciphertext = box.box(nonce_string, bytes)
    end

    def nonce_string
    "CurveCP-#{@type}-M" + @nonce
    end

    def open(public_key, private_key)
    Crypto::Box.new(public_key, private_key).open(nonce_string, @box)
    def open(box)
    box.open(nonce_string, @ciphertext)
    end
    end
    end
    end

    if $0 == __FILE__
    #require 'minitest/spec'
    #require 'minitest/autorun'

    include CurveCPHandshake

    client = Client.new
    server = Server.new

    accepted = server.future.accept
    client_connection = client.connect(server, "hello!")

    server_connection, initial_message = accepted.value

    puts "initial message received: #{initial_message}"

    end
  6. grantr created this gist Feb 11, 2013.
    302 changes: 302 additions & 0 deletions curvecp_handshake.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,302 @@
    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, initial_message=nil)
    # Normally this would be pre-distributed
    server_long_term_pubkey = server.long_term_public_key

    # 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
    # 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
    # Normally the domain name would be pre-distributed
    # This contains the initial message
    initiate_message = InitiateMessage.new(server_short_term_pubkey, short_term_key, cookie, long_term_key.public_key, vouch, server.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

    # returns a connection and initial message
    def accept
    # 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

    # Generate a server short term key
    short_term_key = Crypto::PrivateKey.generate

    # Generate a cookie for the client to authenticate
    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

    # 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
    received_cookie = Cookie.new(initiate_message.cookie)

    # Extract the client's long term public key, vouch, domain name, and initial message
    client_long_term_pubkey, vouch, domain_name, initial_message = initiate_message.open(short_term_key)

    [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)
    @public_key = public_key
    @private_key = private_key
    @type = type
    end

    def box(bytes)
    Message.new(@public_key, @private_key, @type, bytes)
    end

    def open(message)
    message.open(@public_key, @private_key)
    end
    end

    module NonceGenerator
    def short_term_nonce
    @counter = (@counter ? @counter + 1 : 0)
    # CurveCP specifies little-endian
    [@counter].pack("Q<")
    end

    # 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, :box

    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
    @box = 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

    def valid?(server_long_term_privkey)
    string = Crypto::Box.new(@client_short_term_pubkey, server_long_term_privkey).open(nonce_string, @box)
    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, :box

    def initialize(client_short_term_pubkey, server_long_term_privkey, server_short_term_pubkey, cookie)
    @nonce = self.class.long_term_nonce_counter
    @box = 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, @box)
    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
    box = Crypto::SecretBox.new(minute_key).box(nonce_string, client_short_term_pubkey.to_bytes + server_short_term_privkey.to_bytes)
    new(nonce + box)
    end

    def initialize(bytes)
    @cookie = bytes
    end

    def open(minute_key)
    nonce, box = @cookie.unpack("A16A80")
    nonce_string = NONCE_PREFIX + nonce
    plaintext = Crypto::SecretBox.new(minute_key).open(nonce_string, box)
    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, :box

    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
    @box = 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, box)
    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
    box = Crypto::Box.new(server_long_term_pubkey, client_long_term_privkey).box(nonce_string, client_short_term_pubkey.to_bytes)
    new(nonce + box)
    end

    def initialize(bytes)
    @vouch = bytes
    end

    def open(client_long_term_pubkey, server_long_term_privkey)
    nonce, box = @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, box)
    Crypto::PublicKey.new(client_short_term_pubkey)
    end

    def to_bytes
    @vouch
    end
    end

    class Message
    extend NonceGenerator

    attr_accessor :nonce, :box

    def initialize(public_key, private_key, type, bytes)
    raise "invalid type" unless [:server, :client].include?(type)
    @nonce = self.class.short_term_nonce
    @type = type
    @box = Crypto::Box.new(public_key, private_key).box(nonce_string, bytes)
    end

    def nonce_string
    "CurveCP-#{@type}-M" + @nonce
    end

    def open(public_key, private_key)
    Crypto::Box.new(public_key, private_key).open(nonce_string, @box)
    end
    end
    end