tls: session API returns

This commit is contained in:
Fedor Indutny 2013-06-17 12:11:13 +02:00
parent 0a4260c8c0
commit 212e9cd8c9
7 changed files with 480 additions and 11 deletions

View File

@ -403,6 +403,31 @@ established - it will be forwarded here.
`tlsSocket` is the [tls.TLSSocket][] that the error originated from.
### Event: 'newSession'
`function (sessionId, sessionData) { }`
Emitted on creation of TLS session. May be used to store sessions in external
storage.
NOTE: adding this event listener will have an effect only on connections
established after addition of event listener.
### 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.
NOTE: adding this event listener will have an effect only on connections
established after addition of event listener.
### server.listen(port, [host], [callback])
Begin accepting connections on the specified `port` and `host`. If the

View File

@ -47,6 +47,33 @@ function onhandshakedone() {
}
function onclienthello(hello) {
var self = this,
once = false;
function callback(err, session) {
if (once)
return self.destroy(new Error('TLS session callback was called twice'));
once = true;
if (err)
return self.destroy(err);
self.ssl.loadSession(session);
}
if (hello.sessionId.length <= 0 ||
!this.server.emit('resumeSession', hello.sessionId, callback)) {
callback(null, null);
}
}
function onnewsession(key, session) {
this.server.emit('newSession', key, session);
}
/**
* Provides a wrap of socket stream to do encrypted communication.
*/
@ -92,6 +119,7 @@ TLSSocket.prototype._init = function() {
// Wrap socket's handle
var credentials = options.credentials || crypto.createCredentials();
this.ssl = tls_wrap.wrap(this._handle, credentials.context, options.isServer);
this.server = options.server || null;
// For clients, we will always have either a given ca list or be using
// default one
@ -104,8 +132,15 @@ TLSSocket.prototype._init = function() {
if (options.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.lastHandshakeTime = 0;
this.ssl.handshakes = 0;
if (this.server.listeners('resumeSession').length > 0 ||
this.server.listeners('newSession').length > 0) {
this.ssl.enableSessionCallbacks();
}
} else {
this.ssl.onhandshakestart = function() {};
this.ssl.onhandshakedone = this._finishInit.bind(this);

View File

@ -60,9 +60,10 @@ class SecureContext : ObjectWrap {
// TODO: ca_store_ should probably be removed, it's not used anywhere.
X509_STORE *ca_store_;
protected:
static const int kMaxSessionSize = 10 * 1024;
protected:
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);

View File

@ -87,7 +87,9 @@ class NodeBIO {
}
protected:
static const size_t kBufferLength = 16 * 1024;
// NOTE: Size is maximum TLS frame length, this is required if we want
// to fit whole ClientHello into one Buffer of NodeBIO.
static const size_t kBufferLength = 16 * 1024 + 5;
class Buffer {
public:

View File

@ -36,6 +36,8 @@ static Persistent<String> onerror_sym;
static Persistent<String> onsniselect_sym;
static Persistent<String> onhandshakestart_sym;
static Persistent<String> onhandshakedone_sym;
static Persistent<String> onclienthello_sym;
static Persistent<String> onnewsession_sym;
static Persistent<String> subject_sym;
static Persistent<String> subjectaltname_sym;
static Persistent<String> modulus_sym;
@ -47,6 +49,7 @@ static Persistent<String> fingerprint_sym;
static Persistent<String> name_sym;
static Persistent<String> version_sym;
static Persistent<String> ext_key_usage_sym;
static Persistent<String> sessionid_sym;
static Persistent<Function> tlsWrap;
@ -69,7 +72,9 @@ TLSCallbacks::TLSCallbacks(Kind kind,
pending_write_item_(NULL),
started_(false),
established_(false),
shutdown_(false) {
shutdown_(false),
session_callbacks_(false),
next_sess_(NULL) {
// Persist SecureContext
sc_ = ObjectWrap::Unwrap<SecureContext>(sc);
@ -78,17 +83,68 @@ TLSCallbacks::TLSCallbacks(Kind kind,
handle_ = Persistent<Object>::New(node_isolate, tlsWrap->NewInstance());
handle_->SetAlignedPointerInInternalField(0, this);
// No session cache support
SSL_CTX_sess_set_get_cb(sc_->ctx_, NULL);
SSL_CTX_sess_set_new_cb(sc_->ctx_, NULL);
// Initialize queue for clearIn writes
QUEUE_INIT(&write_item_queue_);
// Initialize hello parser
hello_.state = kParseEnded;
hello_.frame_len = 0;
hello_.body_offset = 0;
// We've our own session callbacks
SSL_CTX_sess_set_get_cb(sc_->ctx_, GetSessionCallback);
SSL_CTX_sess_set_new_cb(sc_->ctx_, NewSessionCallback);
InitSSL();
}
SSL_SESSION* TLSCallbacks::GetSessionCallback(SSL* s,
unsigned char* key,
int len,
int* copy) {
HandleScope scope(node_isolate);
TLSCallbacks* c = static_cast<TLSCallbacks*>(SSL_get_app_data(s));
*copy = 0;
SSL_SESSION* sess = c->next_sess_;
c->next_sess_ = NULL;
return sess;
}
int TLSCallbacks::NewSessionCallback(SSL* s, SSL_SESSION* sess) {
HandleScope scope(node_isolate);
TLSCallbacks* c = static_cast<TLSCallbacks*>(SSL_get_app_data(s));
if (!c->session_callbacks_)
return 0;
// Check if session is small enough to be stored
int size = i2d_SSL_SESSION(sess, NULL);
if (size > SecureContext::kMaxSessionSize)
return 0;
// Serialize session
Local<Object> buff = Local<Object>::New(Buffer::New(size)->handle_);
unsigned char* serialized = reinterpret_cast<unsigned char*>(
Buffer::Data(buff));
memset(serialized, 0, size);
i2d_SSL_SESSION(sess, &serialized);
Local<Object> session = Local<Object>::New(
Buffer::New(reinterpret_cast<char*>(sess->session_id),
sess->session_id_length)->handle_);
Handle<Value> argv[2] = { session, buff };
MakeCallback(c->handle_, onnewsession_sym, ARRAY_SIZE(argv), argv);
return 0;
}
TLSCallbacks::~TLSCallbacks() {
SSL_free(ssl_);
ssl_ = NULL;
@ -306,6 +362,10 @@ void TLSCallbacks::SSLInfoCallback(const SSL* ssl_, int where, int ret) {
void TLSCallbacks::EncOut() {
// Ignore cycling data if ClientHello wasn't yet parsed
if (hello_.state != kParseEnded)
return;
// Write in progress
if (write_size_ != 0)
return;
@ -406,6 +466,10 @@ Handle<Value> TLSCallbacks::GetSSLError(int status, int* err) {
void TLSCallbacks::ClearOut() {
// Ignore cycling data if ClientHello wasn't yet parsed
if (hello_.state != kParseEnded)
return;
HandleScope scope(node_isolate);
assert(ssl_ != NULL);
@ -436,6 +500,10 @@ void TLSCallbacks::ClearOut() {
bool TLSCallbacks::ClearIn() {
// Ignore cycling data if ClientHello wasn't yet parsed
if (hello_.state != kParseEnded)
return false;
HandleScope scope(node_isolate);
int written = 0;
@ -569,10 +637,12 @@ void TLSCallbacks::DoRead(uv_stream_t* handle,
// Commit read data
NodeBIO::FromBIO(enc_in_)->Commit(nread);
// Cycle OpenSSL state
ClearIn();
ClearOut();
EncOut();
// Parse ClientHello first
if (hello_.state != kParseEnded)
return ParseClientHello();
// Cycle OpenSSL's state
Cycle();
}
@ -585,6 +655,138 @@ int TLSCallbacks::DoShutdown(ShutdownWrap* req_wrap, uv_shutdown_cb cb) {
}
void TLSCallbacks::ParseClientHello() {
enum FrameType {
kChangeCipherSpec = 20,
kAlert = 21,
kHandshake = 22,
kApplicationData = 23,
kOther = 255
};
enum HandshakeType {
kClientHello = 1
};
assert(session_callbacks_);
HandleScope scope(node_isolate);
NodeBIO* enc_in = NodeBIO::FromBIO(enc_in_);
size_t avail = 0;
uint8_t* data = reinterpret_cast<uint8_t*>(enc_in->Peek(&avail));
assert(avail == 0 || data != NULL);
// Vars for parsing hello
bool is_clienthello = false;
uint8_t session_size = -1;
uint8_t* session_id = NULL;
Local<Object> hello_obj;
Handle<Value> argv[1];
switch (hello_.state) {
case kParseWaiting:
// >= 5 bytes for header parsing
if (avail < 5)
break;
if (data[0] == kChangeCipherSpec ||
data[0] == kAlert ||
data[0] == kHandshake ||
data[0] == kApplicationData) {
hello_.frame_len = (data[3] << 8) + data[4];
hello_.state = kParseTLSHeader;
hello_.body_offset = 5;
} else {
hello_.frame_len = (data[0] << 8) + data[1];
hello_.state = kParseSSLHeader;
if (*data & 0x40) {
// header with padding
hello_.body_offset = 3;
} else {
// without padding
hello_.body_offset = 2;
}
}
// Sanity check (too big frame, or too small)
// Let OpenSSL handle it
if (hello_.frame_len >= kMaxTLSFrameLen)
return ParseFinish();
// Fall through
case kParseTLSHeader:
case kParseSSLHeader:
// >= 5 + frame size bytes for frame parsing
if (avail < hello_.body_offset + hello_.frame_len)
break;
// Skip unsupported frames and gather some data from frame
// TODO(indutny): Check protocol version
if (data[hello_.body_offset] == kClientHello) {
is_clienthello = true;
uint8_t* body;
size_t session_offset;
if (hello_.state == kParseTLSHeader) {
// Skip frame header, hello header, protocol version and random data
session_offset = hello_.body_offset + 4 + 2 + 32;
if (session_offset + 1 < avail) {
body = data + session_offset;
session_size = *body;
session_id = body + 1;
}
} else if (hello_.state == kParseSSLHeader) {
// Skip header, version
session_offset = hello_.body_offset + 3;
if (session_offset + 4 < avail) {
body = data + session_offset;
int ciphers_size = (body[0] << 8) + body[1];
if (body + 4 + ciphers_size < data + avail) {
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 + avail) {
return ParseFinish();
}
// TODO(indutny): Parse other things?
}
// Not client hello - let OpenSSL handle it
if (!is_clienthello)
return ParseFinish();
hello_.state = kParsePaused;
hello_obj = Object::New();
hello_obj->Set(sessionid_sym,
Buffer::New(reinterpret_cast<char*>(session_id),
session_size)->handle_);
argv[0] = hello_obj;
MakeCallback(handle_, onclienthello_sym, 1, argv);
break;
case kParseEnded:
default:
break;
}
}
#define CASE_X509_ERR(CODE) case X509_V_ERR_##CODE: reason = #CODE; break;
Handle<Value> TLSCallbacks::VerifyError(const Arguments& args) {
HandleScope scope(node_isolate);
@ -690,6 +892,20 @@ Handle<Value> TLSCallbacks::IsSessionReused(const Arguments& args) {
}
Handle<Value> TLSCallbacks::EnableSessionCallbacks(const Arguments& args) {
HandleScope scope(node_isolate);
UNWRAP(TLSCallbacks);
wrap->session_callbacks_ = true;
wrap->hello_.state = kParseWaiting;
wrap->hello_.frame_len = 0;
wrap->hello_.body_offset = 0;
return scope.Close(Null(node_isolate));
}
Handle<Value> TLSCallbacks::GetPeerCertificate(const Arguments& args) {
HandleScope scope(node_isolate);
@ -879,6 +1095,30 @@ Handle<Value> TLSCallbacks::SetSession(const Arguments& args) {
}
Handle<Value> TLSCallbacks::LoadSession(const Arguments& args) {
HandleScope scope(node_isolate);
UNWRAP(TLSCallbacks);
if (args.Length() >= 1 && Buffer::HasInstance(args[0])) {
ssize_t slen = Buffer::Length(args[0]);
char* sbuf = Buffer::Data(args[0]);
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 (wrap->next_sess_ != NULL)
SSL_SESSION_free(wrap->next_sess_);
wrap->next_sess_ = sess;
}
wrap->ParseFinish();
return True(node_isolate);
}
Handle<Value> TLSCallbacks::GetCurrentCipher(const Arguments& args) {
HandleScope scope(node_isolate);
@ -1112,10 +1352,14 @@ void TLSCallbacks::Initialize(Handle<Object> target) {
NODE_SET_PROTOTYPE_METHOD(t, "getPeerCertificate", GetPeerCertificate);
NODE_SET_PROTOTYPE_METHOD(t, "getSession", GetSession);
NODE_SET_PROTOTYPE_METHOD(t, "setSession", SetSession);
NODE_SET_PROTOTYPE_METHOD(t, "loadSession", LoadSession);
NODE_SET_PROTOTYPE_METHOD(t, "getCurrentCipher", GetCurrentCipher);
NODE_SET_PROTOTYPE_METHOD(t, "verifyError", VerifyError);
NODE_SET_PROTOTYPE_METHOD(t, "setVerifyMode", SetVerifyMode);
NODE_SET_PROTOTYPE_METHOD(t, "isSessionReused", IsSessionReused);
NODE_SET_PROTOTYPE_METHOD(t,
"enableSessionCallbacks",
EnableSessionCallbacks);
#ifdef OPENSSL_NPN_NEGOTIATED
NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", GetNegotiatedProto);
@ -1134,6 +1378,8 @@ void TLSCallbacks::Initialize(Handle<Object> target) {
onerror_sym = NODE_PSYMBOL("onerror");
onhandshakestart_sym = NODE_PSYMBOL("onhandshakestart");
onhandshakedone_sym = NODE_PSYMBOL("onhandshakedone");
onclienthello_sym = NODE_PSYMBOL("onclienthello");
onnewsession_sym = NODE_PSYMBOL("onnewsession");
subject_sym = NODE_PSYMBOL("subject");
issuer_sym = NODE_PSYMBOL("issuer");
@ -1146,6 +1392,7 @@ void TLSCallbacks::Initialize(Handle<Object> target) {
name_sym = NODE_PSYMBOL("name");
version_sym = NODE_PSYMBOL("version");
ext_key_usage_sym = NODE_PSYMBOL("ext_key_usage");
sessionid_sym = NODE_PSYMBOL("sessionId");
}
} // namespace node

View File

@ -61,7 +61,24 @@ class TLSCallbacks : public StreamWrapCallbacks {
protected:
static const int kClearOutChunkSize = 1024;
static const size_t kMaxTLSFrameLen = 16 * 1024 + 5;
// ClientHello parser types
enum ParseState {
kParseWaiting,
kParseTLSHeader,
kParseSSLHeader,
kParsePaused,
kParseEnded
};
struct HelloState {
ParseState state;
size_t frame_len;
size_t body_offset;
};
// Write callback queue's item
class WriteItem {
public:
WriteItem(WriteWrap* w, uv_write_cb cb) : w_(w), cb_(cb) {
@ -86,6 +103,18 @@ class TLSCallbacks : public StreamWrapCallbacks {
bool ClearIn();
void ClearOut();
void InvokeQueued(int status);
void ParseClientHello();
inline void ParseFinish() {
hello_.state = kParseEnded;
Cycle();
}
inline void Cycle() {
ClearIn();
ClearOut();
EncOut();
}
v8::Handle<v8::Value> GetSSLError(int status, int* err);
@ -99,6 +128,14 @@ class TLSCallbacks : public StreamWrapCallbacks {
static v8::Handle<v8::Value> VerifyError(const v8::Arguments& args);
static v8::Handle<v8::Value> SetVerifyMode(const v8::Arguments& args);
static v8::Handle<v8::Value> IsSessionReused(const v8::Arguments& args);
static v8::Handle<v8::Value> EnableSessionCallbacks(const v8::Arguments& args);
// TLS Session API
static SSL_SESSION* GetSessionCallback(SSL* s,
unsigned char* key,
int len,
int* copy);
static int NewSessionCallback(SSL* s, SSL_SESSION* sess);
#ifdef OPENSSL_NPN_NEGOTIATED
static v8::Handle<v8::Value> GetNegotiatedProto(const v8::Arguments& args);
@ -134,9 +171,13 @@ class TLSCallbacks : public StreamWrapCallbacks {
size_t write_queue_size_;
QUEUE write_item_queue_;
WriteItem* pending_write_item_;
HelloState hello_;
int hello_body_;
bool started_;
bool established_;
bool shutdown_;
bool session_callbacks_;
SSL_SESSION* next_sess_;
#ifdef OPENSSL_NPN_NEGOTIATED
v8::Persistent<v8::Object> npn_protos_;

View File

@ -0,0 +1,118 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
if (!process.versions.openssl) {
console.error('Skipping because node compiled without OpenSSL.');
process.exit(0);
}
require('child_process').exec('openssl version', function(err) {
if (err !== null) {
console.error('Skipping because openssl command is not available.');
process.exit(0);
}
doTest();
});
function doTest() {
var common = require('../common');
var assert = require('assert');
var tls = require('tls');
var fs = require('fs');
var join = require('path').join;
var spawn = require('child_process').spawn;
var keyFile = join(common.fixturesDir, 'agent.key');
var certFile = join(common.fixturesDir, 'agent.crt');
var key = fs.readFileSync(keyFile);
var cert = fs.readFileSync(certFile);
var options = {
key: key,
cert: cert,
ca: [cert],
requestCert: true
};
var requestCount = 0;
var session;
var badOpenSSL = false;
var server = tls.createServer(options, function(cleartext) {
cleartext.on('error', function(er) {
// We're ok with getting ECONNRESET in this test, but it's
// timing-dependent, and thus unreliable. Any other errors
// are just failures, though.
if (er.code !== 'ECONNRESET')
throw er;
});
++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',
'-no_ticket'
], {
stdio: [ 0, 1, 'pipe' ]
});
var err = '';
client.stderr.setEncoding('utf8');
client.stderr.on('data', function(chunk) {
err += chunk;
});
client.on('exit', function(code) {
if (/^unknown option/.test(err)) {
// using an incompatible version of openssl
assert(code);
badOpenSSL = true;
} else
assert.equal(code, 0);
server.close();
});
});
process.on('exit', function() {
if (!badOpenSSL) {
assert.ok(session);
// initial request + reconnect requests (5 times)
assert.equal(requestCount, 6);
}
});
}