Skip to content

Commit

Permalink
fix!: handle poorly formatted PEM files (#85)
Browse files Browse the repository at this point in the history
* fix!: handle poorly formatted PEM files

Added seamless handling of poorly formatted PEM files for signing and encryption
  • Loading branch information
david-renaud-okta authored Apr 28, 2022
1 parent 925c39a commit 8830a23
Show file tree
Hide file tree
Showing 8 changed files with 529 additions and 66 deletions.
50 changes: 34 additions & 16 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
var fs = require('fs');
var Parser = require('@xmldom/xmldom').DOMParser;
const fs = require('fs');
const Parser = require('@xmldom/xmldom').DOMParser;

exports.pemToCert = function(pem) {
var cert = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(pem.toString());
const cert = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(pem.toString());
if (cert && cert.length > 0) {
return cert[1].replace(/[\n|\r\n]/g, '');
}
Expand All @@ -29,28 +29,27 @@ exports.reportError = function(err, callback){
* @api private
*/
exports.uid = function(len) {
var buf = []
, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
, charlen = chars.length;
const buf = []
, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
, charlen = chars.length;

for (var i = 0; i < len; ++i) {
for (let i = 0; i < len; ++i) {
buf.push(chars[getRandomInt(0, charlen - 1)]);
}

return buf.join('');
};

exports.removeWhitespace = function(xml) {
var trimmed = xml
.replace(/\r\n/g, '')
.replace(/\n/g,'')
.replace(/>(\s*)</g, '><') //unindent
.trim();
return trimmed;
return xml
.replace(/\r\n/g, '')
.replace(/\n/g, '')
.replace(/>(\s*)</g, '><') //unindent
.trim();
};

/**
* Retrun a random int, used by `utils.uid()`
* Return a random int, used by `utils.uid()`
*
* @param {Number} min
* @param {Number} max
Expand All @@ -69,10 +68,29 @@ function getRandomInt(min, max) {
* @return {function(): Node}
*/
exports.factoryForNode = function factoryForNode(pathToTemplate) {
var template = fs.readFileSync(pathToTemplate)
var prototypeDoc = new Parser().parseFromString(template.toString())
const template = fs.readFileSync(pathToTemplate);
const prototypeDoc = new Parser().parseFromString(template.toString());

return function () {
return prototypeDoc.cloneNode(true);
};
};

/**
* Standardizes PEM content to match the spec (best effort)
*
* @param pem {Buffer} The PEM content to standardize
* @returns {Buffer} The standardized PEM. Original will be returned unmodified if the content is not PEM.
*/
exports.fixPemFormatting = function (pem) {
let pemEntries = pem.toString().matchAll(/([-]{5}[^-\r\n]+[-]{5})([^-]*)([-]{5}[^-\r\n]+[-]{5})/g);
let fixedPem = ''
for (const pemParts of pemEntries) {
fixedPem = fixedPem.concat(`${pemParts[1]}\n${pemParts[2].replaceAll(/[\r\n]/g, '')}\n${pemParts[3]}\n`)
}
if (fixedPem.length === 0) {
return pem;
}

return Buffer.from(fixedPem)
}
28 changes: 23 additions & 5 deletions lib/xml/encrypt.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
var xmlenc = require('xml-encryption');
const xmlenc = require('xml-encryption');

var utils = require('../utils');
const utils = require('../utils');

exports.fromEncryptXmlOptions = function (options) {
if (!options.encryptionCert) {
return this.unencrypted;
} else {
var encryptOptions = {
const encryptOptions = {
rsa_pub: options.encryptionPublicKey,
pem: options.encryptionCert,
encryptionAlgorithm: options.encryptionAlgorithm || 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
Expand All @@ -29,8 +29,26 @@ exports.unencrypted = function (xml, callback) {
exports.encrypted = function (encryptOptions) {
return function encrypt(xml, callback) {
xmlenc.encrypt(xml, encryptOptions, function (err, encrypted) {
if (err) return callback(err);
callback(null, utils.removeWhitespace(encrypted));
if (err) {
// Attempt to fix errors and retry
xmlenc.encrypt(
xml,
{
...encryptOptions,
rsa_pub: utils.fixPemFormatting(encryptOptions.rsa_pub),
pem: utils.fixPemFormatting(encryptOptions.pem),
},
function (retryErr, retryEncrypted) {
if (retryErr) {
return callback(retryErr);
}

callback(null, utils.removeWhitespace(retryEncrypted));
}
);
} else {
callback(null, utils.removeWhitespace(encrypted));
}
});
};
};
89 changes: 54 additions & 35 deletions lib/xml/sign.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
var utils = require('../utils');
var SignedXml = require('xml-crypto').SignedXml;
const utils = require('../utils');
const SignedXml = require('xml-crypto').SignedXml;

var algorithms = {
const algorithms = {
signature: {
'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
},
digest: {
'sha256': 'http://www.w3.org/2001/04/xmlenc#sha256',
Expand All @@ -22,61 +22,80 @@ exports.fromSignXmlOptions = function (options) {
if (!options.xpathToNodeBeforeSignature)
throw new Error('xpathToNodeBeforeSignature is required')

var key = options.key;
var pem = options.cert;
var signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
var digestAlgorithm = options.digestAlgorithm || 'sha256';
var signatureNamespacePrefix = (function (prefix) {
const key = options.key;
const pem = options.cert;
const signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
const digestAlgorithm = options.digestAlgorithm || 'sha256';
const signatureNamespacePrefix = (function (prefix) {
// 0.10.1 added prefix, but we want to name it signatureNamespacePrefix - This is just to keep supporting prefix
return typeof prefix === 'string' ? prefix : '';
})(options.signatureNamespacePrefix || options.prefix);
var xpathToNodeBeforeSignature = options.xpathToNodeBeforeSignature;
var idAttribute = options.signatureIdAttribute;
const xpathToNodeBeforeSignature = options.xpathToNodeBeforeSignature;
const idAttribute = options.signatureIdAttribute;

/**
* @param {Document} doc
* @param {Function} [callback]
* @return {string}
*/
return function signXmlDocument(doc, callback) {
var unsigned = exports.unsigned(doc);
var cert = utils.pemToCert(pem);
function sign(key) {
const unsigned = exports.unsigned(doc);
const cert = utils.pemToCert(pem);

var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[signatureAlgorithm], idAttribute: idAttribute });
sig.addReference("//*[local-name(.)='Assertion']",
["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"],
algorithms.digest[digestAlgorithm]);
const sig = new SignedXml(null, {
signatureAlgorithm: algorithms.signature[signatureAlgorithm],
idAttribute: idAttribute
});
sig.addReference("//*[local-name(.)='Assertion']",
["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"],
algorithms.digest[digestAlgorithm]);

sig.signingKey = key;
sig.signingKey = key;

sig.keyInfoProvider = {
getKeyInfo: function (key, prefix) {
prefix = prefix ? prefix + ':' : prefix;
return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + "</" + prefix + "X509Certificate></" + prefix + "X509Data>";
}
};
sig.keyInfoProvider = {
getKeyInfo: function (key, prefix) {
prefix = prefix ? prefix + ':' : prefix;
return "<" + prefix + "X509Data><" + prefix + "X509Certificate>" + cert + "</" + prefix + "X509Certificate></" + prefix + "X509Data>";
}
};

sig.computeSignature(unsigned, {
location: {reference: xpathToNodeBeforeSignature, action: 'after'},
prefix: signatureNamespacePrefix
});

return sig.getSignedXml();
}

sig.computeSignature(unsigned, {
location: { reference: xpathToNodeBeforeSignature, action: 'after' },
prefix: signatureNamespacePrefix
});
let signed
try {
try {
signed = sign(key)
} catch (err) {
signed = sign(utils.fixPemFormatting(key))
}

var signed = sig.getSignedXml();
if (callback) {
setImmediate(callback, null, signed);
} else {
return signed;
if (callback) {
setImmediate(callback, null, signed);
} else {
return signed;
}
} catch (e) {
if (callback) {
setImmediate(callback, e)
}
throw e
}
};
};

/**
* @param {Document} doc
* @param {Function} [callback]
* @return {string}
*/
exports.unsigned = function (doc, callback) {
var xml = utils.removeWhitespace(doc.toString());
const xml = utils.removeWhitespace(doc.toString());
if (callback) {
setImmediate(callback, null, xml)
} else {
Expand Down
64 changes: 64 additions & 0 deletions test/saml11.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@ describe('saml 1.1', function () {
assertSignature(signedAssertion, options);
});

it('should not error when cert is missing newlines', function () {
// cert created with:
// openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/CN=auth0.auth0.com/O=Auth0 LLC/C=US/ST=Washington/L=Redmond' -keyout auth0.key -out auth0.pem

var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key')
};

var signedAssertion = saml11[createAssertion]({...options, cert: Buffer.from(options.cert.toString().replaceAll(/[\r\n]/g, ''))});
assertSignature(signedAssertion, options);
});

it('should not error when key is missing newlines', function () {
// cert created with:
// openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/CN=auth0.auth0.com/O=Auth0 LLC/C=US/ST=Washington/L=Redmond' -keyout auth0.key -out auth0.pem

var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key')
};

var signedAssertion = saml11[createAssertion]({...options, key: Buffer.from(options.key.toString().replaceAll(/[\r\n]/g, ''))});
assertSignature(signedAssertion, options);
});

it('should support specifying Issuer property', function () {
var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
Expand Down Expand Up @@ -350,6 +376,44 @@ describe('saml 1.1', function () {
});
});

it('should not error when encryptionPublicKey is missing newlines', function (done) {
var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key'),
encryptionPublicKey: Buffer.from(fs.readFileSync(__dirname + '/test-auth0_rsa.pub').toString().replaceAll(/[\r\n]/g, '')),
encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem')
};

saml11[createAssertion](options, function(err, encrypted) {
if (err) return done(err);

xmlenc.decrypt(encrypted, { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) {
if (err) return done(err);
assertSignature(decrypted, options);
done();
});
});
});

it('should not error when encryptionCert is missing newlines', function (done) {
var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key'),
encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'),
encryptionCert: Buffer.from(fs.readFileSync(__dirname + '/test-auth0.pem').toString().replaceAll(/[\r\n]/g, ''))
};

saml11[createAssertion](options, function(err, encrypted) {
if (err) return done(err);

xmlenc.decrypt(encrypted, { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) {
if (err) return done(err);
assertSignature(decrypted, options);
done();
});
});
});

it('should support holder-of-key suject confirmationmethod', function (done) {
var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
Expand Down
Loading

0 comments on commit 8830a23

Please sign in to comment.