diff --git a/.gitignore b/.gitignore index 801e565..6b475b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ + +node_modules build *.log +*.DS_Store diff --git a/.travis.yml b/.travis.yml index 0b81284..c45c402 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,30 @@ -language: node_js +os: + - linux + +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 +language: node_js node_js: - - "0.11" + - "stable" + - "4.0" + - "0.12" - "0.10" - - "0.8" - -notifications: - email: false - irc: - channels: - - "chat.freenode.net#jitsu" - on_success: change - on_failure: change +install: + - export CXX=g++-4.8 + - $CXX --version + - npm i before_install: - npm install -g node-gyp - rm -rf ~/.node-gyp/ + +notifications: + email: false + +sudo: false diff --git a/README.md b/README.md index 68c9899..6a49152 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ node-x509 ========= +[![Build Status](https://travis-ci.org/Southern/node-x509.svg?branch=master)](https://travis-ci.org/Southern/node-x509) + Simple X509 certificate parser. ## Installation -From NPM *(recommended)*: `npm install x.509` +From NPM *(recommended)*: `npm install x509` Building and testing from source: ``` @@ -17,16 +19,14 @@ npm test ## Usage Reading from a file: ```js -var x509 = require('x509'); - +const x509 = require('x509'); var issuer = x509.getIssuer(__dirname + '/certs/your.crt'); ``` Reading from a string: ```js -var fs = require('fs'), - x509 = require('x509'); - +const fs = require('fs'), + x509 = require('x509'); var issuer = x509.getIssuer(fs.readFileSync('./certs/your.crt').toString()); ``` @@ -34,13 +34,11 @@ var issuer = x509.getIssuer(fs.readFileSync('./certs/your.crt').toString()); **Notes:** - `cert` may be a filename or a raw base64 encoded PEM string in any of these methods. - #### x509.getAltNames(`cert`) Parse certificate with `x509.parseCert` and return the alternate names. ```js -var x509 = require('x509'); - +const x509 = require('x509'); var altNames = x509.getAltNames(__dirname + '/certs/nodejitsu.com.crt'); /* altNames = [ '*.nodejitsu.com', 'nodejitsu.com' ] @@ -51,8 +49,7 @@ altNames = [ '*.nodejitsu.com', 'nodejitsu.com' ] Parse certificate with `x509.parseCert` and return the issuer. ```js -var x509 = require('x509'); - +const x509 = require('x509'); var issuer = x509.getIssuer(__dirname + '/certs/nodejitsu.com.crt'); /* issuer = { countryName: 'GB', @@ -67,8 +64,7 @@ issuer = { countryName: 'GB', Parse certificate with `x509.parseCert` and return the subject. ```js -var x509 = require('x509'); - +const x509 = require('x509'); var subject = x509.getSubject(__dirname + '/certs/nodejitsu.com.crt'); /* subject = { countryName: 'US', @@ -86,11 +82,10 @@ subject = { countryName: 'US', Parse subject, issuer, valid before and after date, and alternate names from certificate. ```js -var x509 = require('x509'); - +const x509 = require('x509'); var cert = x509.parseCert(__dirname + '/certs/nodejitsu.com.crt'); /* -cert = { subject: +cert = { subject: { countryName: 'US', postalCode: '10010', stateOrProvinceName: 'NY', @@ -99,7 +94,7 @@ cert = { subject: organizationName: 'Nodejitsu', organizationalUnitName: 'PremiumSSL Wildcard', commonName: '*.nodejitsu.com' }, - issuer: + issuer: { countryName: 'GB', stateOrProvinceName: 'Greater Manchester', localityName: 'Salford', @@ -117,11 +112,36 @@ cert = { subject: */ ``` + +#### x509.verify(`cert`, `CABundlePath`, function(err, result){ /*...*/}) + +Performs basic certificate validation against a bundle of ca certificates. + +It accepts an error-first callback as first argument. If the error is null, then +the certificate is valid. + +The error messages are the same returned by openssl: [x509_verify_cert_error_string](https://www.openssl.org/docs/man1.0.2/crypto/X509_STORE_CTX_get_error.html) + + +**Note:** +As now, this function only accepts absolute paths to existing files as arguments + +```js +const x509 = require('x509'); + +x509.verify( + __dirname + '/certs/user.com.crt', + __dirname + 'enduser-example.com.chain', + function(err, result){ /*...*/} +); + +``` + ## Examples Checking the date to make sure the certificate is active: ```js -var x509 = require('x509'), - cert = x509.parseCert('yourcert.crt'), +const x509 = require('x509'); +var cert = x509.parseCert('yourcert.crt'), date = new Date(); if (cert.notBefore > date) { @@ -134,4 +154,7 @@ if (cert.notAfter < date) { ## License -MIT \ No newline at end of file +MIT + +#### Alternative implementation / build issues +If you are suffering from hard to fix build issues, there is an alternative (pure javascript) implementation using emscripten: https://github.com/encharm/x509.js (based on node-x509, slightly different API) diff --git a/binding.gyp b/binding.gyp index 4f9f9db..b01bee3 100644 --- a/binding.gyp +++ b/binding.gyp @@ -16,7 +16,8 @@ 'src/x509.cc' ], 'include_dirs': [ - 'include' + 'include', + " exports); +void init(Local exports); #endif diff --git a/include/x509.h b/include/x509.h index 18e321e..ce1198b 100644 --- a/include/x509.h +++ b/include/x509.h @@ -4,6 +4,8 @@ // Include header for addon version, node/v8 inclusions, etc. #include #include +#include +#include // OpenSSL headers #include @@ -17,23 +19,19 @@ using namespace v8; -#if NODE_VERSION_AT_LEAST(0, 11, 3) && defined(__APPLE__) - void get_altnames(const FunctionCallbackInfo &args); - void get_subject(const FunctionCallbackInfo &args); - void get_issuer(const FunctionCallbackInfo &args); - char* parse_args(const FunctionCallbackInfo &args); - void parse_cert(const FunctionCallbackInfo &args); -#else - Handle get_altnames(const Arguments &args); - Handle get_subject(const Arguments &args); - Handle get_issuer(const Arguments &args); - Handle parse_cert(const Arguments &args); -#endif +NAN_METHOD(get_altnames); +NAN_METHOD(get_subject); +NAN_METHOD(get_issuer); +NAN_METHOD(parse_cert); +NAN_METHOD(verify); + +Local try_parse(const std::string& dataString); +Local verify(const std::string& dataString); +Local parse_date(ASN1_TIME *date); +Local parse_serial(ASN1_INTEGER *serial); +Local parse_name(X509_NAME *subject); -Handle try_parse(char *data); -Handle parse_date(char *date); -Handle parse_serial(ASN1_INTEGER *serial); -Handle parse_name(X509_NAME *subject); char* real_name(char *data); +char* trim(char *data, int len); #endif diff --git a/index.js b/index.js index df4c476..539a76c 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,43 @@ - var x509 = require('./build/Release/x509'); +var fs = require('fs'); exports.version = x509.version; exports.getAltNames = x509.getAltNames; exports.getSubject = x509.getSubject; exports.getIssuer = x509.getIssuer; +exports.verify = function(certPath, CABundlePath, cb) { + if (!certPath) { + throw new TypeError('Certificate path is required'); + } + if (!CABundlePath) { + throw new TypeError('CA Bundle path is required'); + } + + fs.stat(certPath, function(certPathErr) { + + if (certPathErr) { + return cb(certPathErr); + } + + fs.stat(CABundlePath, function(bundlePathErr) { + + if (bundlePathErr) { + return cb(bundlePathErr); + } + + try { + x509.verify(certPath, CABundlePath); + cb(null); + } + catch (verificationError) { + cb(verificationError); + } + }); + }); +}; + + exports.parseCert = function(path) { var ret = x509.parseCert(path); var exts = {}; @@ -17,4 +49,4 @@ exports.parseCert = function(path) { delete ret.extensions; ret.extensions = exts; return ret; -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index 3a2e17a..cc6ee44 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,18 @@ { - "name": "x.509", - "version": "0.1.1", + "name": "x509", + "version": "0.3.4", "description": "Simple X509 certificate parser.", - "author": "Yorkie Neil ", + "author": "Colton Baker", "main": "index.js", "repository": { "type": "git", - "url": "http://github.com/yorkie/node-x509" + "url": "git@github.com:Southern/node-x509.git" }, "scripts": { "test": "node test/test" }, - "license": "MIT" + "license": "MIT", + "dependencies": { + "nan": "2.12.0" + } } diff --git a/src/addon.cc b/src/addon.cc index 72bae35..f0aab0c 100644 --- a/src/addon.cc +++ b/src/addon.cc @@ -6,12 +6,27 @@ using namespace v8; -void init(Handle exports) { - exports->Set(String::NewSymbol("version"), String::New(VERSION)); - exports->Set(String::NewSymbol("getAltNames"), FunctionTemplate::New(get_altnames)->GetFunction()); - exports->Set(String::NewSymbol("getSubject"), FunctionTemplate::New(get_subject)->GetFunction()); - exports->Set(String::NewSymbol("getIssuer"), FunctionTemplate::New(get_issuer)->GetFunction()); - exports->Set(String::NewSymbol("parseCert"), FunctionTemplate::New(parse_cert)->GetFunction()); +void init(Local exports) { + Nan::Set(exports, + Nan::New("version").ToLocalChecked(), + Nan::New(VERSION).ToLocalChecked()); + + Nan::Set(exports, + Nan::New("verify").ToLocalChecked(), + Nan::New(verify)->GetFunction()); + + Nan::Set(exports, + Nan::New("getAltNames").ToLocalChecked(), + Nan::New(get_altnames)->GetFunction()); + Nan::Set(exports, + Nan::New("getSubject").ToLocalChecked(), + Nan::New(get_subject)->GetFunction()); + Nan::Set(exports, + Nan::New("getIssuer").ToLocalChecked(), + Nan::New(get_issuer)->GetFunction()); + Nan::Set(exports, + Nan::New("parseCert").ToLocalChecked(), + Nan::New(parse_cert)->GetFunction()); } NODE_MODULE(x509, init) diff --git a/src/x509.cc b/src/x509.cc index 6048d06..00e02ae 100644 --- a/src/x509.cc +++ b/src/x509.cc @@ -1,180 +1,257 @@ #include +#include #include using namespace v8; // Field names that OpenSSL is missing. -char *MISSING[3][2] = { +static const char *MISSING[4][2] = { { - (char*) "1.3.6.1.4.1.311.60.2.1.1", - (char*) "jurisdictionOfIncorpationLocalityName" + "1.2.840.113533.7.65.0", + "entrustVersionInfo" }, { - (char*) "1.3.6.1.4.1.311.60.2.1.2", - (char*) "jurisdictionOfIncorporationStateOrProvinceName" + "1.3.6.1.4.1.311.60.2.1.1", + "jurisdictionOfIncorpationLocalityName" }, { - (char*) "1.3.6.1.4.1.311.60.2.1.3", - (char*) "jurisdictionOfIncorporationCountryName" + "1.3.6.1.4.1.311.60.2.1.2", + "jurisdictionOfIncorporationStateOrProvinceName" + }, + + { + "1.3.6.1.4.1.311.60.2.1.3", + "jurisdictionOfIncorporationCountryName" } }; - -#if NODE_VERSION_AT_LEAST(0, 11, 3) && defined(__APPLE__) -/* - * Code for 0.11.3 and higher. - */ -void get_altnames(const FunctionCallbackInfo &args) { - Local exports(try_parse(parse_args(args))->ToObject()); - args.GetReturnValue().Set(exports->Get(String::NewSymbol("altNames"))); -} - -void get_subject(const FunctionCallbackInfo &args) { - Local exports(try_parse(parse_args(args))->ToObject()); - args.GetReturnValue().Set(exports->Get(String::NewSymbol("subject"))); -} - -void get_issuer(const FunctionCallbackInfo &args) { - Local exports(try_parse(parse_args(args))->ToObject()); - args.GetReturnValue().Set(exports->Get(String::NewSymbol("issuer"))); -} - -char* parse_args(const FunctionCallbackInfo &args) { - if (args.Length() == 0) { - ThrowException(Exception::Error(String::New("Must provide a certificate file."))); - return NULL; +std::string parse_args(const Nan::FunctionCallbackInfo& info) { + if (info.Length() == 0) { + Nan::ThrowTypeError("Must provide a certificate string."); + return std::string(); } - if (!args[0]->IsString()) { - ThrowException(Exception::TypeError(String::New("Certificate must be a string."))); - return NULL; + if (!info[0]->IsString()) { + Nan::ThrowTypeError("Certificate must be a string."); + return std::string(); } - if (args[0]->ToString()->Length() == 0) { - ThrowException(Exception::TypeError(String::New("Certificate argument provided, but left blank."))); - return NULL; + if (info[0]->ToString()->Length() == 0) { + Nan::ThrowTypeError("Certificate argument provided, but left blank."); + return std::string(); } - char *value = (char*) malloc(sizeof(char*) * args[0]->ToString()->Length()); - sprintf(value, "%s", *String::Utf8Value(args[0]->ToString())); - return value; + return *Nan::Utf8String(info[0]->ToString()); } -void parse_cert(const FunctionCallbackInfo &args) { - Local exports(try_parse(parse_args(args))->ToObject()); - args.GetReturnValue().Set(exports); -} -#else -/* - * Code for 0.11.2 and lower. - */ -Handle get_altnames(const Arguments &args) { - HandleScope scope; - Handle exports(Handle::Cast(parse_cert(args))); - return scope.Close(exports->Get(String::NewSymbol("altNames"))); -} +NAN_METHOD(verify) { + Nan::HandleScope scope; + OpenSSL_add_all_algorithms(); -Handle get_subject(const Arguments &args) { - HandleScope scope; - Handle exports(Handle::Cast(parse_cert(args))); + std::string cert_path = *String::Utf8Value(info[0]->ToString()); + std::string ca_bundlestr = *String::Utf8Value(info[1]->ToString()); - return scope.Close(exports->Get(String::NewSymbol("subject"))); -} + X509_STORE *store = NULL; + X509_STORE_CTX *verify_ctx = NULL; + X509 *cert = NULL; + BIO *cert_bio = NULL; + const char *error = NULL; -Handle get_issuer(const Arguments &args) { - HandleScope scope; - Handle exports(Handle::Cast(parse_cert(args))); - - return scope.Close(exports->Get(String::NewSymbol("issuer"))); + do { + store = X509_STORE_new(); + if (store == NULL) { + error = "Failed to create X509 certificate store."; + break; + } + verify_ctx = X509_STORE_CTX_new(); + if (verify_ctx == NULL) { + error = "Failed to create X509 verification context."; + break; + } + cert_bio = BIO_new(BIO_s_file()); + int ret = BIO_read_filename(cert_bio, cert_path.c_str()); + if (ret != 1) { + error = "Error reading file"; + break; + } + cert = PEM_read_bio_X509(cert_bio, NULL, 0, NULL); + if (cert == NULL) { + error = "Failed to load cert"; + break; + } + ret = X509_STORE_load_locations(store, ca_bundlestr.c_str(), NULL); + if (ret != 1) { + error = "Error loading CA chain file"; + break; + } + X509_STORE_CTX_init(verify_ctx, store, cert, NULL); + ret = X509_verify_cert(verify_ctx); + if (ret <= 0) { + error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(verify_ctx)); + break; + } + } while(0); + + X509_STORE_free(store); + X509_free(cert); + X509_STORE_CTX_free(verify_ctx); + BIO_free_all(cert_bio); + if (error != NULL) { + Nan::ThrowError(error); + } else { + info.GetReturnValue().Set(Nan::New(true)); + } } -Handle parse_cert(const Arguments &args) { - HandleScope scope; - if (args.Length() == 0) { - ThrowException(Exception::Error(String::New("Must provide a certificate file."))); - return scope.Close(Undefined()); - } - if (!args[0]->IsString()) { - ThrowException(Exception::TypeError(String::New("Certificate must be a string."))); - return scope.Close(Undefined()); +NAN_METHOD(get_altnames) { + Nan::HandleScope scope; + std::string parsed_arg = parse_args(info); + if(parsed_arg.size() == 0) { + info.GetReturnValue().SetUndefined(); } + Local exports(try_parse(parsed_arg)->ToObject()); + Local key = Nan::New("altNames").ToLocalChecked(); + info.GetReturnValue().Set( + Nan::Get(exports, key).ToLocalChecked()); + ERR_clear_error(); +} - if (args[0]->ToString()->Length() == 0) { - ThrowException(Exception::TypeError(String::New("Certificate argument provided, but left blank."))); - return scope.Close(Undefined()); +NAN_METHOD(get_subject) { + Nan::HandleScope scope; + std::string parsed_arg = parse_args(info); + if(parsed_arg.size() == 0) { + info.GetReturnValue().SetUndefined(); } - - String::Utf8Value value(args[0]); - return scope.Close(try_parse(*value)); + Local exports(try_parse(parsed_arg)->ToObject()); + Local key = Nan::New("subject").ToLocalChecked(); + info.GetReturnValue().Set( + Nan::Get(exports, key).ToLocalChecked()); + ERR_clear_error(); } -#endif // NODE_VERSION_AT_LEAST +NAN_METHOD(get_issuer) { + Nan::HandleScope scope; + std::string parsed_arg = parse_args(info); + if(parsed_arg.size() == 0) { + info.GetReturnValue().SetUndefined(); + } + Local exports(try_parse(parsed_arg)->ToObject()); + Local key = Nan::New("issuer").ToLocalChecked(); + info.GetReturnValue().Set( + Nan::Get(exports, key).ToLocalChecked()); + ERR_clear_error(); +} +NAN_METHOD(parse_cert) { + Nan::HandleScope scope; + std::string parsed_arg = parse_args(info); + if(parsed_arg.size() == 0) { + info.GetReturnValue().SetUndefined(); + } + Local exports(try_parse(parsed_arg)->ToObject()); + info.GetReturnValue().Set(exports); + ERR_clear_error(); +} /* * This is where everything is handled for both -0.11.2 and 0.11.3+. */ -Handle try_parse(char *data) { - HandleScope scope; - Handle exports(Object::New()); +Local try_parse(const std::string& dataString) { + Nan::EscapableHandleScope scope; + const char* data = dataString.c_str(); + + Local exports = Nan::New(); X509 *cert; BIO *bio = BIO_new(BIO_s_mem()); int result = BIO_puts(bio, data); if (result == -2) { - ThrowException(Exception::Error(String::New("BIO doesn't support BIO_puts."))); - return scope.Close(exports); + Nan::ThrowError("BIO doesn't support BIO_puts."); + BIO_free(bio); + return scope.Escape(exports); } else if (result <= 0) { - ThrowException(Exception::Error(String::New("No data was written to BIO."))); - return scope.Close(exports); + Nan::ThrowError("No data was written to BIO."); + BIO_free(bio); + return scope.Escape(exports); } // Try raw read cert = PEM_read_bio_X509(bio, NULL, 0, NULL); if (cert == NULL) { + BIO_free_all(bio); // Switch to file BIO bio = BIO_new(BIO_s_file()); // If raw read fails, try reading the input as a filename. if (!BIO_read_filename(bio, data)) { - ThrowException(Exception::Error(String::New("File doesn't exist."))); - return scope.Close(exports); + ERR_clear_error(); + Nan::ThrowError("File doesn't exist."); + BIO_free(bio); + return scope.Escape(exports); } // Try reading the bio again with the file in it. cert = PEM_read_bio_X509(bio, NULL, 0, NULL); if (cert == NULL) { - ThrowException(Exception::Error(String::New("Unable to parse certificate."))); - return scope.Close(exports); + ERR_clear_error(); + Nan::ThrowError("Unable to parse certificate."); + BIO_free(bio); + return scope.Escape(exports); } } - exports->Set(String::NewSymbol("version"), Integer::New((int) X509_get_version(cert))); - exports->Set(String::NewSymbol("subject"), parse_name(X509_get_subject_name(cert))); - exports->Set(String::NewSymbol("issuer"), parse_name(X509_get_issuer_name(cert))); - exports->Set(String::NewSymbol("serial"), parse_serial(X509_get_serialNumber(cert))); - exports->Set(String::NewSymbol("notBefore"), parse_date((char*) ASN1_STRING_data(X509_get_notBefore(cert)))); - exports->Set(String::NewSymbol("notAfter"), parse_date((char*) ASN1_STRING_data(X509_get_notAfter(cert)))); + Nan::Set(exports, + Nan::New("version").ToLocalChecked(), + Nan::New((int) X509_get_version(cert))); + Nan::Set(exports, + Nan::New("subject").ToLocalChecked(), + parse_name(X509_get_subject_name(cert))); + Nan::Set(exports, + Nan::New("issuer").ToLocalChecked(), + parse_name(X509_get_issuer_name(cert))); + Nan::Set(exports, + Nan::New("serial").ToLocalChecked(), + parse_serial(X509_get_serialNumber(cert))); + Nan::Set(exports, + Nan::New("notBefore").ToLocalChecked(), + parse_date(X509_get_notBefore(cert))); + Nan::Set(exports, + Nan::New("notAfter").ToLocalChecked(), + parse_date(X509_get_notAfter(cert))); + + // Subject hash + std::stringstream stream; + stream << std::hex << X509_subject_name_hash(cert); + Nan::Set(exports, + Nan::New("subjectHash").ToLocalChecked(), + Nan::New(stream.str()).ToLocalChecked()); // Signature Algorithm +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + int sig_alg_nid = X509_get_signature_nid(cert); +#else int sig_alg_nid = OBJ_obj2nid(cert->sig_alg->algorithm); +#endif if (sig_alg_nid == NID_undef) { - ThrowException(Exception::Error( - String::New("unable to find specified signature algorithm name."))); - return scope.Close(Undefined()); + ERR_clear_error(); + Nan::ThrowError("unable to find specified signature algorithm name."); + X509_free(cert); + BIO_free(bio); + return scope.Escape(exports); } - exports->Set(String::NewSymbol("signatureAlgorithm"), - String::New(OBJ_nid2ln(sig_alg_nid))); + Nan::Set(exports, + Nan::New("signatureAlgorithm").ToLocalChecked(), + Nan::New(OBJ_nid2ln(sig_alg_nid)).ToLocalChecked()); // fingerPrint unsigned int md_size, idx; @@ -193,35 +270,64 @@ Handle try_parse(char *data) { } else { fingerprint[0] = '\0'; } - exports->Set(String::NewSymbol("fingerPrint"), String::New(fingerprint)); + Nan::Set(exports, + Nan::New("fingerPrint").ToLocalChecked(), + Nan::New(fingerprint).ToLocalChecked()); } // public key +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + int pkey_nid = X509_get_signature_nid(cert); +#else int pkey_nid = OBJ_obj2nid(cert->cert_info->key->algor->algorithm); +#endif if (pkey_nid == NID_undef) { - ThrowException(Exception::Error( - String::New("unable to find specified public key algorithm name."))); - return scope.Close(Undefined()); + ERR_clear_error(); + Nan::ThrowError("unable to find specified public key algorithm name."); + X509_free(cert); + BIO_free(bio); + return scope.Escape(exports); } EVP_PKEY *pkey = X509_get_pubkey(cert); - Local publicKey = Object::New(); - publicKey->Set(String::NewSymbol("algorithm"), - String::New(OBJ_nid2ln(pkey_nid))); + Local publicKey = Nan::New(); + Nan::Set(publicKey, + Nan::New("algorithm").ToLocalChecked(), + Nan::New(OBJ_nid2ln(pkey_nid)).ToLocalChecked()); if (pkey_nid == NID_rsaEncryption) { char *rsa_e_dec, *rsa_n_hex; + uint32_t rsa_key_length_int; RSA *rsa_key; +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + rsa_key = EVP_PKEY_get1_RSA(pkey); + const BIGNUM *n; + const BIGNUM *e; + RSA_get0_key(rsa_key, &n, &e, NULL); + rsa_e_dec = BN_bn2dec(e); + rsa_n_hex = BN_bn2hex(n); +#else rsa_key = pkey->pkey.rsa; rsa_e_dec = BN_bn2dec(rsa_key->e); rsa_n_hex = BN_bn2hex(rsa_key->n); - publicKey->Set(String::NewSymbol("e"), String::New(rsa_e_dec)); - publicKey->Set(String::NewSymbol("n"), String::New(rsa_n_hex)); +#endif + rsa_key_length_int = RSA_size(rsa_key) * 8; + Nan::Set(publicKey, + Nan::New("e").ToLocalChecked(), + Nan::New(rsa_e_dec).ToLocalChecked()); + OPENSSL_free(rsa_e_dec); + Nan::Set(publicKey, + Nan::New("n").ToLocalChecked(), + Nan::New(rsa_n_hex).ToLocalChecked()); + OPENSSL_free(rsa_n_hex); + Nan::Set(publicKey, + Nan::New("bitSize").ToLocalChecked(), + Nan::New(rsa_key_length_int)); } - exports->Set(String::NewSymbol("publicKey"), publicKey); + Nan::Set(exports, Nan::New("publicKey").ToLocalChecked(), publicKey); EVP_PKEY_free(pkey); // alt names - Local altNames(Array::New()); + Local altNames(Nan::New()); STACK_OF(GENERAL_NAME) *names = NULL; int i; @@ -233,22 +339,34 @@ Handle try_parse(char *data) { GENERAL_NAME *current = sk_GENERAL_NAME_value(names, i); if (current->type == GEN_DNS) { - char *name = (char*) ASN1_STRING_data(current->d.dNSName); + char *name = NULL; +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + name = (char *)ASN1_STRING_get0_data(current->d.dNSName); +#else + name = (char *)ASN1_STRING_data(current->d.dNSName); +#endif if (ASN1_STRING_length(current->d.dNSName) != (int) strlen(name)) { - ThrowException(Exception::Error(String::New("Malformed alternative names field."))); - return scope.Close(exports); + ERR_clear_error(); + Nan::ThrowError("Malformed alternative names field."); + X509_free(cert); + BIO_free(bio); + return scope.Escape(exports); } - - altNames->Set(i, String::New(name)); + Nan::Set(altNames, i, Nan::New(name).ToLocalChecked()); } } + sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); } - exports->Set(String::NewSymbol("altNames"), altNames); + Nan::Set(exports, Nan::New("altNames").ToLocalChecked(), altNames); // Extensions - Local extensions(Object::New()); + Local extensions(Nan::New()); +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + const STACK_OF(X509_EXTENSION) *exts = X509_get0_extensions(cert); +#else STACK_OF(X509_EXTENSION) *exts = cert->cert_info->extensions; +#endif int num_of_exts; int index_of_exts; if (exts) { @@ -268,100 +386,106 @@ Handle try_parse(char *data) { BIO *ext_bio = BIO_new(BIO_s_mem()); // IFNULL_FAIL(ext_bio, "unable to allocate memory for extension value BIO"); if (!X509V3_EXT_print(ext_bio, ext, 0, 0)) { - M_ASN1_OCTET_STRING_print(ext_bio, ext->value); + unsigned char *buf = NULL; + int len = i2d_ASN1_OCTET_STRING(X509_EXTENSION_get_data(ext), &buf); + if (len >= 0) { + BIO_write(ext_bio, static_cast(buf), len); + } } BUF_MEM *bptr; BIO_get_mem_ptr(ext_bio, &bptr); - BIO_set_close(ext_bio, BIO_NOCLOSE); + BIO_set_close(ext_bio, BIO_CLOSE); + + char *data = new char[bptr->length + 1]; + BUF_strlcpy(data, bptr->data, bptr->length + 1); + char *trimmed_data = trim(data, bptr->length); - // remove newlines - int lastchar = bptr->length; - if (lastchar > 1 && (bptr->data[lastchar-1] == '\n' || bptr->data[lastchar-1] == '\r')) { - bptr->data[lastchar-1] = (char) 0; - } - if (lastchar > 0 && (bptr->data[lastchar] == '\n' || bptr->data[lastchar] == '\r')) { - bptr->data[lastchar] = (char) 0; - } BIO_free(ext_bio); unsigned nid = OBJ_obj2nid(obj); if (nid == NID_undef) { char extname[100]; OBJ_obj2txt(extname, 100, (const ASN1_OBJECT *) obj, 1); - extensions->Set(String::NewSymbol(extname), String::New(bptr->data)); + Nan::Set(extensions, + Nan::New(real_name(extname)).ToLocalChecked(), + Nan::New(trimmed_data).ToLocalChecked()); + } else { const char *c_ext_name = OBJ_nid2ln(nid); // IFNULL_FAIL(c_ext_name, "invalid X509v3 extension name"); - extensions->Set(String::NewSymbol(c_ext_name), String::New(bptr->data)); + Nan::Set(extensions, + Nan::New(real_name((char*)c_ext_name)).ToLocalChecked(), + Nan::New(trimmed_data).ToLocalChecked()); } + delete[] data; } - exports->Set(String::NewSymbol("extensions"), extensions); + Nan::Set(exports, + Nan::New("extensions").ToLocalChecked(), extensions); + ERR_clear_error(); X509_free(cert); + BIO_free(bio); -#if NODE_VERSION_AT_LEAST(0, 11, 3) && defined(__APPLE__) - free(data); -#endif - - return scope.Close(exports); + return scope.Escape(exports); } -Handle parse_serial(ASN1_INTEGER *serial) { - HandleScope scope; +Local parse_serial(ASN1_INTEGER *serial) { + Nan::EscapableHandleScope scope; Local serialNumber; BIGNUM *bn = ASN1_INTEGER_to_BN(serial, NULL); char *hex = BN_bn2hex(bn); - serialNumber = String::New(hex); + serialNumber = Nan::New(hex).ToLocalChecked(); BN_free(bn); OPENSSL_free(hex); - return scope.Close(serialNumber); + return scope.Escape(serialNumber); } -Handle parse_date(char *date) { - HandleScope scope; - char current[3]; - int i; - Local dateArray(Array::New()); - Local output(String::New("")); +Local parse_date(ASN1_TIME *date) { + Nan::EscapableHandleScope scope; + BIO *bio; + BUF_MEM *bm; + char formatted[64]; Local args[1]; - for (i = 0; i < (int) strlen(date) - 1; i += 2) { - strncpy(current, &date[i], 2); - current[2] = '\0'; - - dateArray->Set((i / 2), String::New(current)); - } - - output = String::Concat(output, String::Concat(dateArray->Get(1)->ToString(), String::New("/"))); - output = String::Concat(output, String::Concat(dateArray->Get(2)->ToString(), String::New("/"))); - output = String::Concat(output, String::Concat(String::New("20"), dateArray->Get(0)->ToString())); - output = String::Concat(output, String::New(" ")); - output = String::Concat(output, String::Concat(dateArray->Get(3)->ToString(), String::New(":"))); - output = String::Concat(output, String::Concat(dateArray->Get(4)->ToString(), String::New(":"))); - output = String::Concat(output, String::Concat(dateArray->Get(5)->ToString(), String::New(" GMT"))); - - args[0] = output; - - return scope.Close(Context::GetCurrent()->Global()->Get(String::New("Date"))->ToObject()->CallAsConstructor(1, args)); + formatted[0] = '\0'; + bio = BIO_new(BIO_s_mem()); + ASN1_TIME_print(bio, date); + BIO_get_mem_ptr(bio, &bm); + BUF_strlcpy(formatted, bm->data, bm->length + 1); + BIO_free(bio); + args[0] = Nan::New(formatted).ToLocalChecked(); + + Local global = Nan::GetCurrentContext()->Global(); + Local DateObject = Nan::Get(global, + Nan::New("Date").ToLocalChecked()).ToLocalChecked()->ToObject(); + return scope.Escape(Nan::CallAsConstructor(DateObject, 1, args).ToLocalChecked()); } -Handle parse_name(X509_NAME *subject) { - HandleScope scope; - Handle cert(Object::New()); +Local parse_name(X509_NAME *subject) { + Nan::EscapableHandleScope scope; + Local cert = Nan::New(); int i, length; ASN1_OBJECT *entry; - unsigned char *value; + const unsigned char *value; char buf[255]; length = X509_NAME_entry_count(subject); for (i = 0; i < length; i++) { entry = X509_NAME_ENTRY_get_object(X509_NAME_get_entry(subject, i)); OBJ_obj2txt(buf, 255, entry, 0); - value = ASN1_STRING_data(X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject, i))); - cert->Set(String::NewSymbol(real_name(buf)), String::New((const char*) value)); +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + value = ASN1_STRING_get0_data( + X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject, i))); +#else + value = ASN1_STRING_data( + X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject, i))); +#endif + Nan::Set(cert, + Nan::New(real_name(buf)).ToLocalChecked(), + Nan::New((const char*) value).ToLocalChecked()); } - return scope.Close(cert); + return scope.Escape(cert); } // Fix for missing fields in OpenSSL. @@ -370,8 +494,25 @@ char* real_name(char *data) { for (i = 0; i < length; i++) { if (strcmp(data, MISSING[i][0]) == 0) - return MISSING[i][1]; + return (char*) MISSING[i][1]; } return data; } + +char* trim(char *data, int len) { + if (data[0] == '\n' || data[0] == '\r') { + data = data+1; + } + else if (len > 1 && (data[len-1] == '\n' || data[len-1] == '\r')) { + data[len-1] = (char) 0; + } + else if (len > 0 && (data[len] == '\n' || data[len] == '\r')) { + data[len] = (char) 0; + } + else { + return data; + } + + return trim(data, len - 1); +} diff --git a/test/CA_chains/enduser-example.com.chain b/test/CA_chains/enduser-example.com.chain new file mode 100644 index 0000000..15c7840 --- /dev/null +++ b/test/CA_chains/enduser-example.com.chain @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAI6zVCRN44FlMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCXg1MDktdGVzdDAeFw0xNzExMjgwODQ2NDdaFw0yNzExMjYwODQ2NDdaMBQx +EjAQBgNVBAMMCXg1MDktdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMmi9qSlfyxP9Btj1WlPcfymRQg3qHi0cpM8hxlevWf87cP5SQA1QEPq17xQ +EoLUt6nEf9a8BjoE/GCPXifYD6cPb3fqxGi/IiMjuNwznlfGWNhOnqKbhJo0FZna +/wdCEP8upRmmWqx1pHgr7QQgPYHZIMiCbtG66+VV4KNgD5YjIOMABEJfalh3GOFh +STWbFEUXqLjqttFF4xfve88Xk6+xpCFCCzC5SraVCR1OL2UiUyOMMKkyWlLL2Jd3 +TsVbRTTuxLBPohajwGPxuusqaoMLvQ73WzArnfDADRHqm67rlx6EFe1BjCyzKiT7 +f354LnsZZhj6jnPOQxs6rrsqH9kCAwEAAaNQME4wHQYDVR0OBBYEFGimY7g6t0Uh +lrr3ZpBqPXpvW/T+MB8GA1UdIwQYMBaAFGimY7g6t0Uhlrr3ZpBqPXpvW/T+MAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKjz0ylupkmE/FtiDLz1x3uL +UsLo/a82HoAwUi/GPICQdp5wjI5E02GoM04HdRvRZXVXiTdPlZrSEvHHLuBo/BdK +XkEpwFFYhBNg3n+KmU7M4NNL8Uwb2BLX7432I1blQKe756T4A1O8OxKIhD7JyxGn +SAaz5PZNQIm9hw2Elt/P9/7n86aHrsFYOl4K2rUlCfuh+9KzBkjpykU50idbd+FI +V4mmvN9s8XXcYpCzQB5mfVgTSSRpvYxmrkHUODOnxQJ4jn0LNNEhq6HsB5tUhu0b +RZASyTfbHfAfflGbkONnTiEHfux3CCYM2BuJ0Mdk5gZPcCY3yRgZP1i8PGTIaOc= +-----END CERTIFICATE----- diff --git a/test/certs/enduser-example.com.crt b/test/certs/enduser-example.com.crt new file mode 100644 index 0000000..15c7840 --- /dev/null +++ b/test/certs/enduser-example.com.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+zCCAeOgAwIBAgIJAI6zVCRN44FlMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMMCXg1MDktdGVzdDAeFw0xNzExMjgwODQ2NDdaFw0yNzExMjYwODQ2NDdaMBQx +EjAQBgNVBAMMCXg1MDktdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMmi9qSlfyxP9Btj1WlPcfymRQg3qHi0cpM8hxlevWf87cP5SQA1QEPq17xQ +EoLUt6nEf9a8BjoE/GCPXifYD6cPb3fqxGi/IiMjuNwznlfGWNhOnqKbhJo0FZna +/wdCEP8upRmmWqx1pHgr7QQgPYHZIMiCbtG66+VV4KNgD5YjIOMABEJfalh3GOFh +STWbFEUXqLjqttFF4xfve88Xk6+xpCFCCzC5SraVCR1OL2UiUyOMMKkyWlLL2Jd3 +TsVbRTTuxLBPohajwGPxuusqaoMLvQ73WzArnfDADRHqm67rlx6EFe1BjCyzKiT7 +f354LnsZZhj6jnPOQxs6rrsqH9kCAwEAAaNQME4wHQYDVR0OBBYEFGimY7g6t0Uh +lrr3ZpBqPXpvW/T+MB8GA1UdIwQYMBaAFGimY7g6t0Uhlrr3ZpBqPXpvW/T+MAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKjz0ylupkmE/FtiDLz1x3uL +UsLo/a82HoAwUi/GPICQdp5wjI5E02GoM04HdRvRZXVXiTdPlZrSEvHHLuBo/BdK +XkEpwFFYhBNg3n+KmU7M4NNL8Uwb2BLX7432I1blQKe756T4A1O8OxKIhD7JyxGn +SAaz5PZNQIm9hw2Elt/P9/7n86aHrsFYOl4K2rUlCfuh+9KzBkjpykU50idbd+FI +V4mmvN9s8XXcYpCzQB5mfVgTSSRpvYxmrkHUODOnxQJ4jn0LNNEhq6HsB5tUhu0b +RZASyTfbHfAfflGbkONnTiEHfux3CCYM2BuJ0Mdk5gZPcCY3yRgZP1i8PGTIaOc= +-----END CERTIFICATE----- diff --git a/test/certs/equifax.crt b/test/certs/equifax.crt new file mode 100644 index 0000000..7a36225 --- /dev/null +++ b/test/certs/equifax.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/test.js b/test/test.js index 6096f3f..c98818d 100644 --- a/test/test.js +++ b/test/test.js @@ -1,6 +1,7 @@ var x509 = require('../index'), fs = require('fs'), - path = require('path'); + path = require('path'), + assert = require('assert'); // All cert files should read without throwing an error. // Simple enough test, no? @@ -10,3 +11,46 @@ fs.readdirSync(path.join(__dirname, 'certs')).forEach(function (file) { // x509.parseCert(path.join(__dirname, 'certs', file)); console.log(); }); + + +x509.verify( + path.join(__dirname, 'certs/enduser-example.com.crt'), + path.join(__dirname, 'CA_chains/enduser-example.com.chain'), + function (err) { + assert.strictEqual(err, null); + } +); + + + +x509.verify( + path.join(__dirname, 'certs/acaline.com.crt'), + path.join(__dirname, 'CA_chains/enduser-example.com.chain'), + function (err, result) { + assert.throws(assert.ifError.bind(null, err), /unable to get local issuer/) + } +); + +x509.verify( + path.join(__dirname, 'certs/notexisting.com.crt'), + path.join(__dirname, 'CA_chains/enduser-example.com.chain'), + function (err, result) { + assert.throws(assert.ifError.bind(null, err), /ENOENT/) + } +); + +x509.verify( + path.join(__dirname, 'certs/equifax.crt'), + path.join(__dirname, '/test.js'), + function(err, result) { + assert.throws(assert.ifError.bind(null, err), /Error loading CA chain file/) + } +); + +x509.verify( + path.join(__dirname, '/test.js'), + path.join(__dirname, 'CA_chains/enduser-example.com.chain'), + function(err, result) { + assert.throws(assert.ifError.bind(null, err), /Failed to load cert/) + } +); \ No newline at end of file