diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index 3abbe64e478..0e74aa40818 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -644,27 +644,32 @@ specified CAs, otherwise `false` The reason why the peer's certificate has not been verified. This property becomes available only when `tlsSocket.authorized === false`. -### tlsSocket.getPeerCertificate() +### tlsSocket.getPeerCertificate([ detailed ]) 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: - { subject: + { subject: { C: 'UK', ST: 'Acknack Ltd', L: 'Rhys Jones', O: 'node.js', OU: 'Test TLS Certificate', CN: 'localhost' }, - issuer: + issuerInfo: { C: 'UK', ST: 'Acknack Ltd', L: 'Rhys Jones', O: 'node.js', OU: 'Test TLS Certificate', CN: 'localhost' }, + issuer: + { ... another certificate ... }, + raw: < RAW DER buffer >, valid_from: 'Nov 11 09:52:22 2009 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', diff --git a/lib/_tls_common.js b/lib/_tls_common.js index 4cd06f06f86..df2c70cf7b2 100644 --- a/lib/_tls_common.js +++ b/lib/_tls_common.js @@ -134,6 +134,9 @@ exports.translatePeerCertificate = function translatePeerCertificate(c) { return null; 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.infoAccess) { var info = c.infoAccess; diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js index 09fdd577389..8ba661a710e 100644 --- a/lib/_tls_legacy.js +++ b/lib/_tls_legacy.js @@ -378,9 +378,11 @@ CryptoStream.prototype.__defineGetter__('bytesWritten', function() { return this.socket ? this.socket.bytesWritten : 0; }); -CryptoStream.prototype.getPeerCertificate = function() { - if (this.pair.ssl) - return common.translatePeerCertificate(this.pair.ssl.getPeerCertificate()); +CryptoStream.prototype.getPeerCertificate = function(detailed) { + if (this.pair.ssl) { + return common.translatePeerCertificate( + this.pair.ssl.getPeerCertificate(detailed)); + } return null; }; diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index bdedb51210f..183c97112f9 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -473,9 +473,11 @@ TLSSocket.prototype.setSession = function(session) { this.ssl.setSession(session); }; -TLSSocket.prototype.getPeerCertificate = function() { - if (this.ssl) - return common.translatePeerCertificate(this.ssl.getPeerCertificate()); +TLSSocket.prototype.getPeerCertificate = function(detailed) { + if (this.ssl) { + return common.translatePeerCertificate( + this.ssl.getPeerCertificate(detailed)); + } return null; }; diff --git a/src/env.h b/src/env.h index 44054149a4d..d355b4899b8 100644 --- a/src/env.h +++ b/src/env.h @@ -120,6 +120,7 @@ namespace node { V(ipv6_lc_string, "ipv6") \ V(ipv6_string, "IPv6") \ V(issuer_string, "issuer") \ + V(issuercert_string, "issuerCertificate") \ V(kill_signal_string, "killSignal") \ V(mac_string, "mac") \ V(mark_sweep_compact_string, "mark-sweep-compact") \ @@ -169,6 +170,7 @@ namespace node { V(priority_string, "priority") \ V(processed_string, "processed") \ V(prototype_string, "prototype") \ + V(raw_string, "raw") \ V(rdev_string, "rdev") \ V(readable_string, "readable") \ V(received_shutdown_string, "receivedShutdown") \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 0d685c9f516..e29948520c9 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -76,6 +76,7 @@ namespace crypto { using v8::Array; using v8::Boolean; using v8::Context; +using v8::EscapableHandleScope; using v8::Exception; using v8::False; using v8::FunctionCallbackInfo; @@ -471,17 +472,35 @@ void SecureContext::SetKey(const FunctionCallbackInfo& 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, // possibly followed by a sequence of CA certificates that should be // sent to the peer in the Certificate message. // // Taken from OpenSSL - editted for style. -int SSL_CTX_use_certificate_chain(SSL_CTX *ctx, - BIO *in, +int SSL_CTX_use_certificate_chain(SSL_CTX* ctx, + BIO* in, X509** cert, X509** issuer) { int ret = 0; - X509 *x = NULL; + X509* x = 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 if (ret) { if (*issuer == NULL) { - 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, x); - X509_STORE_CTX_cleanup(&store_ctx); - + ret = SSL_CTX_get_issuer(ctx, x, issuer); ret = ret < 0 ? 0 : 1; // NOTE: get_cert_store doesn't increment reference count, // no need to free `store` @@ -1081,6 +1091,158 @@ void SSLWrap::OnClientHello(void* arg, } +static Local X509ToObject(Environment* env, X509* cert) { + EscapableHandleScope scope(env->isolate()); + + Local 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 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( + X509_get_ext_d2i(cert, NID_ext_key_usage, NULL, NULL)); + if (eku != NULL) { + Local 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 buff = Buffer::New(env, size); + unsigned char* serialized = reinterpret_cast( + 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 template void SSLWrap::GetPeerCertificate( @@ -1093,148 +1255,96 @@ void SSLWrap::GetPeerCertificate( ClearErrorOnReturn clear_error_on_return; (void) &clear_error_on_return; // Silence unused variable warning. - Local info = Object::New(env->isolate()); - X509* peer_cert = SSL_get_peer_certificate(w->ssl_); - if (peer_cert != NULL) { - 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); + Local result; + Local info; + X509* cert; - X509_NAME* issuer_name = X509_get_issuer_name(peer_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(args.GetIsolate(), mem->data, mem->length)); - } - (void) BIO_reset(bio); + STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(w->ssl_); + STACK_OF(X509)* peer_certs = NULL; + if (ssl_certs == NULL) + goto done; - int nids[] = { NID_subject_alt_name, NID_info_access }; - Local 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(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( - X509_get_ext_d2i(peer_cert, NID_ext_key_usage, NULL, NULL)); - if (eku != NULL) { - Local 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); + if (sk_X509_num(ssl_certs) == 0) { + goto done; } - 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 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 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); } diff --git a/test/simple/test-tls-peer-certificate.js b/test/simple/test-tls-peer-certificate.js index 8b0418e11f4..f1d0a7f1971 100644 --- a/test/simple/test-tls-peer-certificate.js +++ b/test/simple/test-tls-peer-certificate.js @@ -33,8 +33,9 @@ var join = require('path').join; var spawn = require('child_process').spawn; var options = { - key: fs.readFileSync(join(common.fixturesDir, 'agent.key')), - cert: fs.readFileSync(join(common.fixturesDir, 'alice.crt')) + key: fs.readFileSync(join(common.fixturesDir, 'keys', 'agent1-key.pem')), + cert: fs.readFileSync(join(common.fixturesDir, 'keys', 'agent1-cert.pem')), + ca: [ fs.readFileSync(join(common.fixturesDir, 'keys', 'ca1-cert.pem')) ] }; var verified = false; @@ -47,10 +48,21 @@ server.listen(common.PORT, function() { rejectUnauthorized: false }, function() { 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)); - assert.equal(peerCert.subject.subjectAltName, - 'uniformResourceIdentifier:http://localhost:8000/alice.foaf#me'); - assert.equal(peerCert.serialNumber, 'B9B0D332A1AA5635'); + assert.equal(peerCert.subject.emailAddress, 'ry@tinyclouds.org'); + assert.equal(peerCert.serialNumber, '9A84ABCFB8A72ABE'); + 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; server.close(); });