Browse Source

tls: async session storage

v0.9.2-release
Fedor Indutny 13 years ago
parent
commit
8e0c830cd0
  1. 19
      doc/api/tls.markdown
  2. 42
      lib/tls.js
  3. 250
      src/node_crypto.cc
  4. 71
      src/node_crypto.h
  5. 22
      test/simple/test-tls-session-cache.js

19
doc/api/tls.markdown

@ -373,6 +373,25 @@ When a client connection emits an 'error' event before secure connection is
established - it will be forwarded here.
### Event: 'newSession'
`function (sessionId, sessionData) { }`
Emitted on creation of TLS session. May be used to store sessions in external
storage.
### Event: 'resumeSession'
`function (sessionId, callback) { }`
Emitted when client wants to resume previous TLS session. Event listener may
perform lookup in external storage using given `sessionId`, and invoke
`callback(null, sessionData)` once finished. If session can't be resumed
(i.e. doesn't exist in storage) one may call `callback(null, null)`. Calling
`callback(err)` will terminate incoming connection and destroy socket.
### server.listen(port, [host], [callback])
Begin accepting connections on the specified `port` and `host`. If the

42
lib/tls.js

@ -725,6 +725,37 @@ function onhandshakedone() {
debug('onhandshakedone');
}
function onclienthello(hello) {
var self = this,
once = false;
this.encrypted.pause();
this.cleartext.pause();
function callback(err, session) {
if (once) return;
once = true;
if (err) return self.socket.destroy(err);
self.ssl.loadSession(session);
self.encrypted.resume();
self.cleartext.resume();
}
if (hello.sessionId.length <= 0 ||
!this.server ||
!this.server.emit('resumeSession', hello.sessionId, callback)) {
callback(null, null);
}
}
function onnewsession(key, session) {
if (!this.server) return;
this.server.emit('newSession', key, session);
}
/**
* Provides a pair of streams to do encrypted communication.
@ -746,6 +777,7 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
events.EventEmitter.call(this);
this.server = options.server;
this._secureEstablished = false;
this._isServer = isServer ? true : false;
this._encWriteState = true;
@ -768,13 +800,16 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
this._requestCert = requestCert ? true : false;
this.ssl = new Connection(this.credentials.context,
this._isServer ? true : false,
this._isServer ? this._requestCert : options.servername,
this._rejectUnauthorized);
this._isServer ? true : false,
this._isServer ? this._requestCert :
options.servername,
this._rejectUnauthorized);
if (this._isServer) {
this.ssl.onhandshakestart = onhandshakestart.bind(this);
this.ssl.onhandshakedone = onhandshakedone.bind(this);
this.ssl.onclienthello = onclienthello.bind(this);
this.ssl.onnewsession = onnewsession.bind(this);
this.ssl.handshakes = 0;
this.ssl.timer = null;
}
@ -1084,6 +1119,7 @@ function Server(/* [options], listener */) {
self.requestCert,
self.rejectUnauthorized,
{
server: self,
NPNProtocols: self.NPNProtocols,
SNICallback: self.SNICallback
});

250
src/node_crypto.cc

@ -84,6 +84,9 @@ static Persistent<String> version_symbol;
static Persistent<String> ext_key_usage_symbol;
static Persistent<String> onhandshakestart_sym;
static Persistent<String> onhandshakedone_sym;
static Persistent<String> onclienthello_sym;
static Persistent<String> onnewsession_sym;
static Persistent<String> sessionid_sym;
static Persistent<FunctionTemplate> secure_context_constructor;
@ -221,15 +224,71 @@ Handle<Value> SecureContext::Init(const Arguments& args) {
}
sc->ctx_ = SSL_CTX_new(method);
// Enable session caching?
SSL_CTX_set_session_cache_mode(sc->ctx_, SSL_SESS_CACHE_SERVER);
// SSL_CTX_set_session_cache_mode(sc->ctx_,SSL_SESS_CACHE_OFF);
// SSL session cache configuration
SSL_CTX_set_session_cache_mode(sc->ctx_,
SSL_SESS_CACHE_SERVER |
SSL_SESS_CACHE_NO_INTERNAL |
SSL_SESS_CACHE_NO_AUTO_CLEAR);
SSL_CTX_sess_set_get_cb(sc->ctx_, GetSessionCallback);
SSL_CTX_sess_set_new_cb(sc->ctx_, NewSessionCallback);
sc->ca_store_ = NULL;
return True();
}
SSL_SESSION* SecureContext::GetSessionCallback(SSL* s,
unsigned char* key,
int len,
int* copy) {
HandleScope scope;
Connection* p = static_cast<Connection*>(SSL_get_app_data(s));
*copy = 0;
SSL_SESSION* sess = p->next_sess_;
p->next_sess_ = NULL;
return sess;
}
void SessionDataFree(char* data, void* hint) {
delete[] data;
}
int SecureContext::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
HandleScope scope;
Connection* p = static_cast<Connection*>(SSL_get_app_data(s));
// Check if session is small enough to be stored
int size = i2d_SSL_SESSION(sess, NULL);
if (size > kMaxSessionSize) return 0;
// Serialize session
char* serialized = new char[size];
unsigned char* pserialized = reinterpret_cast<unsigned char*>(serialized);
memset(serialized, 0, size);
i2d_SSL_SESSION(sess, &pserialized);
Handle<Value> argv[2] = {
Buffer::New(reinterpret_cast<char*>(sess->session_id),
sess->session_id_length)->handle_,
Buffer::New(serialized, size, SessionDataFree, NULL)->handle_
};
if (onnewsession_sym.IsEmpty()) {
onnewsession_sym = NODE_PSYMBOL("onnewsession");
}
MakeCallback(p->handle_, onnewsession_sym, ARRAY_SIZE(argv), argv);
return 0;
}
// Takes a string or buffer and loads it into a BIO.
// Caller responsible for BIO_free-ing the returned object.
static BIO* LoadBIO (Handle<Value> v) {
@ -667,6 +726,150 @@ Handle<Value> SecureContext::LoadPKCS12(const Arguments& args) {
}
size_t ClientHelloParser::Write(const uint8_t* data, size_t len) {
HandleScope scope;
// Just accumulate data, everything will be pushed to BIO later
if (state_ == kPaused) return 0;
// Copy incoming data to the internal buffer
// (which has a size of the biggest possible TLS frame)
size_t available = sizeof(data_) - offset_;
size_t copied = len < available ? len : available;
memcpy(data_ + offset_, data, copied);
offset_ += copied;
// Vars for parsing hello
bool is_clienthello = false;
uint8_t session_size = -1;
uint8_t* session_id = NULL;
Local<Object> hello;
Handle<Value> argv[1];
switch (state_) {
case kWaiting:
// >= 5 bytes for header parsing
if (offset_ < 5) break;
if (data_[0] == kChangeCipherSpec || data_[0] == kAlert ||
data_[0] == kHandshake || data_[0] == kApplicationData) {
frame_len_ = (data_[3] << 8) + data_[4];
state_ = kTLSHeader;
body_offset_ = 5;
} else {
frame_len_ = (data_[0] << 8) + data_[1];
state_ = kSSLHeader;
if (*data_ & 0x40) {
// header with padding
body_offset_ = 3;
} else {
// without padding
body_offset_ = 2;
}
}
// Sanity check (too big frame, or too small)
if (frame_len_ >= sizeof(data_)) {
// Let OpenSSL handle it
Finish();
return copied;
}
case kTLSHeader:
case kSSLHeader:
// >= 5 + frame size bytes for frame parsing
if (offset_ < body_offset_ + frame_len_) break;
// Skip unsupported frames and gather some data from frame
// TODO: Check protocol version
if (data_[body_offset_] == kClientHello) {
is_clienthello = true;
uint8_t* body;
size_t session_offset;
if (state_ == kTLSHeader) {
// Skip frame header, hello header, protocol version and random data
session_offset = body_offset_ + 4 + 2 + 32;
if (session_offset + 1 < offset_) {
body = data_ + session_offset;
session_size = *body;
session_id = body + 1;
}
} else if (state_ == kSSLHeader) {
// Skip header, version
session_offset = body_offset_ + 3;
if (session_offset + 4 < offset_) {
body = data_ + session_offset;
int ciphers_size = (body[0] << 8) + body[1];
if (body + 4 + ciphers_size < data_ + offset_) {
session_size = (body[2] << 8) + body[3];
session_id = body + 4 + ciphers_size;
}
}
} else {
// Whoa? How did we get here?
abort();
}
// Check if we overflowed (do not reply with any private data)
if (session_id == NULL ||
session_size > 32 ||
session_id + session_size > data_ + offset_) {
Finish();
return copied;
}
// TODO: Parse other things?
}
// Not client hello - let OpenSSL handle it
if (!is_clienthello) {
Finish();
return copied;
}
// Parse frame, call javascript handler and
// move parser into the paused state
if (onclienthello_sym.IsEmpty()) {
onclienthello_sym = NODE_PSYMBOL("onclienthello");
}
if (sessionid_sym.IsEmpty()) {
sessionid_sym = NODE_PSYMBOL("sessionId");
}
state_ = kPaused;
hello = Object::New();
hello->Set(sessionid_sym,
Buffer::New(reinterpret_cast<char*>(session_id),
session_size)->handle_);
argv[0] = hello;
MakeCallback(conn_->handle_, onclienthello_sym, 1, argv);
break;
case kEnded:
default:
break;
}
return copied;
}
void ClientHelloParser::Finish() {
assert(state_ != kEnded);
state_ = kEnded;
// Write all accumulated data
int r = BIO_write(conn_->bio_read_, reinterpret_cast<char*>(data_), offset_);
conn_->HandleBIOError(conn_->bio_read_, "BIO_write", r);
conn_->SetShutdownFlags();
}
#ifdef SSL_PRINT_DEBUG
# define DEBUG_PRINT(...) fprintf (stderr, __VA_ARGS__)
#else
@ -789,6 +992,7 @@ void Connection::Initialize(Handle<Object> target) {
NODE_SET_PROTOTYPE_METHOD(t, "getPeerCertificate", Connection::GetPeerCertificate);
NODE_SET_PROTOTYPE_METHOD(t, "getSession", Connection::GetSession);
NODE_SET_PROTOTYPE_METHOD(t, "setSession", Connection::SetSession);
NODE_SET_PROTOTYPE_METHOD(t, "loadSession", Connection::LoadSession);
NODE_SET_PROTOTYPE_METHOD(t, "isSessionReused", Connection::IsSessionReused);
NODE_SET_PROTOTYPE_METHOD(t, "isInitFinished", Connection::IsInitFinished);
NODE_SET_PROTOTYPE_METHOD(t, "verifyError", Connection::VerifyError);
@ -1112,9 +1316,17 @@ Handle<Value> Connection::EncIn(const Arguments& args) {
String::New("off + len > buffer.length")));
}
int bytes_written = BIO_write(ss->bio_read_, buffer_data + off, len);
ss->HandleBIOError(ss->bio_read_, "BIO_write", bytes_written);
ss->SetShutdownFlags();
int bytes_written;
char* data = buffer_data + off;
if (ss->is_server_ && !ss->hello_parser_.ended()) {
bytes_written = ss->hello_parser_.Write(reinterpret_cast<uint8_t*>(data),
len);
} else {
bytes_written = BIO_write(ss->bio_read_, data, len);
ss->HandleBIOError(ss->bio_read_, "BIO_write", bytes_written);
ss->SetShutdownFlags();
}
return scope.Close(Integer::New(bytes_written));
}
@ -1444,7 +1656,7 @@ Handle<Value> Connection::SetSession(const Arguments& args) {
ssize_t wlen = DecodeWrite(sbuf, slen, args[0], BINARY);
assert(wlen == slen);
const unsigned char* p = (unsigned char*) sbuf;
const unsigned char* p = reinterpret_cast<const unsigned char*>(sbuf);
SSL_SESSION* sess = d2i_SSL_SESSION(NULL, &p, wlen);
delete [] sbuf;
@ -1463,6 +1675,30 @@ Handle<Value> Connection::SetSession(const Arguments& args) {
return True();
}
Handle<Value> Connection::LoadSession(const Arguments& args) {
HandleScope scope;
Connection *ss = Connection::Unwrap(args);
if (args.Length() >= 1 && Buffer::HasInstance(args[0])) {
ssize_t slen = Buffer::Length(args[0].As<Object>());
char* sbuf = Buffer::Data(args[0].As<Object>());
const unsigned char* p = reinterpret_cast<unsigned char*>(sbuf);
SSL_SESSION* sess = d2i_SSL_SESSION(NULL, &p, slen);
// Setup next session and move hello to the BIO buffer
if (ss->next_sess_ != NULL) {
SSL_SESSION_free(ss->next_sess_);
}
ss->next_sess_ = sess;
}
ss->hello_parser_.Finish();
return True();
}
Handle<Value> Connection::IsSessionReused(const Arguments& args) {
HandleScope scope;

71
src/node_crypto.h

@ -49,6 +49,9 @@ namespace crypto {
static X509_STORE* root_cert_store;
// Forward declaration
class Connection;
class SecureContext : ObjectWrap {
public:
static void Initialize(v8::Handle<v8::Object> target);
@ -58,6 +61,8 @@ class SecureContext : ObjectWrap {
X509_STORE *ca_store_;
protected:
static const int kMaxSessionSize = 10 * 1024;
static v8::Handle<v8::Value> New(const v8::Arguments& args);
static v8::Handle<v8::Value> Init(const v8::Arguments& args);
static v8::Handle<v8::Value> SetKey(const v8::Arguments& args);
@ -71,6 +76,12 @@ class SecureContext : ObjectWrap {
static v8::Handle<v8::Value> Close(const v8::Arguments& args);
static v8::Handle<v8::Value> LoadPKCS12(const v8::Arguments& args);
static SSL_SESSION* GetSessionCallback(SSL* s,
unsigned char* key,
int len,
int* copy);
static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
SecureContext() : ObjectWrap() {
ctx_ = NULL;
ca_store_ = NULL;
@ -100,6 +111,51 @@ class SecureContext : ObjectWrap {
private:
};
class ClientHelloParser {
public:
enum FrameType {
kChangeCipherSpec = 20,
kAlert = 21,
kHandshake = 22,
kApplicationData = 23,
kOther = 255
};
enum HandshakeType {
kClientHello = 1
};
enum ParseState {
kWaiting,
kTLSHeader,
kSSLHeader,
kPaused,
kEnded
};
ClientHelloParser(Connection* c) : conn_(c),
state_(kWaiting),
offset_(0),
body_offset_(0),
written_(0) {
}
size_t Write(const uint8_t* data, size_t len);
void Finish();
inline bool ended() { return state_ == kEnded; }
private:
Connection* conn_;
ParseState state_;
size_t frame_len_;
uint8_t data_[18432];
size_t offset_;
size_t body_offset_;
size_t written_;
};
class Connection : ObjectWrap {
public:
static void Initialize(v8::Handle<v8::Object> target);
@ -126,6 +182,7 @@ class Connection : ObjectWrap {
static v8::Handle<v8::Value> GetPeerCertificate(const v8::Arguments& args);
static v8::Handle<v8::Value> GetSession(const v8::Arguments& args);
static v8::Handle<v8::Value> SetSession(const v8::Arguments& args);
static v8::Handle<v8::Value> LoadSession(const v8::Arguments& args);
static v8::Handle<v8::Value> IsSessionReused(const v8::Arguments& args);
static v8::Handle<v8::Value> IsInitFinished(const v8::Arguments& args);
static v8::Handle<v8::Value> VerifyError(const v8::Arguments& args);
@ -168,9 +225,10 @@ class Connection : ObjectWrap {
return ss;
}
Connection() : ObjectWrap() {
Connection() : ObjectWrap(), hello_parser_(this) {
bio_read_ = bio_write_ = NULL;
ssl_ = NULL;
next_sess_ = NULL;
}
~Connection() {
@ -179,6 +237,11 @@ class Connection : ObjectWrap {
ssl_ = NULL;
}
if (next_sess_ != NULL) {
SSL_SESSION_free(next_sess_);
next_sess_ = NULL;
}
#ifdef OPENSSL_NPN_NEGOTIATED
if (!npnProtos_.IsEmpty()) npnProtos_.Dispose();
if (!selectedNPNProto_.IsEmpty()) selectedNPNProto_.Dispose();
@ -198,7 +261,13 @@ class Connection : ObjectWrap {
BIO *bio_write_;
SSL *ssl_;
ClientHelloParser hello_parser_;
bool is_server_; /* coverity[member_decl] */
SSL_SESSION* next_sess_;
friend class ClientHelloParser;
friend class SecureContext;
};
void InitCrypto(v8::Handle<v8::Object> target);

22
test/simple/test-tls-session-cache.js

@ -50,18 +50,36 @@ function doTest() {
requestCert: true
};
var requestCount = 0;
var session;
var server = tls.createServer(options, function(cleartext) {
++requestCount;
cleartext.end();
});
server.on('newSession', function(id, data) {
assert.ok(!session);
session = {
id: id,
data: data
};
});
server.on('resumeSession', function(id, callback) {
assert.ok(session);
assert.equal(session.id.toString('hex'), id.toString('hex'));
// Just to check that async really works there
setTimeout(function() {
callback(null, session.data);
}, 100);
});
server.listen(common.PORT, function() {
var client = spawn('openssl', [
's_client',
'-connect', 'localhost:' + common.PORT,
'-key', join(common.fixturesDir, 'agent.key'),
'-cert', join(common.fixturesDir, 'agent.crt'),
'-reconnect'
'-reconnect',
'-no_ticket'
], {
customFds: [0, 1, 2]
});
@ -72,6 +90,8 @@ function doTest() {
});
process.on('exit', function() {
assert.ok(session);
// initial request + reconnect requests (5 times)
assert.equal(requestCount, 6);
});

Loading…
Cancel
Save