Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add a getFingerprint method, docs, and unit test.

  • Loading branch information...
commit 0a720c763527c323bf0d071e145e09f22859025d 1 parent 44c08bc
@hildjj hildjj authored
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();
+ });
+
});
}
-};
+};
Please sign in to comment.
Something went wrong with that request. Please try again.