tls: async session storage
This commit is contained in:
parent
790d651f0d
commit
8e0c830cd0
@ -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
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
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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…
x
Reference in New Issue
Block a user