tls: getPeerCertificate(detailed)

Add `raw` property to certificate, add mode to output full certificate
chain.
This commit is contained in:
Fedor Indutny 2014-04-17 15:57:36 +04:00
parent b3ef289ffb
commit 345c40b661
7 changed files with 302 additions and 166 deletions

View File

@ -644,27 +644,32 @@ specified CAs, otherwise `false`
The reason why the peer's certificate has not been verified. This property The reason why the peer's certificate has not been verified. This property
becomes available only when `tlsSocket.authorized === false`. becomes available only when `tlsSocket.authorized === false`.
### tlsSocket.getPeerCertificate() ### tlsSocket.getPeerCertificate([ detailed ])
Returns an object representing the peer's certificate. The returned object has Returns an object representing the peer's certificate. The returned object has
some properties corresponding to the field of the certificate. some properties corresponding to the field of the certificate. If `detailed`
argument is `true` - the full chain with `issuer` property will be returned,
if `false` - only the top certificate without `issuer` property.
Example: Example:
{ subject: { subject:
{ C: 'UK', { C: 'UK',
ST: 'Acknack Ltd', ST: 'Acknack Ltd',
L: 'Rhys Jones', L: 'Rhys Jones',
O: 'node.js', O: 'node.js',
OU: 'Test TLS Certificate', OU: 'Test TLS Certificate',
CN: 'localhost' }, CN: 'localhost' },
issuer: issuerInfo:
{ C: 'UK', { C: 'UK',
ST: 'Acknack Ltd', ST: 'Acknack Ltd',
L: 'Rhys Jones', L: 'Rhys Jones',
O: 'node.js', O: 'node.js',
OU: 'Test TLS Certificate', OU: 'Test TLS Certificate',
CN: 'localhost' }, CN: 'localhost' },
issuer:
{ ... another certificate ... },
raw: < RAW DER buffer >,
valid_from: 'Nov 11 09:52:22 2009 GMT', valid_from: 'Nov 11 09:52:22 2009 GMT',
valid_to: 'Nov 6 09:52:22 2029 GMT', valid_to: 'Nov 6 09:52:22 2029 GMT',
fingerprint: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF', fingerprint: '2A:7A:C2:DD:E5:F9:CC:53:72:35:99:7A:02:5A:71:38:52:EC:8A:DF',

View File

@ -134,6 +134,9 @@ exports.translatePeerCertificate = function translatePeerCertificate(c) {
return null; return null;
if (c.issuer) c.issuer = tls.parseCertString(c.issuer); if (c.issuer) c.issuer = tls.parseCertString(c.issuer);
if (c.issuerCertificate && c.issuerCertificate !== c) {
c.issuerCertificate = translatePeerCertificate(c.issuerCertificate);
}
if (c.subject) c.subject = tls.parseCertString(c.subject); if (c.subject) c.subject = tls.parseCertString(c.subject);
if (c.infoAccess) { if (c.infoAccess) {
var info = c.infoAccess; var info = c.infoAccess;

View File

@ -378,9 +378,11 @@ CryptoStream.prototype.__defineGetter__('bytesWritten', function() {
return this.socket ? this.socket.bytesWritten : 0; return this.socket ? this.socket.bytesWritten : 0;
}); });
CryptoStream.prototype.getPeerCertificate = function() { CryptoStream.prototype.getPeerCertificate = function(detailed) {
if (this.pair.ssl) if (this.pair.ssl) {
return common.translatePeerCertificate(this.pair.ssl.getPeerCertificate()); return common.translatePeerCertificate(
this.pair.ssl.getPeerCertificate(detailed));
}
return null; return null;
}; };

View File

@ -473,9 +473,11 @@ TLSSocket.prototype.setSession = function(session) {
this.ssl.setSession(session); this.ssl.setSession(session);
}; };
TLSSocket.prototype.getPeerCertificate = function() { TLSSocket.prototype.getPeerCertificate = function(detailed) {
if (this.ssl) if (this.ssl) {
return common.translatePeerCertificate(this.ssl.getPeerCertificate()); return common.translatePeerCertificate(
this.ssl.getPeerCertificate(detailed));
}
return null; return null;
}; };

View File

@ -120,6 +120,7 @@ namespace node {
V(ipv6_lc_string, "ipv6") \ V(ipv6_lc_string, "ipv6") \
V(ipv6_string, "IPv6") \ V(ipv6_string, "IPv6") \
V(issuer_string, "issuer") \ V(issuer_string, "issuer") \
V(issuercert_string, "issuerCertificate") \
V(kill_signal_string, "killSignal") \ V(kill_signal_string, "killSignal") \
V(mac_string, "mac") \ V(mac_string, "mac") \
V(mark_sweep_compact_string, "mark-sweep-compact") \ V(mark_sweep_compact_string, "mark-sweep-compact") \
@ -169,6 +170,7 @@ namespace node {
V(priority_string, "priority") \ V(priority_string, "priority") \
V(processed_string, "processed") \ V(processed_string, "processed") \
V(prototype_string, "prototype") \ V(prototype_string, "prototype") \
V(raw_string, "raw") \
V(rdev_string, "rdev") \ V(rdev_string, "rdev") \
V(readable_string, "readable") \ V(readable_string, "readable") \
V(received_shutdown_string, "receivedShutdown") \ V(received_shutdown_string, "receivedShutdown") \

View File

@ -76,6 +76,7 @@ namespace crypto {
using v8::Array; using v8::Array;
using v8::Boolean; using v8::Boolean;
using v8::Context; using v8::Context;
using v8::EscapableHandleScope;
using v8::Exception; using v8::Exception;
using v8::False; using v8::False;
using v8::FunctionCallbackInfo; using v8::FunctionCallbackInfo;
@ -471,17 +472,35 @@ void SecureContext::SetKey(const FunctionCallbackInfo<Value>& args) {
} }
int SSL_CTX_get_issuer(SSL_CTX* ctx, X509* cert, X509** issuer) {
int ret;
X509_STORE* store = SSL_CTX_get_cert_store(ctx);
X509_STORE_CTX store_ctx;
ret = X509_STORE_CTX_init(&store_ctx, store, NULL, NULL);
if (!ret)
goto end;
ret = X509_STORE_CTX_get1_issuer(issuer, &store_ctx, cert);
X509_STORE_CTX_cleanup(&store_ctx);
end:
return ret;
}
// Read a file that contains our certificate in "PEM" format, // Read a file that contains our certificate in "PEM" format,
// possibly followed by a sequence of CA certificates that should be // possibly followed by a sequence of CA certificates that should be
// sent to the peer in the Certificate message. // sent to the peer in the Certificate message.
// //
// Taken from OpenSSL - editted for style. // Taken from OpenSSL - editted for style.
int SSL_CTX_use_certificate_chain(SSL_CTX *ctx, int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
BIO *in, BIO* in,
X509** cert, X509** cert,
X509** issuer) { X509** issuer) {
int ret = 0; int ret = 0;
X509 *x = NULL; X509* x = NULL;
x = PEM_read_bio_X509_AUX(in, NULL, CryptoPemCallback, NULL); x = PEM_read_bio_X509_AUX(in, NULL, CryptoPemCallback, NULL);
@ -542,16 +561,7 @@ int SSL_CTX_use_certificate_chain(SSL_CTX *ctx,
// Try getting issuer from a cert store // Try getting issuer from a cert store
if (ret) { if (ret) {
if (*issuer == NULL) { if (*issuer == NULL) {
X509_STORE* store = SSL_CTX_get_cert_store(ctx); ret = SSL_CTX_get_issuer(ctx, x, issuer);
X509_STORE_CTX store_ctx;
ret = X509_STORE_CTX_init(&store_ctx, store, NULL, NULL);
if (!ret)
goto end;
ret = X509_STORE_CTX_get1_issuer(issuer, &store_ctx, x);
X509_STORE_CTX_cleanup(&store_ctx);
ret = ret < 0 ? 0 : 1; ret = ret < 0 ? 0 : 1;
// NOTE: get_cert_store doesn't increment reference count, // NOTE: get_cert_store doesn't increment reference count,
// no need to free `store` // no need to free `store`
@ -1081,6 +1091,158 @@ void SSLWrap<Base>::OnClientHello(void* arg,
} }
static Local<Object> X509ToObject(Environment* env, X509* cert) {
EscapableHandleScope scope(env->isolate());
Local<Object> info = Object::New(env->isolate());
BIO* bio = BIO_new(BIO_s_mem());
BUF_MEM* mem;
if (X509_NAME_print_ex(bio,
X509_get_subject_name(cert),
0,
X509_NAME_FLAGS) > 0) {
BIO_get_mem_ptr(bio, &mem);
info->Set(env->subject_string(),
OneByteString(env->isolate(), mem->data, mem->length));
}
(void) BIO_reset(bio);
X509_NAME* issuer_name = X509_get_issuer_name(cert);
if (X509_NAME_print_ex(bio, issuer_name, 0, X509_NAME_FLAGS) > 0) {
BIO_get_mem_ptr(bio, &mem);
info->Set(env->issuer_string(),
OneByteString(env->isolate(), mem->data, mem->length));
}
(void) BIO_reset(bio);
int nids[] = { NID_subject_alt_name, NID_info_access };
Local<String> keys[] = { env->subjectaltname_string(),
env->infoaccess_string() };
CHECK_EQ(ARRAY_SIZE(nids), ARRAY_SIZE(keys));
for (unsigned int i = 0; i < ARRAY_SIZE(nids); i++) {
int index = X509_get_ext_by_NID(cert, nids[i], -1);
if (index < 0)
continue;
X509_EXTENSION* ext;
int rv;
ext = X509_get_ext(cert, index);
assert(ext != NULL);
rv = X509V3_EXT_print(bio, ext, 0, 0);
assert(rv == 1);
BIO_get_mem_ptr(bio, &mem);
info->Set(keys[i],
OneByteString(env->isolate(), mem->data, mem->length));
(void) BIO_reset(bio);
}
EVP_PKEY* pkey = X509_get_pubkey(cert);
RSA* rsa = NULL;
if (pkey != NULL)
rsa = EVP_PKEY_get1_RSA(pkey);
if (rsa != NULL) {
BN_print(bio, rsa->n);
BIO_get_mem_ptr(bio, &mem);
info->Set(env->modulus_string(),
OneByteString(env->isolate(), mem->data, mem->length));
(void) BIO_reset(bio);
BN_print(bio, rsa->e);
BIO_get_mem_ptr(bio, &mem);
info->Set(env->exponent_string(),
OneByteString(env->isolate(), mem->data, mem->length));
(void) BIO_reset(bio);
}
if (pkey != NULL) {
EVP_PKEY_free(pkey);
pkey = NULL;
}
if (rsa != NULL) {
RSA_free(rsa);
rsa = NULL;
}
ASN1_TIME_print(bio, X509_get_notBefore(cert));
BIO_get_mem_ptr(bio, &mem);
info->Set(env->valid_from_string(),
OneByteString(env->isolate(), mem->data, mem->length));
(void) BIO_reset(bio);
ASN1_TIME_print(bio, X509_get_notAfter(cert));
BIO_get_mem_ptr(bio, &mem);
info->Set(env->valid_to_string(),
OneByteString(env->isolate(), mem->data, mem->length));
BIO_free_all(bio);
unsigned int md_size, i;
unsigned char md[EVP_MAX_MD_SIZE];
if (X509_digest(cert, EVP_sha1(), md, &md_size)) {
const char hex[] = "0123456789ABCDEF";
char fingerprint[EVP_MAX_MD_SIZE * 3];
// TODO(indutny): Unify it with buffer's code
for (i = 0; i < md_size; i++) {
fingerprint[3*i] = hex[(md[i] & 0xf0) >> 4];
fingerprint[(3*i)+1] = hex[(md[i] & 0x0f)];
fingerprint[(3*i)+2] = ':';
}
if (md_size > 0) {
fingerprint[(3*(md_size-1))+2] = '\0';
} else {
fingerprint[0] = '\0';
}
info->Set(env->fingerprint_string(),
OneByteString(env->isolate(), fingerprint));
}
STACK_OF(ASN1_OBJECT)* eku = static_cast<STACK_OF(ASN1_OBJECT)*>(
X509_get_ext_d2i(cert, NID_ext_key_usage, NULL, NULL));
if (eku != NULL) {
Local<Array> ext_key_usage = Array::New(env->isolate());
char buf[256];
int j = 0;
for (int i = 0; i < sk_ASN1_OBJECT_num(eku); i++) {
if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(eku, i), 1) >= 0)
ext_key_usage->Set(j++, OneByteString(env->isolate(), buf));
}
sk_ASN1_OBJECT_pop_free(eku, ASN1_OBJECT_free);
info->Set(env->ext_key_usage_string(), ext_key_usage);
}
if (ASN1_INTEGER* serial_number = X509_get_serialNumber(cert)) {
if (BIGNUM* bn = ASN1_INTEGER_to_BN(serial_number, NULL)) {
if (char* buf = BN_bn2hex(bn)) {
info->Set(env->serial_number_string(),
OneByteString(env->isolate(), buf));
OPENSSL_free(buf);
}
BN_free(bn);
}
}
// Raw DER certificate
int size = i2d_X509(cert, NULL);
Local<Object> buff = Buffer::New(env, size);
unsigned char* serialized = reinterpret_cast<unsigned char*>(
Buffer::Data(buff));
i2d_X509(cert, &serialized);
info->Set(env->raw_string(), buff);
return scope.Escape(info);
}
// TODO(indutny): Split it into multiple smaller functions // TODO(indutny): Split it into multiple smaller functions
template <class Base> template <class Base>
void SSLWrap<Base>::GetPeerCertificate( void SSLWrap<Base>::GetPeerCertificate(
@ -1093,148 +1255,96 @@ void SSLWrap<Base>::GetPeerCertificate(
ClearErrorOnReturn clear_error_on_return; ClearErrorOnReturn clear_error_on_return;
(void) &clear_error_on_return; // Silence unused variable warning. (void) &clear_error_on_return; // Silence unused variable warning.
Local<Object> info = Object::New(env->isolate()); Local<Object> result;
X509* peer_cert = SSL_get_peer_certificate(w->ssl_); Local<Object> info;
if (peer_cert != NULL) { X509* cert;
BIO* bio = BIO_new(BIO_s_mem());
BUF_MEM* mem;
if (X509_NAME_print_ex(bio,
X509_get_subject_name(peer_cert),
0,
X509_NAME_FLAGS) > 0) {
BIO_get_mem_ptr(bio, &mem);
info->Set(env->subject_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length));
}
(void) BIO_reset(bio);
X509_NAME* issuer_name = X509_get_issuer_name(peer_cert); STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(w->ssl_);
if (X509_NAME_print_ex(bio, issuer_name, 0, X509_NAME_FLAGS) > 0) { STACK_OF(X509)* peer_certs = NULL;
BIO_get_mem_ptr(bio, &mem); if (ssl_certs == NULL)
info->Set(env->issuer_string(), goto done;
OneByteString(args.GetIsolate(), mem->data, mem->length));
}
(void) BIO_reset(bio);
int nids[] = { NID_subject_alt_name, NID_info_access }; if (sk_X509_num(ssl_certs) == 0) {
Local<String> keys[] = { env->subjectaltname_string(), goto done;
env->infoaccess_string() };
CHECK_EQ(ARRAY_SIZE(nids), ARRAY_SIZE(keys));
for (unsigned int i = 0; i < ARRAY_SIZE(nids); i++) {
int index = X509_get_ext_by_NID(peer_cert, nids[i], -1);
if (index < 0)
continue;
X509_EXTENSION* ext;
int rv;
ext = X509_get_ext(peer_cert, index);
assert(ext != NULL);
rv = X509V3_EXT_print(bio, ext, 0, 0);
assert(rv == 1);
BIO_get_mem_ptr(bio, &mem);
info->Set(keys[i],
OneByteString(args.GetIsolate(), mem->data, mem->length));
(void) BIO_reset(bio);
}
EVP_PKEY* pkey = X509_get_pubkey(peer_cert);
RSA* rsa = NULL;
if (pkey != NULL)
rsa = EVP_PKEY_get1_RSA(pkey);
if (rsa != NULL) {
BN_print(bio, rsa->n);
BIO_get_mem_ptr(bio, &mem);
info->Set(env->modulus_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length));
(void) BIO_reset(bio);
BN_print(bio, rsa->e);
BIO_get_mem_ptr(bio, &mem);
info->Set(env->exponent_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length));
(void) BIO_reset(bio);
}
if (pkey != NULL) {
EVP_PKEY_free(pkey);
pkey = NULL;
}
if (rsa != NULL) {
RSA_free(rsa);
rsa = NULL;
}
ASN1_TIME_print(bio, X509_get_notBefore(peer_cert));
BIO_get_mem_ptr(bio, &mem);
info->Set(env->valid_from_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length));
(void) BIO_reset(bio);
ASN1_TIME_print(bio, X509_get_notAfter(peer_cert));
BIO_get_mem_ptr(bio, &mem);
info->Set(env->valid_to_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length));
BIO_free_all(bio);
unsigned int md_size, i;
unsigned char md[EVP_MAX_MD_SIZE];
if (X509_digest(peer_cert, EVP_sha1(), md, &md_size)) {
const char hex[] = "0123456789ABCDEF";
char fingerprint[EVP_MAX_MD_SIZE * 3];
// TODO(indutny): Unify it with buffer's code
for (i = 0; i < md_size; i++) {
fingerprint[3*i] = hex[(md[i] & 0xf0) >> 4];
fingerprint[(3*i)+1] = hex[(md[i] & 0x0f)];
fingerprint[(3*i)+2] = ':';
}
if (md_size > 0) {
fingerprint[(3*(md_size-1))+2] = '\0';
} else {
fingerprint[0] = '\0';
}
info->Set(env->fingerprint_string(),
OneByteString(args.GetIsolate(), fingerprint));
}
STACK_OF(ASN1_OBJECT)* eku = static_cast<STACK_OF(ASN1_OBJECT)*>(
X509_get_ext_d2i(peer_cert, NID_ext_key_usage, NULL, NULL));
if (eku != NULL) {
Local<Array> ext_key_usage = Array::New(env->isolate());
char buf[256];
int j = 0;
for (int i = 0; i < sk_ASN1_OBJECT_num(eku); i++) {
if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(eku, i), 1) >= 0)
ext_key_usage->Set(j++, OneByteString(args.GetIsolate(), buf));
}
sk_ASN1_OBJECT_pop_free(eku, ASN1_OBJECT_free);
info->Set(env->ext_key_usage_string(), ext_key_usage);
}
if (ASN1_INTEGER* serial_number = X509_get_serialNumber(peer_cert)) {
if (BIGNUM* bn = ASN1_INTEGER_to_BN(serial_number, NULL)) {
if (char* buf = BN_bn2hex(bn)) {
info->Set(env->serial_number_string(),
OneByteString(args.GetIsolate(), buf));
OPENSSL_free(buf);
}
BN_free(bn);
}
}
X509_free(peer_cert);
} }
args.GetReturnValue().Set(info); // Short result requested
if (args.Length() < 1 || !args[0]->IsTrue()) {
result = X509ToObject(env, sk_X509_value(ssl_certs, 0));
goto done;
}
// Clone `ssl_certs`, because we are going to destruct it
peer_certs = sk_X509_new(NULL);
for (int i = 0; i < sk_X509_num(ssl_certs); i++) {
cert = X509_dup(sk_X509_value(ssl_certs, i));
if (cert == NULL)
goto done;
if (!sk_X509_push(peer_certs, cert))
goto done;
}
// First and main certificate
cert = sk_X509_value(peer_certs, 0);
result = X509ToObject(env, cert);
info = result;
// Put issuer inside the object
cert = sk_X509_delete(peer_certs, 0);
while (sk_X509_num(peer_certs) > 0) {
int i;
for (i = 0; i < sk_X509_num(peer_certs); i++) {
X509* ca = sk_X509_value(peer_certs, i);
if (X509_check_issued(ca, cert) != X509_V_OK)
continue;
Local<Object> ca_info = X509ToObject(env, ca);
info->Set(env->issuercert_string(), ca_info);
info = ca_info;
// NOTE: Intentionally freeing cert that is not used anymore
X509_free(cert);
// Delete cert and continue aggregating issuers
cert = sk_X509_delete(peer_certs, i);
break;
}
// Issuer not found, break out of the loop
if (i == sk_X509_num(peer_certs))
break;
}
// Last certificate should be self-signed
while (X509_check_issued(cert, cert) != X509_V_OK) {
X509* ca;
if (SSL_CTX_get_issuer(w->ssl_->ctx, cert, &ca) <= 0)
break;
Local<Object> ca_info = X509ToObject(env, ca);
info->Set(env->issuercert_string(), ca_info);
info = ca_info;
// NOTE: Intentionally freeing cert that is not used anymore
X509_free(cert);
// Delete cert and continue aggregating issuers
cert = ca;
}
// Self-issued certificate
if (X509_check_issued(cert, cert) == X509_V_OK)
info->Set(env->issuercert_string(), info);
CHECK_NE(cert, NULL);
X509_free(cert);
done:
if (peer_certs != NULL)
sk_X509_pop_free(peer_certs, X509_free);
if (result.IsEmpty())
result = Object::New(env->isolate());
args.GetReturnValue().Set(result);
} }

View File

@ -33,8 +33,9 @@ var join = require('path').join;
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var options = { var options = {
key: fs.readFileSync(join(common.fixturesDir, 'agent.key')), key: fs.readFileSync(join(common.fixturesDir, 'keys', 'agent1-key.pem')),
cert: fs.readFileSync(join(common.fixturesDir, 'alice.crt')) cert: fs.readFileSync(join(common.fixturesDir, 'keys', 'agent1-cert.pem')),
ca: [ fs.readFileSync(join(common.fixturesDir, 'keys', 'ca1-cert.pem')) ]
}; };
var verified = false; var verified = false;
@ -47,10 +48,21 @@ server.listen(common.PORT, function() {
rejectUnauthorized: false rejectUnauthorized: false
}, function() { }, function() {
var peerCert = socket.getPeerCertificate(); var peerCert = socket.getPeerCertificate();
assert.ok(!peerCert.issuerCertificate);
// Verify that detailed return value has all certs
peerCert = socket.getPeerCertificate(true);
assert.ok(peerCert.issuerCertificate);
common.debug(util.inspect(peerCert)); common.debug(util.inspect(peerCert));
assert.equal(peerCert.subject.subjectAltName, assert.equal(peerCert.subject.emailAddress, 'ry@tinyclouds.org');
'uniformResourceIdentifier:http://localhost:8000/alice.foaf#me'); assert.equal(peerCert.serialNumber, '9A84ABCFB8A72ABE');
assert.equal(peerCert.serialNumber, 'B9B0D332A1AA5635'); assert.deepEqual(peerCert.infoAccess['OCSP - URI'],
[ 'http://ocsp.nodejs.org/' ]);
var issuer = peerCert.issuerCertificate;
assert.ok(issuer.issuerCertificate === issuer);
assert.equal(issuer.serialNumber, 'B5090C899FC2FF93');
verified = true; verified = true;
server.close(); server.close();
}); });