Skip to content
This repository was archived by the owner on Apr 22, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 54 additions & 14 deletions doc/api/tls.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ Use `require('tls')` to access this module.
The `tls` module uses OpenSSL to provide Transport Layer Security and/or
Secure Socket Layer: encrypted stream communication.

TLS/SSL is a public/private key infrastructure. Each client and each
Traditional TLS/SSL is a public/private key infrastructure. Each client and each
server must have a private key. A private key is created like this

openssl genrsa -out ryans-key.pem 1024

All severs and some clients need to have a certificate. Certificates are public
All servers and some clients need to have a certificate. Certificates are public
keys signed by a Certificate Authority or self-signed. The first step to
getting a certificate is to create a "Certificate Signing Request" (CSR)
file. This is done with:
Expand All @@ -26,29 +26,55 @@ Alternatively you can send the CSR to a Certificate Authority for signing.
(TODO: docs on creating a CA, for now interested users should just look at
`test/fixtures/keys/Makefile` in the Node source code)

If node is compiled with OpenSSL v1.0.0 or later then TLS-PSK (RFC 4279) support is
available as an alternative to normal certificate-based authentication. PSK uses
a pre-shared key instead of certificates to authenticate a TLS connection, providing
mutual authentication. PSK and certificate auth are not mutually exclusive; one server
can accommodate both, with the variety used determined by the normal cipher negotiation
step. Note that PSK is only a good choice where means exist to securely share a key
with every connecting machine, so it does not replace PKI for the majority of TLS uses.

### s = tls.connect(port, [host], [options], callback)

Creates a new client connection to the given `port` and `host`. (If `host`
defaults to `localhost`.) `options` should be an object which specifies

- `key`: A string or `Buffer` containing the private key of the server in
PEM format. (Required)
- `key`: A string or `Buffer` containing the client's private key in PEM format.

- `cert`: A string or `Buffer` containing the certificate key of the server in
PEM format.
- `cert`: A string or `Buffer` containing the client's certificate in PEM format.

- `ca`: An array of strings or `Buffer`s of trusted certificates. If this is
omitted several well known "root" CAs will be used, like VeriSign.
These are used to authorize connections.

- `pskIdentity`: A string containing the TLS-PSK identity to use when connecting.

- `pskKey`: A `Buffer` containing the binary pre-shared key corresponding
to the `pskIdentity`.

- `pskCallback`: A callback that may be provided if you wish to select the identity
and key based on the "hint" provided by the server. The callback receives the hint
as its sole argument and must return an object with `pskIdentity` and `pskKey`
attributes as described above. This option is ignored if `pskIdentity` and `pskKey`
are provided outright.

- `ciphers`: An OpenSSL-style cipher priority list (eg `RC4-SHA:AES128-SHA:AES256-SHA`).
Optional, but note that the cipher negotiation between client and server determines
how the TLS handshake will proceed; for PSK both client and server must be configured
to use PSK ciphers. For ease of use, if PSK identity options are provided but `ciphers`
is not, it will be configured with a default set of PSK ciphers.

`tls.connect()` returns a cleartext `CryptoStream` object.

After the TLS/SSL handshake the `callback` is called. The `callback` will be
called no matter if the server's certificate was authorized or not. It is up
to the user to test `s.authorized` to see if the server certificate was
signed by one of the specified CAs. If `s.authorized === false` then the error
can be found in `s.authorizationError`.
After the TLS/SSL handshake the `callback` is called. If using normal certificate-based
TLS, The `callback` will be called even if the server's certificate was not authorized.
It is up to the user to test `s.authorized` to see if the server certificate was signed
by one of the specified CAs. If `s.authorized === false` then the error can be found in
`s.authorizationError`. If using PSK, the handshake will only complete if the pre-shared
keys match; `s.authorized` will be `true` and `s.pskIdentity` will be clientss identity
string. If the keys do not match then the handshake will fail. The callback will not be
called but the cleartext stream will emit an `error` event.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking purely as a consumer of this API, not having a unified behavoir between PSK and certificates for the error handling of a failed to authenticate connection is kinda a bummer.

Is there a better way to unify them so the same code that checked authorized === false would be re-used?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this a lot before doing it this way, and I'm still not sure of the best strategy. The problem with a unified behavior is that the existing interface makes perfect sense for certs and very little sense for PSK. As I understand it, the callback indicates that the handshake is complete and a secure (but not necessarily authenticated) channel exists. In TLS-PSK the handshake doesn't complete and there is no encrypted channel unless the keys match, so the callback should be called only on success. Obviously we can call the callback unconditionally if that's the desired behavior, but it won't imply that a channel exists.

Perhaps what would be best is a callback that is called unconditionally but receives an error as its first argument, consistent with most other Node interfaces, but I am quite open to better ideas on this point.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anyone else care to weigh in on this? I think it is the main issue with this pull request, and I'd like to clear it up in the hopes of getting this change moving along soon.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Started a thread on nodejsdev specifically about this api. Lets try to get a consensus there and then get this pull request landed.



### STARTTLS
Expand Down Expand Up @@ -98,10 +124,10 @@ This is a constructor for the `tls.Server` class. The options object
has these possibilities:

- `key`: A string or `Buffer` containing the private key of the server in
PEM format. (Required)
PEM format. (Required, unless using PSK)

- `cert`: A string or `Buffer` containing the certificate key of the server in
PEM format. (Required)
PEM format. (Required, unless using PSK)

- `ca`: An array of strings or `Buffer`s of trusted certificates. If this is
omitted several well known "root" CAs will be used, like VeriSign.
Expand All @@ -115,6 +141,20 @@ has these possibilities:
which is not authorized with the list of supplied CAs. This option only
has an effect if `requestCert` is `true`. Default: `false`.

- `pskCallback`: A function that receives a TLS-PSK identity string sent by
the connecting client and should synchronously return a `Buffer` containing
that user's key, or null if the user isn't recognized. (Required if using PSK)

- `pskHint`: Optional "hint" string sent to each connecting client to help the
client determine which identity to use. By default, no hint is sent. See RFC
4279 for details.

- `ciphers`: An OpenSSL-style cipher priority list (eg `RC4-SHA:AES128-SHA:AES256-SHA`).
Optional, but note that the cipher negotiation between client and server determines
how the TLS handshake will proceed; for PSK both client and server must be configured
to use PSK ciphers. For ease of use, if a PSK callback is provided but `ciphers`
is not, it will be configured with a default set of PSK ciphers.


#### Event: 'secureConnection'

Expand All @@ -129,7 +169,7 @@ client has verified by one of the supplied certificate authorities for the
server. If `cleartextStream.authorized` is false, then
`cleartextStream.authorizationError` is set to describe how authorization
failed. Implied but worth mentioning: depending on the settings of the TLS
server, you unauthorized connections may be accepted.
server, your unauthorized connections may be accepted.


#### server.listen(port, [host], [callback])
Expand Down
11 changes: 9 additions & 2 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ exports.createCredentials = function(options, context) {
}
}

if (options.pskServerCallback) {
c.context.setPskServerCallback(options.pskServerCallback);
}

if (options.pskServerHint) {
c.context.setPskHint(options.pskServerHint);
}

return c;
};

Expand Down Expand Up @@ -158,5 +166,4 @@ exports.createDiffieHellman = function(size_or_key, enc) {
} else {
return new DiffieHellman(size_or_key, enc);
}

}
};
126 changes: 95 additions & 31 deletions lib/tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) {
debug = function() { };
}

// ciphers to use if a server or client is configured for TLS-PSK but does not specify a cipher list
var DefaultPSKCiphers = 'PSK-AES256-CBC-SHA:PSK-3DES-EDE-CBC-SHA:PSK-AES128-CBC-SHA:PSK-RC4-SHA';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we should put these in the same order and basic composition as the main cipher list -- ie, PSK-RC4-SHA first, no 3des?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just used OpenSSL's default cipher ordering filtered for PSK suites. Is RC4 first for a reason?


var Connection = null;
try {
Expand Down Expand Up @@ -69,7 +71,7 @@ function convertNPNProtocols(NPNProtocols, out) {
if (Buffer.isBuffer(NPNProtocols)) {
out.NPNProtocols = NPNProtocols;
}
};
}

// Base class of both CleartextStream and EncryptedStream
function CryptoStream(pair) {
Expand Down Expand Up @@ -478,15 +480,17 @@ EncryptedStream.prototype._pusher = function(pool, offset, length) {
*/

function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
NPNProtocols) {
NPNProtocols, pskCallback) {
if (!(this instanceof SecurePair)) {
return new SecurePair(credentials,
isServer,
requestCert,
rejectUnauthorized,
NPNProtocols);
NPNProtocols,
pskCallback);
}


var self = this;

events.EventEmitter.call(this);
Expand Down Expand Up @@ -523,6 +527,10 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
this.npnProtocol = null;
}

if (pskCallback) {
this.ssl.setPskClientCallback(pskCallback);
}

/* Acts as a r/w stream to the cleartext side of the stream. */
this.cleartext = new CleartextStream(this);

Expand All @@ -541,11 +549,14 @@ util.inherits(SecurePair, events.EventEmitter);
exports.createSecurePair = function(credentials,
isServer,
requestCert,
rejectUnauthorized) {
rejectUnauthorized,
pskCallback) {
var pair = new SecurePair(credentials,
isServer,
requestCert,
rejectUnauthorized);
rejectUnauthorized,
null,
pskCallback);
return pair;
};

Expand Down Expand Up @@ -675,7 +686,7 @@ SecurePair.prototype.error = function() {
}
};

// TODO: support anonymous (nocert) and PSK
// TODO: support anonymous (nocert)


// AUTHENTICATION MODES
Expand Down Expand Up @@ -774,11 +785,11 @@ function Server(/* [options], listener */) {
ciphers: self.ciphers,
secureProtocol: self.secureProtocol,
secureOptions: self.secureOptions,
crl: self.crl
crl: self.crl,
pskServerCallback: self.pskCallback,
pskServerHint: self.pskHint
});

sharedCreds.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA');

// constructor call
net.Server.call(this, function(socket) {
var creds = crypto.createCredentials(null, sharedCreds.context);
Expand All @@ -795,25 +806,35 @@ function Server(/* [options], listener */) {
pair.on('secure', function() {
pair.cleartext.authorized = false;
pair.cleartext.npnProtocol = pair.npnProtocol;
if (!self.requestCert) {

// if using PSK, then handshake completion implies authorization
if (pair.ssl.pskIdentity) {
pair.cleartext.pskIdentity = pair.ssl.pskIdentity;
pair.cleartext.authorized = true;
cleartext._controlReleased = true;
self.emit('secureConnection', pair.cleartext, pair.encrypted);
} else {
var verifyError = pair.ssl.verifyError();
if (verifyError) {
pair.cleartext.authorizationError = verifyError;

if (self.rejectUnauthorized) {
socket.destroy();
pair.destroy();
}
else {
// otherwise, might need to check for cert verification errors
if (!self.requestCert) {
cleartext._controlReleased = true;
self.emit('secureConnection', pair.cleartext, pair.encrypted);
} else {
var verifyError = pair.ssl.verifyError();
if (verifyError) {
pair.cleartext.authorizationError = verifyError;
if (self.rejectUnauthorized) {
socket.destroy();
pair.destroy();
} else {
cleartext._controlReleased = true;
self.emit('secureConnection', pair.cleartext, pair.encrypted);
}
} else {
pair.cleartext.authorized = true;
cleartext._controlReleased = true;
self.emit('secureConnection', pair.cleartext, pair.encrypted);
}
} else {
pair.cleartext.authorized = true;
cleartext._controlReleased = true;
self.emit('secureConnection', pair.cleartext, pair.encrypted);
}
}
});
Expand Down Expand Up @@ -847,6 +868,12 @@ Server.prototype.setOptions = function(options) {
this.rejectUnauthorized = false;
}

if (options.pskCallback && !options.ciphers) {
// set default PSK ciphers if it looks like we're trying to use
// PSK but no preference has been specified
options.ciphers = DefaultPSKCiphers;
}

if (options.key) this.key = options.key;
if (options.cert) this.cert = options.cert;
if (options.ca) this.ca = options.ca;
Expand All @@ -856,6 +883,8 @@ Server.prototype.setOptions = function(options) {
if (options.secureProtocol) this.secureProtocol = options.secureProtocol;
if (options.secureOptions) this.secureOptions = options.secureOptions;
if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this);
if (options.pskCallback) this.pskCallback = options.pskCallback;
if (options.pskHint) this.pskHint = options.pskHint;
};


Expand Down Expand Up @@ -893,31 +922,66 @@ exports.connect = function(port /* host, options, cb */) {
}
}

// if a PSK identity/key pair was provided, translate it into the
// callback expected by SecurePair
if (options.pskIdentity && options.pskKey) {
options.pskClientCallback = function(hint) {
return {
identity: options.pskIdentity,
key: options.pskKey
}
};
}

if (options.pskClientCallback && !options.ciphers) {
// set default PSK ciphers if it looks like we're trying to use
// PSK but no preference has been specified
options.ciphers = DefaultPSKCiphers;
}

var socket = new net.Stream();

var sslcontext = crypto.createCredentials(options);
//sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA');

convertNPNProtocols(options.NPNProtocols, this);
var pair = new SecurePair(sslcontext, false, true, false,
this.NPNProtocols);
this.NPNProtocols, options.pskClientCallback);

var cleartext = pipe(pair, socket);

// This handler runs if the socket closes before the TLS handshake completes,
// either because the remote host aborted a normal TLS handshake for some reason
// or, in the case of TLS-PSK, if the identity and/or secret is invalid.
function onSocketClose() {
cleartext.emit('error', new Error('remote host closed the connection'));
}
socket.on('close', onSocketClose);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. Little concerned about the mapping of this here, converting a lower level close into an error. The pipe() from the socket will emit a close event on us eventually, do we need do emit an error event like this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This close handler is only in place until the TLS handshake completes, on the theory that anything preventing the TLS handshake from completing is an error from the standpoint of a TLS user (in the same way that a DNS lookup failure is an error to an HTTP user, etc). Once the handshake completes and a channel exists the handler is removed and the application code can handle or ignore close events as it sees fit. Does this make sense?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, though it implies we should have the same behavior during renegotiation right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are asking if a renegotiation failure on an established TLS channel should manifest as an error or a close? Good question. I'd say that yes, it should be an error, but I'll admit that I don't have much experience with this sort of decision in similar libraries. What happens in the case of renegotiation failure now?


socket.connect(port, host);

pair.on('secure', function() {
var verifyError = pair.ssl.verifyError();

// the handshake is complete, so remove the close listener;
// a close hereafter is just a close, not an error
socket.removeListener('close', onSocketClose);
cleartext.npnProtocol = pair.npnProtocol;

if (verifyError) {
cleartext.authorized = false;
cleartext.authorizationError = verifyError;
} else {
if (pair.ssl.pskIdentity) {
// if there is a PSK identity, then we're authorized, since PSK inherently
// provides mutual authentication
cleartext.pskIdentity = pair.ssl.pskIdentity;
cleartext.authorized = true;
} else {
// otherwise check the ssl verifyError and make it
// available for the client code to inspect
var verifyError = pair.ssl.verifyError();
if (verifyError) {
cleartext.authorized = false;
cleartext.authorizationError = verifyError;
} else {
cleartext.authorized = true;
}
}

// call the callback, indicating that the handshake is complete
if (cb) cb();
});

Expand Down
Loading