Browse Source

tls: `getPeerCertificate(detailed)`

Add `raw` property to certificate, add mode to output full certificate
chain.
v0.11.13-release
Fedor Indutny 11 years ago
parent
commit
345c40b661
  1. 11
      doc/api/tls.markdown
  2. 3
      lib/_tls_common.js
  3. 8
      lib/_tls_legacy.js
  4. 8
      lib/_tls_wrap.js
  5. 2
      src/env.h
  6. 382
      src/node_crypto.cc
  7. 22
      test/simple/test-tls-peer-certificate.js

11
doc/api/tls.markdown

@ -644,10 +644,12 @@ 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:
@ -658,13 +660,16 @@ Example:
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',

3
lib/_tls_common.js

@ -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;

8
lib/_tls_legacy.js

@ -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;
}; };

8
lib/_tls_wrap.js

@ -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;
}; };

2
src/env.h

@ -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") \

382
src/node_crypto.cc

@ -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,160 +1091,260 @@ void SSLWrap<Base>::OnClientHello(void* arg,
} }
// TODO(indutny): Split it into multiple smaller functions static Local<Object> X509ToObject(Environment* env, X509* cert) {
template <class Base> EscapableHandleScope scope(env->isolate());
void SSLWrap<Base>::GetPeerCertificate(
const FunctionCallbackInfo<Value>& args) {
HandleScope scope(args.GetIsolate());
Base* w = Unwrap<Base>(args.Holder()); Local<Object> info = Object::New(env->isolate());
Environment* env = w->ssl_env();
ClearErrorOnReturn clear_error_on_return; BIO* bio = BIO_new(BIO_s_mem());
(void) &clear_error_on_return; // Silence unused variable warning. 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);
Local<Object> info = Object::New(env->isolate()); X509_NAME* issuer_name = X509_get_issuer_name(cert);
X509* peer_cert = SSL_get_peer_certificate(w->ssl_); if (X509_NAME_print_ex(bio, issuer_name, 0, X509_NAME_FLAGS) > 0) {
if (peer_cert != NULL) { BIO_get_mem_ptr(bio, &mem);
BIO* bio = BIO_new(BIO_s_mem()); info->Set(env->issuer_string(),
BUF_MEM* mem; OneByteString(env->isolate(), mem->data, mem->length));
if (X509_NAME_print_ex(bio, }
X509_get_subject_name(peer_cert), (void) BIO_reset(bio);
0,
X509_NAME_FLAGS) > 0) { int nids[] = { NID_subject_alt_name, NID_info_access };
BIO_get_mem_ptr(bio, &mem); Local<String> keys[] = { env->subjectaltname_string(),
info->Set(env->subject_string(), env->infoaccess_string() };
OneByteString(args.GetIsolate(), mem->data, mem->length)); CHECK_EQ(ARRAY_SIZE(nids), ARRAY_SIZE(keys));
} for (unsigned int i = 0; i < ARRAY_SIZE(nids); i++) {
(void) BIO_reset(bio); int index = X509_get_ext_by_NID(cert, nids[i], -1);
if (index < 0)
continue;
X509_NAME* issuer_name = X509_get_issuer_name(peer_cert); X509_EXTENSION* ext;
if (X509_NAME_print_ex(bio, issuer_name, 0, X509_NAME_FLAGS) > 0) { int rv;
BIO_get_mem_ptr(bio, &mem);
info->Set(env->issuer_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length));
}
(void) BIO_reset(bio);
int nids[] = { NID_subject_alt_name, NID_info_access }; ext = X509_get_ext(cert, index);
Local<String> keys[] = { env->subjectaltname_string(), assert(ext != NULL);
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; rv = X509V3_EXT_print(bio, ext, 0, 0);
int rv; assert(rv == 1);
ext = X509_get_ext(peer_cert, index); BIO_get_mem_ptr(bio, &mem);
assert(ext != NULL); info->Set(keys[i],
OneByteString(env->isolate(), mem->data, mem->length));
rv = X509V3_EXT_print(bio, ext, 0, 0); (void) BIO_reset(bio);
assert(rv == 1); }
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); BIO_get_mem_ptr(bio, &mem);
info->Set(keys[i], info->Set(env->modulus_string(),
OneByteString(args.GetIsolate(), mem->data, mem->length)); 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); (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] = ':';
} }
EVP_PKEY* pkey = X509_get_pubkey(peer_cert); if (md_size > 0) {
RSA* rsa = NULL; fingerprint[(3*(md_size-1))+2] = '\0';
if (pkey != NULL) } else {
rsa = EVP_PKEY_get1_RSA(pkey); fingerprint[0] = '\0';
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) { info->Set(env->fingerprint_string(),
EVP_PKEY_free(pkey); OneByteString(env->isolate(), fingerprint));
pkey = NULL; }
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));
} }
if (rsa != NULL) {
RSA_free(rsa); sk_ASN1_OBJECT_pop_free(eku, ASN1_OBJECT_free);
rsa = NULL; 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);
} }
}
ASN1_TIME_print(bio, X509_get_notBefore(peer_cert)); // Raw DER certificate
BIO_get_mem_ptr(bio, &mem); int size = i2d_X509(cert, NULL);
info->Set(env->valid_from_string(), Local<Object> buff = Buffer::New(env, size);
OneByteString(args.GetIsolate(), mem->data, mem->length)); unsigned char* serialized = reinterpret_cast<unsigned char*>(
(void) BIO_reset(bio); Buffer::Data(buff));
i2d_X509(cert, &serialized);
info->Set(env->raw_string(), buff);
ASN1_TIME_print(bio, X509_get_notAfter(peer_cert)); return scope.Escape(info);
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) { // TODO(indutny): Split it into multiple smaller functions
fingerprint[(3*(md_size-1))+2] = '\0'; template <class Base>
} else { void SSLWrap<Base>::GetPeerCertificate(
fingerprint[0] = '\0'; const FunctionCallbackInfo<Value>& args) {
} HandleScope scope(args.GetIsolate());
info->Set(env->fingerprint_string(), Base* w = Unwrap<Base>(args.Holder());
OneByteString(args.GetIsolate(), fingerprint)); Environment* env = w->ssl_env();
}
ClearErrorOnReturn clear_error_on_return;
(void) &clear_error_on_return; // Silence unused variable warning.
STACK_OF(ASN1_OBJECT)* eku = static_cast<STACK_OF(ASN1_OBJECT)*>( Local<Object> result;
X509_get_ext_d2i(peer_cert, NID_ext_key_usage, NULL, NULL)); Local<Object> info;
if (eku != NULL) { X509* cert;
Local<Array> ext_key_usage = Array::New(env->isolate());
char buf[256];
int j = 0; STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(w->ssl_);
for (int i = 0; i < sk_ASN1_OBJECT_num(eku); i++) { STACK_OF(X509)* peer_certs = NULL;
if (OBJ_obj2txt(buf, sizeof(buf), sk_ASN1_OBJECT_value(eku, i), 1) >= 0) if (ssl_certs == NULL)
ext_key_usage->Set(j++, OneByteString(args.GetIsolate(), buf)); goto done;
}
sk_ASN1_OBJECT_pop_free(eku, ASN1_OBJECT_free); if (sk_X509_num(ssl_certs) == 0) {
info->Set(env->ext_key_usage_string(), ext_key_usage); goto done;
} }
if (ASN1_INTEGER* serial_number = X509_get_serialNumber(peer_cert)) { // Short result requested
if (BIGNUM* bn = ASN1_INTEGER_to_BN(serial_number, NULL)) { if (args.Length() < 1 || !args[0]->IsTrue()) {
if (char* buf = BN_bn2hex(bn)) { result = X509ToObject(env, sk_X509_value(ssl_certs, 0));
info->Set(env->serial_number_string(), goto done;
OneByteString(args.GetIsolate(), buf)); }
OPENSSL_free(buf);
} // Clone `ssl_certs`, because we are going to destruct it
BN_free(bn); 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;
} }
X509_free(peer_cert); // Issuer not found, break out of the loop
if (i == sk_X509_num(peer_certs))
break;
} }
args.GetReturnValue().Set(info); // 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);
} }

22
test/simple/test-tls-peer-certificate.js

@ -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();
}); });

Loading…
Cancel
Save