Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add getFingerprint, fix intermittent failure #1

Merged
merged 1 commit into from

2 participants

@hildjj

Added a getFingerprint(cert, callback) method. As well, fixed an issue where calls to openssl would sometimes fail on the latest node, since stdout may close after the process exit event.

Sorry about the spurious whitespace changes. If you're really bothered by it (my editor did s/\s+$//), I can tweak the patch.

@andris9 andris9 merged commit 3cd272a into andris9:master

1 check failed

Details default The Travis build failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 16, 2012
  1. @hildjj
This page is out of date. Refresh to see the latest.
Showing with 215 additions and 124 deletions.
  1. +11 −0 README.md
  2. +132 −70 lib/pem.js
  3. +72 −54 test/pem.js
View
11 README.md
@@ -93,6 +93,17 @@ Where
* **certificate** is a PEM encoded CSR or a certificate
* **callback** is a callback function with an error object and `{country, state, locality, organization, organizationUnit, commonName, emailAddress}`
+### Get fingerprint
+
+Use `getFingerprint` to get the SHA1 fingerprint for a certificate
+
+ pem.getFingerprint(certificate, callback)
+
+Where
+
+ * **certificate** is a PEM encoded certificate
+ * **callback** is a callback function with an error object and `{fingerprint}`
+
## License
**MIT**
View
202 lib/pem.js
@@ -5,12 +5,13 @@ module.exports.createCSR =createCSR;
module.exports.createCertificate = createCertificate;
module.exports.readCertificateInfo = readCertificateInfo;
module.exports.getPublicKey = getPublicKey;
+module.exports.getFingerprint = getFingerprint;
// PUBLIC API
/**
* Creates a private key
- *
+ *
* @param {Number} [keyBitsize=1024] Size of the key, defaults to 1024bit
* @param {Function} callback Callback function with an error object and {key}
*/
@@ -19,7 +20,7 @@ function createPrivateKey(keyBitsize, callback){
callback = keyBitsize;
keyBitsize = undefined;
}
-
+
keyBitsize = Number(keyBitsize) || 1024;
var params = ["genrsa",
@@ -27,7 +28,7 @@ function createPrivateKey(keyBitsize, callback){
"/var/log/mail:/var/log/messages",
keyBitsize
];
-
+
execOpenSSL(params, "RSA PRIVATE KEY", function(error, key){
if(error){
return callback(error);
@@ -38,10 +39,10 @@ function createPrivateKey(keyBitsize, callback){
/**
* Creates a Certificate Signing Request
- *
+ *
* If client key is undefined, a new key is created automatically. The used key is included
* in the callback return as clientKey
- *
+ *
* @param {Object} [options] Optional options object
* @param {String} [options.clientKey] Optional client key to use
* @param {Number} [options.keyBitsize] If clientKey is undefined, bit size to use for generating a new key (defaults to 1024)
@@ -60,9 +61,9 @@ function createCSR(options, callback){
callback = options;
options = undefined;
}
-
+
options = options || {};
-
+
if(!options.clientKey){
createPrivateKey(options.keyBitsize || 1024, function(error, keyData){
if(error){
@@ -73,7 +74,7 @@ function createCSR(options, callback){
});
return;
}
-
+
var params = ["req",
"-new",
"-" + (options.hash || "sha1"),
@@ -82,7 +83,7 @@ function createCSR(options, callback){
"-key",
"/dev/stdin"
];
-
+
execOpenSSL(params, "CERTIFICATE REQUEST", options.clientKey, function(error, data){
if(error){
return callback(error);
@@ -92,7 +93,7 @@ function createCSR(options, callback){
clientKey: options.clientKey
};
return callback(null, response);
-
+
});
}
@@ -100,7 +101,7 @@ function createCSR(options, callback){
* Creates a certificate based on a CSR. If CSR is not defined, a new one
* will be generated automatically. For CSR generation all the options values
* can be used as with createCSR.
- *
+ *
* @param {Object} [options] Optional options object
* @param {String} [options.serviceKey] Private key for signing the certificate, if not defined a new one is generated
* @param {Boolean} [options.selfSigned] If set to true and serviceKey is not defined, use clientKey for signing
@@ -113,9 +114,9 @@ function createCertificate(options, callback){
callback = options;
options = undefined;
}
-
+
options = options || {};
-
+
if(!options.csr){
createCSR(options, function(error, keyData){
if(error){
@@ -127,9 +128,9 @@ function createCertificate(options, callback){
});
return;
}
-
+
if(!options.serviceKey){
-
+
if(options.selfSigned){
options.serviceKey = options.clientKey;
}else{
@@ -143,18 +144,34 @@ function createCertificate(options, callback){
return;
}
}
-
+
var params = ["x509",
"-req",
"-days",
Number(options.days) || "365",
"-in",
- "/dev/stdin",
- "-signkey",
"/dev/stdin"
];
+ var stdin = [options.csr];
+ if (options.serviceCertificate) {
+ if (!options.serial) {
+ return callback(new Error("serial option required for CA signing"));
+ }
+ params.push("-CA");
+ params.push("/dev/stdin");
+ params.push("-CAkey");
+ params.push("/dev/stdin");
+ params.push("-set_serial");
+ params.push("0x" + ("00000000" + options.serial.toString(16)).slice(-8));
+ stdin.push(options.serviceCertificate)
+ stdin.push(options.serviceKey)
+ } else {
+ params.push("-signkey");
+ params.push("/dev/stdin");
+ stdin.push(options.serviceKey)
+ }
- execOpenSSL(params, "CERTIFICATE", [options.csr, options.serviceKey], function(error, data){
+ execOpenSSL(params, "CERTIFICATE", stdin, function(error, data){
if(error){
return callback(error);
}
@@ -170,7 +187,7 @@ function createCertificate(options, callback){
/**
* Exports a public key from a private key, CSR or certificate
- *
+ *
* @param {String} certificate PEM encoded private key, CSR or certificate
* @param {Function} callback Callback function with an error object and {publicKey}
*/
@@ -179,11 +196,11 @@ function getPublicKey(certificate, callback){
callback = certificate;
certificate = undefined;
}
-
+
certificate = (certificate || "").toString();
-
+
var params;
-
+
if(certificate.match(/BEGIN CERTIFICATE REQUEST/)){
params = ["req",
"-in",
@@ -213,7 +230,7 @@ function getPublicKey(certificate, callback){
/**
* Reads subject data from a certificate or a CSR
- *
+ *
* @param {String} certificate PEM encoded CSR or certificate
* @param {Function} callback Callback function with an error object and {country, state, locality, organization, organizationUnit, commonName, emailAddress}
*/
@@ -222,40 +239,47 @@ function readCertificateInfo(certificate, callback){
callback = certificate;
certificate = undefined;
}
-
+
certificate = (certificate || "").toString();
-
+
var type = certificate.match(/BEGIN CERTIFICATE REQUEST/)?"req":"x509",
params = [type,
"-noout",
"-text",
"-in",
"/dev/stdin"
- ],
- openssl = spawn("openssl", params),
- stdout = "",
- stderr = "";
-
- openssl.stdin.write(certificate);
- openssl.stdin.end();
-
- openssl.stdout.on('data', function (data) {
- stdout += (data || "").toString("binary");
+ ];
+ spawnOpenSSL(params, certificate, function(err, code, stdout, stderr){
+ if (err) {
+ return callback(err);
+ }
+ return fetchCertificateData(stdout, callback);
});
+}
- openssl.stderr.on('data', function (data) {
- stderr += (data || "").toString("binary");
- });
+/**
+ * Gets the fingerprint for a certificate
+ *
+ * @param {String} PEM encoded certificate
+ * @param {Function} callback Callback function with an error object and {fingerprint}
+ */
+function getFingerprint(certificate, callback){
+ var params = ["x509",
+ "-in",
+ "/dev/stdin",
+ "-fingerprint",
+ "-noout"];
-
- openssl.on('exit', function (code) {
- if(code){
- return callback(new Error("Invalid openssl exit code "+code));
+ spawnOpenSSL(params, certificate, function(err, code, stdout, stderr){
+ if (err) {
+ return callback(err);
+ }
+ var match = stdout.match(/Fingerprint=([0-9a-fA-F:]+)$/m);
+ if (match){
+ return callback(null, {fingerprint: match[1]});
+ } else {
+ return callback(new Error("No fingerprint"));
}
- stdout = new Buffer(stdout, "binary").toString("utf-8");
- stderr = new Buffer(stderr, "binary").toString("utf-8");
-
- return fetchCertificateData(stdout, callback);
});
}
@@ -263,9 +287,9 @@ function readCertificateInfo(certificate, callback){
function fetchCertificateData(certData, callback){
certData = (certData || "").toString();
-
+
var subject, extra, tmp, certValues = {};
-
+
if((subject = certData.match(/Subject:([^\n]*)\n/)) && subject.length>1){
subject = subject[1];
extra = subject.split("/");
@@ -300,7 +324,7 @@ function fetchCertificateData(certData, callback){
function generateCSRSubject(options){
options = options || {};
-
+
var csrData = {
C: options.country || options.C || "",
ST: options.state || options.ST || "",
@@ -311,26 +335,30 @@ function generateCSRSubject(options){
emailAddress: options.emailAddress || ""
},
csrBuilder = [];
-
+
Object.keys(csrData).forEach(function(key){
if(csrData[key]){
csrBuilder.push("/" + key + "=" + csrData[key].replace(/[^\w \.\-@]+/g, " ").trim());
}
});
-
+
return csrBuilder.join("");
}
/**
- * Spawn an openssl command
+ * Generically spawn openSSL, without processing the result
+ *
+ * @param {Array} params The parameters to pass to openssl
+ * @param {String|Array} stdin Stuff to pass to stdin
+ * @param {Function} callback Called with (error, exitCode, stdout, stderr)
*/
-function execOpenSSL(params, searchStr, stdin, callback){
+function spawnOpenSSL(params, stdin, callback) {
var openssl = spawn("openssl", params),
stdout = "",
stderr = "",
pushToStdin = function(){
var data = stdin.shift();
-
+
if(data){
openssl.stdin.write(data + "\n");
if(!stdin.length){
@@ -338,12 +366,12 @@ function execOpenSSL(params, searchStr, stdin, callback){
}
}
};
-
+
if(!callback && typeof stdin == "function"){
callback = stdin;
stdin = false;
}
-
+
if(stdin){
if(Array.isArray(stdin)){
pushToStdin();
@@ -352,7 +380,7 @@ function execOpenSSL(params, searchStr, stdin, callback){
openssl.stdin.end();
}
}
-
+
openssl.stdout.on('data', function (data) {
stdout += (data || "").toString("binary");
});
@@ -364,33 +392,67 @@ function execOpenSSL(params, searchStr, stdin, callback){
}
});
- openssl.on('exit', function (code) {
- var start, end;
-
+ // We need both the return code and access to all of stdout. Stdout isn't
+ // *really* available until the close event fires; the timing nuance was
+ // making this fail periodically.
+ var needed = 2; // wait for both exit and close.
+ var code = -1;
+ var bothDone = function() {
+ if (code) {
+ callback(new Error("Invalid openssl exit code: " + code + "\n% openssl " + params.join(" ") + "\n" + stderr), code);
+ } else {
+ callback(null, code, stdout, stderr);
+ }
+ };
+
+ openssl.on('exit', function (ret) {
+ code = ret;
+ if (--needed < 1) {
+ bothDone();
+ }
+ });
+
+ openssl.on('close', function () {
stdout = new Buffer(stdout, "binary").toString("utf-8");
stderr = new Buffer(stderr, "binary").toString("utf-8");
-
- if(code){
- return callback(new Error("Invalid openssl exit code "+code+"\nSTDOUT:\n"+stdout+"\nSTDERR:\n"+stderr));
+
+ if (--needed < 1) {
+ bothDone();
}
-
+ });
+}
+
+/**
+ * Spawn an openssl command
+ */
+function execOpenSSL(params, searchStr, stdin, callback){
+ if(!callback && typeof stdin == "function"){
+ callback = stdin;
+ stdin = false;
+ }
+
+ spawnOpenSSL(params, stdin, function(err, code, stdout, stderr) {
+ var start, end;
+ if (err) {
+ return callback(err);
+ }
+
if((start = stdout.match(new RegExp("\\-+BEGIN "+searchStr+"\\-+$", "m")))){
start = start.index;
}else{
start = -1;
}
-
+
if((end = stdout.match(new RegExp("^\\-+END "+searchStr+"\\-+", "m")))){
end = end.index + (end[0] || "").length;
}else{
end = -1;
}
-
+
if(start >= 0 && end >=0){
return callback(null, stdout.substring(start, end));
}else{
- return callback(new Error(searchStr + " not found from openssl output"));
+ return callback(new Error(searchStr + " not found from openssl output:\n---stdout---\n" + stdout + "\n---stderr---\n" + stderr + "\ncode: " + code + "\nsignal: " + signal));
}
});
-
-}
+}
View
126 test/pem.js
@@ -14,7 +14,7 @@ exports["General Tests"] = {
test.done();
});
},
-
+
"Create 2048bit Private key": function(test){
pem.createPrivateKey(2048, function(error, data){
var key = (data && data.key || "").toString();
@@ -26,7 +26,7 @@ exports["General Tests"] = {
test.done();
});
},
-
+
"Create default CSR": function(test){
pem.createCSR(function(error, data){
var csr = (data && data.csr || "").toString();
@@ -34,34 +34,34 @@ exports["General Tests"] = {
test.ok(csr);
test.ok(csr.match(/^\n*\-\-\-\-\-BEGIN CERTIFICATE REQUEST\-\-\-\-\-\n/));
test.ok(csr.match(/\n\-\-\-\-\-END CERTIFICATE REQUEST\-\-\-\-\-\n*$/));
-
+
test.ok(data && data.clientKey);
-
+
test.done();
});
},
-
+
"Create CSR with own key": function(test){
pem.createPrivateKey(function(error, data){
var key = (data && data.key || "").toString();
-
+
pem.createCSR({clientKey: key}, function(error, data){
var csr = (data && data.csr || "").toString();
test.ifError(error);
test.ok(csr);
test.ok(csr.match(/^\n*\-\-\-\-\-BEGIN CERTIFICATE REQUEST\-\-\-\-\-\n/));
test.ok(csr.match(/\n\-\-\-\-\-END CERTIFICATE REQUEST\-\-\-\-\-\n*$/));
-
+
test.equal(data && data.clientKey, key);
-
+
test.ok(data && data.clientKey);
-
+
test.done();
});
-
+
});
},
-
+
"Create default certificate": function(test){
pem.createCertificate(function(error, data){
var certificate = (data && data.certificate || "").toString();
@@ -69,17 +69,17 @@ exports["General Tests"] = {
test.ok(certificate);
test.ok(certificate.match(/^\n*\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-\n/));
test.ok(certificate.match(/\n\-\-\-\-\-END CERTIFICATE\-\-\-\-\-\n*$/));
-
+
test.ok((data && data.clientKey) != (data && data.serviceKey));
-
+
test.ok(data && data.clientKey);
test.ok(data && data.serviceKey);
test.ok(data && data.csr);
-
+
test.done();
});
},
-
+
"Create self signed certificate": function(test){
pem.createCertificate({selfSigned: true}, function(error, data){
var certificate = (data && data.certificate || "").toString();
@@ -87,22 +87,22 @@ exports["General Tests"] = {
test.ok(certificate);
test.ok(certificate.match(/^\n*\-\-\-\-\-BEGIN CERTIFICATE\-\-\-\-\-\n/));
test.ok(certificate.match(/\n\-\-\-\-\-END CERTIFICATE\-\-\-\-\-\n*$/));
-
+
test.ok((data && data.clientKey) == (data && data.serviceKey));
-
+
test.ok(data && data.clientKey);
test.ok(data && data.serviceKey);
test.ok(data && data.csr);
-
+
test.done();
});
},
-
+
"Read default cert data from CSR": function(test){
pem.createCSR(function(error, data){
var csr = (data && data.csr || "").toString();
test.ifError(error);
-
+
pem.readCertificateInfo(csr, function(error, data){
test.ifError(error);
test.deepEqual(data,{
@@ -117,19 +117,19 @@ exports["General Tests"] = {
});
});
},
-
+
"Read edited cert data from CSR": function(test){
- var certInfo = {country:"EE",
- state:"Harjumaa",
- locality:"Tallinn",
- organization:"Node.ee",
- organizationUnit:"test",
- commonName:"www.node.ee",
+ var certInfo = {country:"EE",
+ state:"Harjumaa",
+ locality:"Tallinn",
+ organization:"Node.ee",
+ organizationUnit:"test",
+ commonName:"www.node.ee",
emailAddress:"andris@node.ee"};
pem.createCSR(Object.create(certInfo), function(error, data){
var csr = (data && data.csr || "").toString();
test.ifError(error);
-
+
pem.readCertificateInfo(csr, function(error, data){
test.ifError(error);
test.deepEqual(data, certInfo);
@@ -137,12 +137,12 @@ exports["General Tests"] = {
});
});
},
-
+
"Read default cert data from certificate": function(test){
pem.createCertificate(function(error, data){
var certificate = (data && data.certificate || "").toString();
test.ifError(error);
-
+
pem.readCertificateInfo(certificate, function(error, data){
test.ifError(error);
test.deepEqual(data,{
@@ -157,19 +157,19 @@ exports["General Tests"] = {
});
});
},
-
+
"Read edited cert data from certificate": function(test){
- var certInfo = {country:"EE",
- state:"Harjumaa",
- locality:"Tallinn",
- organization:"Node.ee",
- organizationUnit:"test",
- commonName:"www.node.ee",
+ var certInfo = {country:"EE",
+ state:"Harjumaa",
+ locality:"Tallinn",
+ organization:"Node.ee",
+ organizationUnit:"test",
+ commonName:"www.node.ee",
emailAddress:"andris@node.ee"};
pem.createCertificate(Object.create(certInfo), function(error, data){
var certificate = (data && data.certificate || "").toString();
test.ifError(error);
-
+
pem.readCertificateInfo(certificate, function(error, data){
test.ifError(error);
test.deepEqual(data, certInfo);
@@ -177,64 +177,82 @@ exports["General Tests"] = {
});
});
},
-
+
"Get public key from private key": function(test){
pem.createPrivateKey(function(error, data){
var key = (data && data.key || "").toString();
test.ifError(error);
test.ok(key);
-
+
pem.getPublicKey(key, function(error, data){
var pubkey = (data && data.publicKey || "").toString();
test.ifError(error);
test.ok(pubkey);
-
+
test.ok(pubkey.match(/^\n*\-\-\-\-\-BEGIN PUBLIC KEY\-\-\-\-\-\n/));
test.ok(pubkey.match(/\n\-\-\-\-\-END PUBLIC KEY\-\-\-\-\-\n*$/));
-
+
test.done();
});
-
+
});
},
-
+
"Get public key from CSR": function(test){
pem.createCSR(function(error, data){
var key = (data && data.clientKey || "").toString();
test.ifError(error);
test.ok(key);
-
+
pem.getPublicKey(key, function(error, data){
var pubkey = (data && data.publicKey || "").toString();
test.ifError(error);
test.ok(pubkey);
-
+
test.ok(pubkey.match(/^\n*\-\-\-\-\-BEGIN PUBLIC KEY\-\-\-\-\-\n/));
test.ok(pubkey.match(/\n\-\-\-\-\-END PUBLIC KEY\-\-\-\-\-\n*$/));
-
+
test.done();
});
-
+
});
},
-
+
"Get public key from certificate": function(test){
pem.createCertificate(function(error, data){
var key = (data && data.clientKey || "").toString();
test.ifError(error);
test.ok(key);
-
+
pem.getPublicKey(key, function(error, data){
var pubkey = (data && data.publicKey || "").toString();
test.ifError(error);
test.ok(pubkey);
-
+
test.ok(pubkey.match(/^\n*\-\-\-\-\-BEGIN PUBLIC KEY\-\-\-\-\-\n/));
test.ok(pubkey.match(/\n\-\-\-\-\-END PUBLIC KEY\-\-\-\-\-\n*$/));
-
+
test.done();
});
-
+
+ });
+ },
+
+ "Get fingerprint from certificate": function(test){
+ pem.createCertificate(function(error, data){
+ var certificate = (data && data.certificate || "").toString();
+ test.ifError(error);
+ test.ok(certificate);
+
+ pem.getFingerprint(certificate, function(error, data){
+ var fingerprint = (data && data.fingerprint || "").toString();
+ test.ifError(error);
+ test.ok(fingerprint);
+ test.ok(fingerprint.match(/^[0-9A-F]{2}(:[0-9A-F]{2}){19}$/));
+
+ test.done();
+ });
+
});
}
-};
+};
Something went wrong with that request. Please try again.