Merge pull request #476 from murgatroid99/verify_callback_backport

Backport "Add checkServerIdentity callback"
This commit is contained in:
Michael Lumish 2018-08-07 10:45:18 -07:00 committed by GitHub
commit e6f07b43fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 218 additions and 4 deletions

View File

@ -38,6 +38,8 @@ using Nan::ObjectWrap;
using Nan::Persistent;
using Nan::Utf8String;
using v8::Array;
using v8::Context;
using v8::Exception;
using v8::External;
using v8::Function;
@ -58,6 +60,44 @@ ChannelCredentials::~ChannelCredentials() {
grpc_channel_credentials_release(wrapped_credentials);
}
static int verify_peer_callback_wrapper(const char* servername, const char* cert, void* userdata) {
Nan::HandleScope scope;
Nan::TryCatch try_catch;
Nan::Callback *callback = (Nan::Callback*)userdata;
const unsigned argc = 2;
Local<Value> argv[argc];
if (servername == NULL) {
argv[0] = Nan::Null();
} else {
argv[0] = Nan::New<v8::String>(servername).ToLocalChecked();
}
if (cert == NULL) {
argv[1] = Nan::Null();
} else {
argv[1] = Nan::New<v8::String>(cert).ToLocalChecked();
}
Local<Value> result = callback->Call(argc, argv);
// Catch any exception and return with a distinct status code which indicates this
if (try_catch.HasCaught()) {
return 2;
}
// If the result is an error, return a failure
if (result->IsNativeError()) {
return 1;
}
return 0;
}
static void verify_peer_callback_destruct(void *userdata) {
Nan::Callback *callback = (Nan::Callback*)userdata;
delete callback;
}
void ChannelCredentials::Init(Local<Object> exports) {
HandleScope scope;
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
@ -148,9 +188,31 @@ NAN_METHOD(ChannelCredentials::CreateSsl) {
"createSsl's second and third arguments must be"
" provided or omitted together");
}
verify_peer_options verify_options = {NULL, NULL, NULL};
if (!info[3]->IsUndefined()) {
if (!info[3]->IsObject()) {
return Nan::ThrowTypeError("createSsl's fourth argument must be an object");
}
Local<Object> object = info[3]->ToObject();
Local<Value> checkServerIdentityValue = Nan::Get(object,
Nan::New("checkServerIdentity").ToLocalChecked()).ToLocalChecked();
if (!checkServerIdentityValue->IsUndefined()) {
if (!checkServerIdentityValue->IsFunction()) {
return Nan::ThrowTypeError("Value of checkServerIdentity must be a function.");
}
Nan::Callback *callback = new Callback(Local<Function>::Cast(
checkServerIdentityValue));
verify_options.verify_peer_callback = verify_peer_callback_wrapper;
verify_options.verify_peer_callback_userdata = (void*)callback;
verify_options.verify_peer_destruct = verify_peer_callback_destruct;
}
}
grpc_channel_credentials *creds = grpc_ssl_credentials_create(
root_certs.get(), private_key.isAssigned() ? &key_cert_pair : NULL,
NULL, NULL);
&verify_options, NULL);
if (creds == NULL) {
info.GetReturnValue().SetNull();
} else {

View File

@ -794,6 +794,36 @@ declare module "grpc" {
ERROR,
}
/**
* A certificate as received by the checkServerIdentity callback.
*/
export interface Certificate {
/**
* The raw certificate in DER form.
*/
raw: Buffer;
}
/**
* A callback that will receive the expected hostname and presented peer
* certificate as parameters. The callback should return an error to
* indicate that the presented certificate is considered invalid and
* otherwise returned undefined.
*/
export type CheckServerIdentityCallback = (hostname: string, cert: Certificate) => Error | undefined;
/**
* Additional peer verification options that can be set when creating
* SSL credentials.
*/
export interface VerifyOptions: {
/**
* If set, this callback will be invoked after the usual hostname verification
* has been performed on the peer certificate.
*/
checkServerIdentity?: CheckServerIdentityCallback;
}
/**
* Credentials module
*
@ -828,9 +858,10 @@ declare module "grpc" {
* @param rootCerts The root certificate data
* @param privateKey The client certificate private key, if applicable
* @param certChain The client certificate cert chain, if applicable
* @param verifyOptions Additional peer verification options, if desired
* @return The SSL Credentials object
*/
createSsl(rootCerts?: Buffer, privateKey?: Buffer, certChain?: Buffer): ChannelCredentials;
createSsl(rootCerts?: Buffer, privateKey?: Buffer, certChain?: Buffer, verifyOptions?: VerifyOptions): ChannelCredentials;
/**
* Create a gRPC credentials object from a metadata generation function. This

View File

@ -44,6 +44,7 @@
"istanbul": "^0.4.4",
"lodash": "^4.17.4",
"minimist": "^1.1.0",
"node-forge": "^0.7.5",
"poisson-process": "^0.2.1"
},
"engines": {

View File

@ -76,9 +76,35 @@ var _ = require('lodash');
* @see https://github.com/google/google-auth-library-nodejs
*/
const PEM_CERT_HEADER = "-----BEGIN CERTIFICATE-----";
const PEM_CERT_FOOTER = "-----END CERTIFICATE-----";
function wrapCheckServerIdentityCallback(callback) {
return function(hostname, cert) {
// Parse cert from pem to a version that matches the tls.checkServerIdentity
// format.
// https://nodejs.org/api/tls.html#tls_tls_checkserveridentity_hostname_cert
var pemHeaderIndex = cert.indexOf(PEM_CERT_HEADER);
if (pemHeaderIndex === -1) {
return new Error("Unable to parse certificate PEM.");
}
cert = cert.substring(pemHeaderIndex);
var pemFooterIndex = cert.indexOf(PEM_CERT_FOOTER);
if (pemFooterIndex === -1) {
return new Error("Unable to parse certificate PEM.");
}
cert = cert.substring(PEM_CERT_HEADER.length, pemFooterIndex);
var rawBuffer = new Buffer(cert.replace("\n", "").replace(" ", ""), "base64");
return callback(hostname, { raw: rawBuffer });
}
}
/**
* Create an SSL Credentials object. If using a client-side certificate, both
* the second and third arguments must be passed.
* the second and third arguments must be passed. Additional peer verification
* options can be passed in the fourth argument as described below.
* @memberof grpc.credentials
* @alias grpc.credentials.createSsl
* @kind function
@ -86,9 +112,30 @@ var _ = require('lodash');
* @param {Buffer=} private_key The client certificate private key, if
* applicable
* @param {Buffer=} cert_chain The client certificate cert chain, if applicable
* @param {Function} verify_options.checkServerIdentity Optional callback
* receiving the expected hostname and peer certificate for additional
* verification. The callback should return an Error if verification
* fails and otherwise return undefined.
* @return {grpc.credentials~ChannelCredentials} The SSL Credentials object
*/
exports.createSsl = ChannelCredentials.createSsl;
exports.createSsl = function(root_certs, private_key, cert_chain, verify_options) {
// The checkServerIdentity callback from gRPC core will receive the cert as a PEM.
// To better match the checkServerIdentity callback of Node, we wrap the callback
// to decode the PEM and populate a cert object.
if (verify_options && verify_options.checkServerIdentity) {
if (typeof verify_options.checkServerIdentity !== 'function') {
throw new TypeError("Value of checkServerIdentity must be a function.");
}
// Make a shallow clone of verify_options so our modification of the callback
// isn't reflected to the caller
var updated_verify_options = Object.assign({}, verify_options);
updated_verify_options.checkServerIdentity = wrapCheckServerIdentityCallback(
verify_options.checkServerIdentity);
arguments[3] = updated_verify_options;
}
return ChannelCredentials.createSsl.apply(this, arguments);
}
/**
* @callback grpc.credentials~metadataCallback

View File

@ -46,6 +46,7 @@
"istanbul": "^0.4.4",
"lodash": "^4.17.4",
"minimist": "^1.1.0",
"node-forge": "^0.7.5",
"poisson-process": "^0.2.1"
},
"engines": {

View File

@ -21,6 +21,7 @@
var assert = require('assert');
var fs = require('fs');
var path = require('path');
var forge = require('node-forge');
var grpc = require('..');
@ -128,6 +129,25 @@ describe('channel credentials', function() {
grpc.credentials.createSsl(null, null, pem_data);
});
});
it('works if the fourth argument is an empty object', function() {
var creds;
assert.doesNotThrow(function() {
creds = grpc.credentials.createSsl(ca_data, null, null, {});
});
assert.notEqual(creds, null);
});
it('fails if the fourth argument is a non-object value', function() {
assert.throws(function() {
grpc.credentials.createSsl(ca_data, null, null, 'test');
}, TypeError);
});
it('fails if the checkServerIdentity is a non-function', function() {
assert.throws(function() {
grpc.credentials.createSsl(ca_data, null, null, {
"checkServerIdentity": 'test'
});
}, TypeError);
});
});
});
@ -260,6 +280,58 @@ describe('client credentials', function() {
done();
});
});
it('Verify callback receives correct arguments', function(done) {
var callback_host, callback_cert;
var client_ssl_creds = grpc.credentials.createSsl(ca_data, null, null, {
"checkServerIdentity": function(host, cert) {
callback_host = host;
callback_cert = cert;
}
});
var client = new Client('localhost:' + port, client_ssl_creds,
client_options);
client.unary({}, function(err, data) {
assert.ifError(err);
assert.equal(callback_host, 'foo.test.google.fr');
// The roundabout forge APIs for converting PEM to a node DER Buffer
var expected_der = new Buffer(forge.asn1.toDer(
forge.pki.certificateToAsn1(forge.pki.certificateFromPem(pem_data)))
.getBytes(), 'binary');
// Assert the buffers are equal by converting them to hex strings
assert.equal(callback_cert.raw.toString('hex'), expected_der.toString('hex'));
// Documented behavior of callback cert is that raw should be its only property
assert.equal(Object.keys(callback_cert).length, 1);
done();
});
});
it('Verify callback exception causes connection failure', function(done) {
var client_ssl_creds = grpc.credentials.createSsl(ca_data, null, null, {
"checkServerIdentity": function(host, cert) {
throw "Verification callback exception";
}
});
var client = new Client('localhost:' + port, client_ssl_creds,
client_options);
client.unary({}, function(err, data) {
assert.ok(err, "Should have raised an error");
done();
});
});
it('Verify callback returning an Error causes connection failure', function(done) {
var client_ssl_creds = grpc.credentials.createSsl(ca_data, null, null, {
"checkServerIdentity": function(host, cert) {
return new Error("Verification error");
}
});
var client = new Client('localhost:' + port, client_ssl_creds,
client_options);
client.unary({}, function(err, data) {
assert.ok(err, "Should have raised an error");
done();
});
});
it('Should update metadata with SSL creds', function(done) {
var metadataUpdater = function(service_url, callback) {
var metadata = new grpc.Metadata();