Start of "Simple Peer-To-Peer Security" protocol.

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
This commit is contained in:
Guus Sliepen 2011-07-24 15:44:51 +02:00
parent ff751903aa
commit 3d75dbc088
9 changed files with 607 additions and 8 deletions

View 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

View 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;
}
}

View 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;
}

View 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 *);

View 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. */

View 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

358
src/sptps.c Normal file
View file

@ -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;
}

67
src/sptps.h Normal file
View file

@ -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);

165
src/sptps_test.c Normal file
View file

@ -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;
}