Start of "Simple Peer-To-Peer Security" protocol.
authorGuus Sliepen <guus@tinc-vpn.org>
Sun, 24 Jul 2011 13:44:51 +0000 (15:44 +0200)
committerGuus Sliepen <guus@tinc-vpn.org>
Sun, 24 Jul 2011 13:44:51 +0000 (15:44 +0200)
Encryption and authentication of the meta connection is spread out over
meta.c and protocol_auth.c. The new protocol was added there as well,
leading to spaghetti code. To improve things, the new protocol will now
be implemented in sptps.[ch].

The goal is to have a very simplified version of TLS. There is a record
layer, and there are only two record types: application data and
handshake messages. The handshake message contains a random nonce, an
ephemeral ECDH public key, and an ECDSA signature over the former. After
the ECDH public keys are exchanged, a shared secret is calculated, and a
TLS style PRF is used to generate the key material for the cipher and
HMAC algorithm, and further communication is encrypted and authenticated.

A lot of the simplicity comes from the fact that both sides must have
each other's public keys in advance, and there are no options to choose.
There will be one fixed cipher suite, and both peers always authenticate
each other. (Inspiration taken from Ian Grigg's hypotheses[0].)
There might be some compromise in the future, to enable or disable
encryption, authentication and compression, but there will be no choice
of algorithms. This will allow SPTPS to be built with a few embedded
crypto algorithms instead of linking with huge crypto libraries.

The API is also kept simple. There is a start and a stop function. All
data necessary to make the connection work is passed in the start
function. Instead having both send- and receive-record functions, there
is a send-record function and a receive-data function. The latter will
pass protocol data received from the peer to the SPTPS implementation,
which will in turn call a receive-record callback function when
necessary. This hides all the handshaking from the application, and is
completely independent from any event loop or socket characteristics.

[0] http://iang.org/ssl/hn_hypotheses_in_secure_protocol_design.html

src/Makefile.am
src/openssl/cipher.c
src/openssl/digest.c
src/openssl/digest.h
src/openssl/prf.c
src/openssl/prf.h
src/sptps.c [new file with mode: 0644]
src/sptps.h [new file with mode: 0644]
src/sptps_test.c [new file with mode: 0644]

index 186c042c898169a68bb96bdba400003118bb1984..61f9a9360a6af768e351e0c4ef0116b148a59183 100644 (file)
@@ -1,6 +1,6 @@
 ## Produce this file with automake to get Makefile.in
 
-sbin_PROGRAMS = tincd tincctl
+sbin_PROGRAMS = tincd tincctl sptps_test
 
 EXTRA_DIST = linux bsd solaris cygwin mingw raw_socket uml_socket openssl gcrypt
 
@@ -20,6 +20,10 @@ tincctl_SOURCES = \
 nodist_tincctl_SOURCES = \
        ecdsagen.c rsagen.c
 
+sptps_test_SOURCES = \
+       logger.c cipher.c crypto.c ecdh.c ecdsa.c digest.c prf.c \
+       sptps.c sptps_test.c
+
 if TUNEMU
 tincd_SOURCES += bsd/tunemu.c
 endif
index 86a1acad889d981a8afcbdbed2eb58b0490b5260..743449a33a498f70c7bdf9c37f54c3623ca0d386 100644 (file)
@@ -101,13 +101,13 @@ bool cipher_encrypt(cipher_t *cipher, const void *indata, size_t inlen, void *ou
                if(EVP_EncryptInit_ex(&cipher->ctx, NULL, NULL, NULL, NULL)
                                && EVP_EncryptUpdate(&cipher->ctx, (unsigned char *)outdata, &len, indata, inlen)
                                && EVP_EncryptFinal(&cipher->ctx, (unsigned char *)outdata + len, &pad)) {
-                       *outlen = len + pad;
+                       if(outlen) *outlen = len + pad;
                        return true;
                }
        } else {
                int len;
                if(EVP_EncryptUpdate(&cipher->ctx, outdata, &len, indata, inlen)) {
-                       *outlen = len;
+                       if(outlen) *outlen = len;
                        return true;
                }
        }
@@ -122,13 +122,13 @@ bool cipher_decrypt(cipher_t *cipher, const void *indata, size_t inlen, void *ou
                if(EVP_DecryptInit_ex(&cipher->ctx, NULL, NULL, NULL, NULL)
                                && EVP_DecryptUpdate(&cipher->ctx, (unsigned char *)outdata, &len, indata, inlen)
                                && EVP_DecryptFinal(&cipher->ctx, (unsigned char *)outdata + len, &pad)) {
-                       *outlen = len + pad;
+                       if(outlen) *outlen = len + pad;
                        return true;
                }
        } else {
                int len;
                if(EVP_EncryptUpdate(&cipher->ctx, outdata, &len, indata, inlen)) {
-                       *outlen = len;
+                       if(outlen) *outlen = len;
                        return true;
                }
        }
index 09ed666f18cf73ef2b6db668a92d6fb88c9d4820..88bdeb3135491b68f5ca28bf3148057a525423d1 100644 (file)
@@ -115,6 +115,10 @@ int digest_get_nid(const digest_t *digest) {
        return digest->digest ? digest->digest->type : 0;
 }
 
+size_t digest_keylength(const digest_t *digest) {
+       return digest->digest->md_size;
+}
+
 size_t digest_length(const digest_t *digest) {
        return digest->maclength;
 }
index f3855c9243c58222e1c5e1ddb0a1593cccca72d1..24562b6b856770c96794b91a0bb15eea3fe382a5 100644 (file)
@@ -39,6 +39,7 @@ extern bool digest_create(struct digest *, const void *indata, size_t inlen, voi
 extern bool digest_verify(struct digest *, const void *indata, size_t inlen, const void *digestdata);
 extern bool digest_set_key(struct digest *, const void *key, size_t len);
 extern int digest_get_nid(const struct digest *);
+extern size_t digest_keylength(const struct digest *);
 extern size_t digest_length(const struct digest *);
 extern bool digest_active(const struct digest *);
 
index df7f445c4940adda32449a944009cb8febda2a85..648a157b4eba96d1aa2796fbf9de0212c181fadd 100644 (file)
@@ -26,7 +26,7 @@
    We use SHA512 and Whirlpool instead of MD5 and SHA1.
  */
 
-static bool prf_xor(int nid, char *secret, size_t secretlen, char *seed, size_t seedlen, char *out, ssize_t outlen) {
+static bool prf_xor(int nid, const char *secret, size_t secretlen, char *seed, size_t seedlen, char *out, ssize_t outlen) {
        digest_t digest;
        
        if(!digest_open_by_nid(&digest, nid, -1))
@@ -65,7 +65,7 @@ static bool prf_xor(int nid, char *secret, size_t secretlen, char *seed, size_t
        return true;
 }
 
-bool prf(char *secret, size_t secretlen, char *seed, size_t seedlen, char *out, size_t outlen) {
+bool prf(const char *secret, size_t secretlen, char *seed, size_t seedlen, char *out, size_t outlen) {
        /* Split secret in half, generate outlen bits with two different hash algorithms,
           and XOR the results. */
 
index 264d198062ee96857615e7ab6e34931d53b83eff..6525505e840851d48a6dd420622007a3069b4e5e 100644 (file)
@@ -20,6 +20,6 @@
 #ifndef __TINC_PRF_H__
 #define __TINC_PRF_H__
 
-extern bool prf(char *secret, size_t secretlen, char *seed, size_t seedlen, char *out, size_t outlen);
+extern bool prf(const char *secret, size_t secretlen, char *seed, size_t seedlen, char *out, size_t outlen);
 
 #endif
diff --git a/src/sptps.c b/src/sptps.c
new file mode 100644 (file)
index 0000000..ac710f5
--- /dev/null
@@ -0,0 +1,358 @@
+/*
+    sptps.c -- Simple Peer-to-Peer Security
+    Copyright (C) 2011 Guus Sliepen <guus@tinc-vpn.org>,
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "system.h"
+
+#include "cipher.h"
+#include "crypto.h"
+#include "digest.h"
+#include "ecdh.h"
+#include "ecdsa.h"
+#include "prf.h"
+#include "sptps.h"
+
+char *logfilename;
+#include "utils.c"
+
+static bool error(sptps_t *s, int s_errno, const char *msg) {
+       fprintf(stderr, "SPTPS error: %s\n", msg);
+       errno = s_errno;
+       return false;
+}
+
+static bool send_record_priv(sptps_t *s, uint8_t type, const char *data, uint16_t len) {
+       char plaintext[len + 23];
+       char ciphertext[len + 19];
+
+       // Create header with sequence number, length and record type
+       uint32_t seqno = htonl(s->outseqno++);
+       uint16_t netlen = htons(len);
+
+       memcpy(plaintext, &seqno, 4);
+       memcpy(plaintext + 4, &netlen, 2);
+       plaintext[6] = type;
+
+       // Add plaintext (TODO: avoid unnecessary copy)
+       memcpy(plaintext + 7, data, len);
+
+       if(s->state) {
+               // If first handshake has finished, encrypt and HMAC
+               if(!digest_create(&s->outdigest, plaintext, len + 7, plaintext + 7 + len))
+                       return false;
+
+               if(!cipher_encrypt(&s->outcipher, plaintext + 4, sizeof ciphertext, ciphertext, NULL, false))
+                       return false;
+
+               return s->send_data(s->handle, ciphertext, len + 19);
+       } else {
+               // Otherwise send as plaintext
+               return s->send_data(s->handle, plaintext + 4, len + 3);
+       }
+}
+
+bool send_record(sptps_t *s, uint8_t type, const char *data, uint16_t len) {
+       // Sanity checks: application cannot send data before handshake is finished,
+       // and only record types 0..127 are allowed.
+       if(!s->state)
+               return error(s, EINVAL, "Handshake phase not finished yet");
+
+       if(type & 128)
+               return error(s, EINVAL, "Invalid application record type");
+
+       return send_record_priv(s, type, data, len);
+}
+
+static bool send_kex(sptps_t *s) {
+       size_t keylen = ECDH_SIZE;
+       size_t siglen = ecdsa_size(&s->mykey);
+       char data[32 + keylen + siglen];
+
+       // Create a random nonce.
+       s->myrandom = realloc(s->myrandom, 32);
+       if(!s->myrandom)
+               return error(s, errno, strerror(errno));
+
+       randomize(s->myrandom, 32);
+       memcpy(data, s->myrandom, 32);
+
+       // Create a new ECDH public key.
+       if(!ecdh_generate_public(&s->ecdh, data + 32))
+               return false;
+
+       // Sign the former.
+       if(!ecdsa_sign(&s->mykey, data, 32 + keylen, data + 32 + keylen))
+               return false;
+
+       // Send the handshake record.
+       return send_record_priv(s, 128, data, sizeof data);
+}
+
+static bool generate_key_material(sptps_t *s, const char *shared, size_t len, const char *hisrandom) {
+       // Initialise cipher and digest structures if necessary
+       if(!s->state) {
+               bool result
+                       =  cipher_open_by_name(&s->incipher, "aes-256-ofb")
+                       && cipher_open_by_name(&s->outcipher, "aes-256-ofb")
+                       && digest_open_by_name(&s->indigest, "sha256", 16)
+                       && digest_open_by_name(&s->outdigest, "sha256", 16);
+               if(!result)
+                       return false;
+       }
+
+       // Allocate memory for key material
+       size_t keylen = digest_keylength(&s->indigest) + digest_keylength(&s->outdigest) + cipher_keylength(&s->incipher) + cipher_keylength(&s->outcipher);
+
+       s->key = realloc(s->key, keylen);
+       if(!s->key)
+               return error(s, errno, strerror(errno));
+
+       // Create the HMAC seed, which is "key expansion" + session label + server nonce + client nonce
+       char seed[s->labellen + 64 + 13];
+       strcpy(seed, "key expansion");
+       if(s->initiator) {
+               memcpy(seed + 13, hisrandom, 32);
+               memcpy(seed + 45, s->myrandom, 32);
+       } else {
+               memcpy(seed + 13, s->myrandom, 32);
+               memcpy(seed + 45, hisrandom, 32);
+       }
+       memcpy(seed + 78, s->label, s->labellen);
+
+       // Use PRF to generate the key material
+       if(!prf(shared, len, seed, s->labellen + 64 + 13, s->key, keylen))
+               return false;
+
+       return true;
+}
+
+static bool send_ack(sptps_t *s) {
+       return send_record_priv(s, 128, "", 0);
+}
+
+static bool receive_ack(sptps_t *s, const char *data, uint16_t len) {
+       if(len)
+               return false;
+
+       // TODO: set cipher/digest keys
+       return error(s, ENOSYS, "receive_ack() not completely implemented yet");
+}
+
+static bool receive_kex(sptps_t *s, const char *data, uint16_t len) {
+       size_t keylen = ECDH_SIZE;
+       size_t siglen = ecdsa_size(&s->hiskey);
+
+       // Verify length of KEX record.
+       if(len != 32 + keylen + siglen)
+               return error(s, EIO, "Invalid KEX record length");
+
+       // Verify signature.
+       if(!ecdsa_verify(&s->hiskey, data, 32 + keylen, data + 32 + keylen))
+               return false;
+
+       // Compute shared secret.
+       char shared[ECDH_SHARED_SIZE];
+       if(!ecdh_compute_shared(&s->ecdh, data + 32, shared))
+               return false;
+
+       // Generate key material from shared secret.
+       if(!generate_key_material(s, shared, sizeof shared, data))
+               return false;
+
+       // Send cipher change record if necessary
+       if(s->state)
+               if(!send_ack(s))
+                       return false;
+
+       // TODO: set cipher/digest keys
+       if(s->initiator) {
+               bool result
+                       =  cipher_set_key(&s->incipher, s->key, false)
+                       && digest_set_key(&s->indigest, s->key + cipher_keylength(&s->incipher), digest_keylength(&s->indigest))
+                       && cipher_set_key(&s->outcipher, s->key + cipher_keylength(&s->incipher) + digest_keylength(&s->indigest), true)
+                       && digest_set_key(&s->outdigest, s->key + cipher_keylength(&s->incipher) + digest_keylength(&s->indigest) + cipher_keylength(&s->outcipher), digest_keylength(&s->outdigest));
+               if(!result)
+                       return false;
+       } else {
+               bool result
+                       =  cipher_set_key(&s->outcipher, s->key, true)
+                       && digest_set_key(&s->outdigest, s->key + cipher_keylength(&s->outcipher), digest_keylength(&s->outdigest))
+                       && cipher_set_key(&s->incipher, s->key + cipher_keylength(&s->outcipher) + digest_keylength(&s->outdigest), false)
+                       && digest_set_key(&s->indigest, s->key + cipher_keylength(&s->outcipher) + digest_keylength(&s->outdigest) + cipher_keylength(&s->incipher), digest_keylength(&s->indigest));
+               if(!result)
+                       return false;
+       }
+
+       return true;
+}
+
+static bool receive_handshake(sptps_t *s, const char *data, uint16_t len) {
+       // Only a few states to deal with handshaking.
+       switch(s->state) {
+               case 0:
+                       // We have sent our public ECDH key, we expect our peer to sent one as well.
+                       if(!receive_kex(s, data, len))
+                               return false;
+                       s->state = 1;
+                       return true;
+               case 1:
+                       // We receive a secondary key exchange request, first respond by sending our own public ECDH key.
+                       if(!send_kex(s))
+                               return false;
+               case 2:
+                       // If we already sent our secondary public ECDH key, we expect the peer to send his.
+                       if(!receive_kex(s, data, len))
+                               return false;
+                       s->state = 3;
+                       return true;
+               case 3:
+                       // We expect an empty handshake message to indicate transition to the new keys.
+                       if(!receive_ack(s, data, len))
+                               return false;
+                       s->state = 1;
+                       return true;
+               default:
+                       return error(s, EIO, "Invalid session state");
+       }
+}
+
+bool receive_data(sptps_t *s, const char *data, size_t len) {
+       while(len) {
+               // First read the 2 length bytes.
+               if(s->buflen < 6) {
+                       size_t toread = 6 - s->buflen;
+                       if(toread > len)
+                               toread = len;
+
+                       if(s->state) {
+                               if(!cipher_decrypt(&s->incipher, data, toread, s->inbuf + s->buflen, NULL, false))
+                                       return false;
+                       } else {
+                               memcpy(s->inbuf + s->buflen, data, toread);
+                       }
+
+                       s->buflen += toread;
+                       len -= toread;
+                       data += toread;
+
+                       // Exit early if we don't have the full length.
+                       if(s->buflen < 6)
+                               return true;
+
+                       // If we have the length bytes, ensure our buffer can hold the whole request.
+                       uint16_t reclen;
+                       memcpy(&reclen, s->inbuf + 4, 2);
+                       reclen = htons(reclen);
+                       s->inbuf = realloc(s->inbuf, reclen + 23UL);
+                       if(!s->inbuf)
+                               return error(s, errno, strerror(errno));
+
+                       // Add sequence number.
+                       uint32_t seqno = htonl(s->inseqno++);
+                       memcpy(s->inbuf, &seqno, 4);
+
+                       // Exit early if we have no more data to process.
+                       if(!len)
+                               return true;
+               }
+
+               // Read up to the end of the record.
+               uint16_t reclen;
+               memcpy(&reclen, s->inbuf + 4, 2);
+               reclen = htons(reclen);
+               size_t toread = reclen + (s->state ? 23UL : 7UL) - s->buflen;
+               if(toread > len)
+                       toread = len;
+
+               if(s->state) {
+                       if(!cipher_decrypt(&s->incipher, data, toread, s->inbuf + s->buflen, NULL, false))
+                               return false;
+               } else {
+                       memcpy(s->inbuf + s->buflen, data, toread);
+               }
+
+               s->buflen += toread;
+               len -= toread;
+               data += toread;
+
+               // If we don't have a whole record, exit.
+               if(s->buflen < reclen + (s->state ? 23UL : 7UL))
+                       return true;
+
+               // Check HMAC.
+               if(s->state)
+                       if(!digest_verify(&s->indigest, s->inbuf, reclen + 7UL, s->inbuf + reclen + 7UL))
+                               error(s, EIO, "Invalid HMAC");
+
+               uint8_t type = s->inbuf[6];
+
+               // Handle record.
+               if(type < 128) {
+                       if(!s->receive_record(s->handle, type, s->inbuf + 7, reclen))
+                               return false;
+               } else if(type == 128) {
+                       if(!receive_handshake(s, s->inbuf + 7, reclen))
+                               return false;
+               } else {
+                       return error(s, EIO, "Invalid record type");
+               }
+
+               s->buflen = 4;
+       }
+
+       return true;
+}
+
+bool start_sptps(sptps_t *s, void *handle, bool initiator, ecdsa_t mykey, ecdsa_t hiskey, const char *label, size_t labellen, send_data_t send_data, receive_record_t receive_record) {
+       // Initialise struct sptps
+       memset(s, 0, sizeof *s);
+
+       s->handle = handle;
+       s->initiator = initiator;
+       s->mykey = mykey;
+       s->hiskey = hiskey;
+
+       s->label = malloc(labellen);
+       if(!s->label)
+               return error(s, errno, strerror(errno));
+
+       s->inbuf = malloc(7);
+       if(!s->inbuf)
+               return error(s, errno, strerror(errno));
+       s->buflen = 4;
+       memset(s->inbuf, 0, 4);
+
+       memcpy(s->label, label, labellen);
+       s->labellen = labellen;
+
+       s->send_data = send_data;
+       s->receive_record = receive_record;
+
+       // Do first KEX immediately
+       return send_kex(s);
+}
+
+bool stop_sptps(sptps_t *s) {
+       // Clean up any resources.
+       ecdh_free(&s->ecdh);
+       free(s->inbuf);
+       free(s->myrandom);
+       free(s->key);
+       free(s->label);
+       return true;
+}
diff --git a/src/sptps.h b/src/sptps.h
new file mode 100644 (file)
index 0000000..b1026d5
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+    sptps.h -- Simple Peer-to-Peer Security
+    Copyright (C) 2011 Guus Sliepen <guus@tinc-vpn.org>,
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "system.h"
+
+#include "cipher.h"
+#include "digest.h"
+#include "ecdh.h"
+#include "ecdsa.h"
+
+#define STATE_FIRST_KEX 0 // Waiting for peer's ECDHE pubkey
+#define STATE_NORMAL 1
+#define STATE_WAIT_KEX 2 // Waiting for peer's ECDHE pubkey
+#define STATE_WAIT_ACK 3 // Waiting for peer's acknowledgement of pubkey reception
+
+typedef bool (*send_data_t)(void *handle, const char *data, size_t len);
+typedef bool (*receive_record_t)(void *handle, uint8_t type, const char *data, uint16_t len);
+
+typedef struct sptps {
+       bool initiator;
+       int state;
+
+       char *inbuf;
+       size_t buflen;
+
+       cipher_t incipher;
+       digest_t indigest;
+       uint32_t inseqno;
+
+       cipher_t outcipher;
+       digest_t outdigest;
+       uint32_t outseqno;
+
+       ecdsa_t mykey;
+       ecdsa_t hiskey;
+       ecdh_t ecdh;
+
+       char *myrandom;
+       char *key;
+       char *label;
+       size_t labellen;
+
+       void *handle;
+       send_data_t send_data;
+       receive_record_t receive_record;
+} sptps_t;
+
+extern bool start_sptps(sptps_t *s, void *handle, bool initiator, ecdsa_t mykey, ecdsa_t hiskey, const char *label, size_t labellen, send_data_t send_data, receive_record_t receive_record);
+extern bool stop_sptps(sptps_t *s);
+extern bool send_record(sptps_t *s, uint8_t type, const char *data, uint16_t len);
+extern bool receive_data(sptps_t *s, const char *data, size_t len);
diff --git a/src/sptps_test.c b/src/sptps_test.c
new file mode 100644 (file)
index 0000000..7b33eef
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+    sptps_test.c -- Simple Peer-to-Peer Security test program
+    Copyright (C) 2011 Guus Sliepen <guus@tinc-vpn.org>,
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "system.h"
+#include "poll.h"
+
+#include "crypto.h"
+#include "ecdsa.h"
+#include "sptps.h"
+#include "utils.h"
+
+ecdsa_t mykey, hiskey;
+
+static bool send_data(void *handle, const char *data, size_t len) {
+       char hex[len * 2 + 1];
+       bin2hex(data, hex, len);
+       fprintf(stderr, "Sending %zu bytes of data:\n%s\n", len, hex);
+       const int *sock = handle;
+       if(send(*sock, data, len, 0) != len)
+               return false;
+       return true;
+}
+
+static bool receive_record(void *handle, uint8_t type, const char *data, uint16_t len) {
+       fprintf(stderr, "Received type %d record of %hu bytes:\n", type, len);
+       fwrite(data, len, 1, stdout);
+       return true;
+}
+
+int main(int argc, char *argv[]) {
+       bool initiator = false;
+
+       if(argc < 3) {
+               fprintf(stderr, "Usage: %s my_ecdsa_key_file his_ecdsa_key_file [host] port\n", argv[0]);
+               return 1;
+       }
+
+       if(argc > 4)
+               initiator = true;
+
+       struct addrinfo *ai, hint;
+       memset(&hint, 0, sizeof hint);
+
+       hint.ai_family = AF_UNSPEC;
+       hint.ai_socktype = SOCK_STREAM;
+       hint.ai_protocol = IPPROTO_TCP;
+       hint.ai_flags = initiator ? 0 : AI_PASSIVE;
+       
+       if(getaddrinfo(initiator ? argv[3] : NULL, initiator ? argv[4] : argv[3], &hint, &ai) || !ai) {
+               fprintf(stderr, "getaddrinfo() failed: %s\n", strerror(errno));
+               return 1;
+       }
+
+       int sock = socket(ai->ai_family, SOCK_STREAM, IPPROTO_TCP);
+       if(sock < 0) {
+               fprintf(stderr, "Could not create socket: %s\n", strerror(errno));
+               return 1;
+       }
+
+       int one = 1;
+       setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *)&one, sizeof one);
+
+       if(initiator) {
+               if(connect(sock, ai->ai_addr, ai->ai_addrlen)) {
+                       fprintf(stderr, "Could not connect to peer: %s\n", strerror(errno));
+                       return 1;
+               }
+               fprintf(stderr, "Connected\n");
+       } else {
+               if(bind(sock, ai->ai_addr, ai->ai_addrlen)) {
+                       fprintf(stderr, "Could not bind socket: %s\n", strerror(errno));
+                       return 1;
+               }
+               if(listen(sock, 1)) {
+                       fprintf(stderr, "Could not listen on socket: %s\n", strerror(errno));
+                       return 1;
+               }
+               fprintf(stderr, "Listening...\n");
+
+               sock = accept(sock, NULL, NULL);
+               if(sock < 0) {
+                       fprintf(stderr, "Could not accept connection: %s\n", strerror(errno));
+                       return 1;
+               }
+
+               fprintf(stderr, "Connected\n");
+       }
+
+       crypto_init();
+
+       FILE *fp = fopen(argv[1], "r");
+       if(!ecdsa_read_pem_private_key(&mykey, fp))
+               return 1;
+       fclose(fp);
+
+       fp = fopen(argv[2], "r");
+       if(!ecdsa_read_pem_public_key(&hiskey, fp))
+               return 1;
+       fclose(fp);
+
+       fprintf(stderr, "Keys loaded\n");
+
+       sptps_t s;
+       if(!start_sptps(&s, &sock, initiator, mykey, hiskey, "sptps_test", 10, send_data, receive_record))
+               return 1;
+
+       while(true) {
+               char buf[4095];
+
+               struct pollfd fds[2];
+               fds[0].fd = 0;
+               fds[0].events = POLLIN;
+               fds[1].fd = sock;
+               fds[1].events = POLLIN;
+               if(poll(fds, 2, -1) < 0)
+                       return 1;
+
+               if(fds[0].revents) {
+                       ssize_t len = read(0, buf, sizeof buf);
+                       if(len < 0) {
+                               fprintf(stderr, "Could not read from stdin: %s\n", strerror(errno));
+                               return 1;
+                       }
+                       if(len == 0)
+                               break;
+                       if(!send_record(&s, 0, buf, len))
+                               return 1;
+               }
+
+               if(fds[1].revents) {
+                       ssize_t len = recv(sock, buf, sizeof buf, 0);
+                       if(len < 0) {
+                               fprintf(stderr, "Could not read from socket: %s\n", strerror(errno));
+                               return 1;
+                       }
+                       if(len == 0) {
+                               fprintf(stderr, "Connection terminated by peer.\n");
+                               break;
+                       }
+                       char hex[len * 2 + 1];
+                       bin2hex(buf, hex, len);
+                       fprintf(stderr, "Received %zd bytes of data:\n%s\n", len, hex);
+                       if(!receive_data(&s, buf, len))
+                               return 1;
+               }
+       }
+
+       return 0;
+}