Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pkcs12): support different MAC Algorithms for PKCS12 generation #1062

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 66 additions & 40 deletions lib/pkcs12.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,14 @@ var certBagValidator = {
}]
};

var algoToOidMap = {
sha1: pki.oids.sha1,
sha256: pki.oids.sha256,
sha384: pki.oids.sha384,
sha512: pki.oids.sha512,
md5: pki.oids.md5,
};

/**
* Search SafeContents structure for bags with matching attributes.
*
Expand Down Expand Up @@ -304,6 +312,27 @@ function _getBagsByAttribute(safeContents, attrName, attrValue, bagType) {
return result;
}

/**
* Converts a PKCS#12 PFX in ASN.1 notation and returns the captured data
*
* If invalid, then it throws error
*
* @param obj The PKCS#12 PFX in ASN.1 notation.
* @returns capture captured data as specified in pfxValidator
*/
p12.validatePfx = function(obj) {
var capture = {};
var errors = [];
if(!asn1.validate(obj, pfxValidator, capture, errors)) {
var error = new Error('Cannot read PKCS#12 PFX. ' +
'ASN.1 object is not an PKCS#12 PFX.');
error.errors = errors;
throw error;
}

return capture;
};

/**
* Converts a PKCS#12 PFX in ASN.1 notation into a PFX object.
*
Expand All @@ -323,14 +352,7 @@ p12.pkcs12FromAsn1 = function(obj, strict, password) {
}

// validate PFX and capture data
var capture = {};
var errors = [];
if(!asn1.validate(obj, pfxValidator, capture, errors)) {
var error = new Error('Cannot read PKCS#12 PFX. ' +
'ASN.1 object is not an PKCS#12 PFX.');
error.errors = error;
throw error;
}
var capture = p12.validatePfx(obj);

var pfx = {
version: capture.version.charCodeAt(0),
Expand Down Expand Up @@ -433,40 +455,34 @@ p12.pkcs12FromAsn1 = function(obj, strict, password) {
// check for MAC
if(capture.mac) {
var md = null;
var macKeyBytes = 0;
var macAlgorithm = asn1.derToOid(capture.macAlgorithm);
switch(macAlgorithm) {
case pki.oids.sha1:
md = forge.md.sha1.create();
macKeyBytes = 20;
break;
case pki.oids.sha256:
md = forge.md.sha256.create();
macKeyBytes = 32;
break;
case pki.oids.sha384:
md = forge.md.sha384.create();
macKeyBytes = 48;
break;
case pki.oids.sha512:
md = forge.md.sha512.create();
macKeyBytes = 64;
break;
case pki.oids.md5:
md = forge.md.md5.create();
macKeyBytes = 16;
break;
var macAlgorithmOid = asn1.derToOid(capture.macAlgorithm);
switch(macAlgorithmOid) {
case pki.oids.sha1:
md = forge.md.sha1.create();
break;
case pki.oids.sha256:
md = forge.md.sha256.create();
break;
case pki.oids.sha384:
md = forge.md.sha384.create();
break;
case pki.oids.sha512:
md = forge.md.sha512.create();
break;
case pki.oids.md5:
md = forge.md.md5.create();
break;
}
if(md === null) {
throw new Error('PKCS#12 uses unsupported MAC algorithm: ' + macAlgorithm);
throw new Error('PKCS#12 uses unsupported MAC algorithm: ' + macAlgorithmOid);
}

// verify MAC (iterations default to 1)
var macSalt = new forge.util.ByteBuffer(capture.macSalt);
var macIterations = (('macIterations' in capture) ?
parseInt(forge.util.bytesToHex(capture.macIterations), 16) : 1);
var macKey = p12.generateKey(
password, macSalt, 3, macIterations, macKeyBytes, md);
password, macSalt, 3, macIterations, md.digestLength, md);
var mac = forge.hmac.create();
mac.start(md, macKey);
mac.update(data.value);
Expand Down Expand Up @@ -799,6 +815,15 @@ p12.toPkcs12Asn1 = function(key, cert, password, options) {
options.saltSize = options.saltSize || 8;
options.count = options.count || 2048;
options.algorithm = options.algorithm || options.encAlgorithm || 'aes128';

// process macAlgorithm option
options.macAlgorithm = options.macAlgorithm || 'sha1';
var macAlgorithm = forge.md.algorithms[options.macAlgorithm] || '';
var macAlgoirthmOid = algoToOidMap[options.macAlgorithm] || '';
if(!macAlgorithm || !macAlgoirthmOid) {
throw new Error('PKCS#12 unsupported MAC algorithm: ' + options.macAlgorithm);
}

if(!('useMac' in options)) {
options.useMac = true;
}
Expand All @@ -820,9 +845,10 @@ p12.toPkcs12Asn1 = function(key, cert, password, options) {
if(typeof pairedCert === 'string') {
pairedCert = pki.certificateFromPem(pairedCert);
}
var sha1 = forge.md.sha1.create();
sha1.update(asn1.toDer(pki.certificateToAsn1(pairedCert)).getBytes());
localKeyId = sha1.digest().getBytes();

var md = macAlgorithm.create();
md.update(asn1.toDer(pki.certificateToAsn1(pairedCert)).getBytes());
localKeyId = md.digest().getBytes();
} else {
// FIXME: consider using SHA-1 of public key (which can be generated
// from private key components), see: cert.generateSubjectKeyIdentifier
Expand Down Expand Up @@ -1000,14 +1026,14 @@ p12.toPkcs12Asn1 = function(key, cert, password, options) {
var macData;
if(options.useMac) {
// MacData
var sha1 = forge.md.sha1.create();
var md = macAlgorithm.create();
var macSalt = new forge.util.ByteBuffer(
forge.random.getBytes(options.saltSize));
var count = options.count;
// 160-bit key
var key = p12.generateKey(password, macSalt, 3, count, 20);
var key = p12.generateKey(password, macSalt, 3, count, md.digestLength, md);
var mac = forge.hmac.create();
mac.start(sha1, key);
mac.start(md, key);
mac.update(asn1.toDer(safe).getBytes());
var macValue = mac.getMac();
macData = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
Expand All @@ -1017,7 +1043,7 @@ p12.toPkcs12Asn1 = function(key, cert, password, options) {
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
// algorithm = SHA-1
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false,
asn1.oidToDer(pki.oids.sha1).getBytes()),
asn1.oidToDer(macAlgoirthmOid).getBytes()),
// parameters = Null
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '')
]),
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/pkcs12.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,65 @@ var UTIL = require('../../lib/util');
(function() {
var _data;
describe('pkcs12', function() {
it('should allow macAlgorithm option', function() {
var algoOptions = {
sha1: PKI.oids.sha1,
sha256: PKI.oids.sha256,
sha384: PKI.oids.sha384,
sha512: PKI.oids.sha512,
md5: PKI.oids.md5,
};
var privateKey = PKI.privateKeyFromPem(_data.privateKey);

for(var algoName in algoOptions) {
var algoOid = algoOptions[algoName];

var algorithm = forge.md.algorithms[algoName] || null;
ASSERT.notEqual(algorithm, null);

var md = algorithm.create();

// generate pkcs12
var options = {
macAlgorithm: md.algorithm,
saltSize: 20,
count: 10000,
};

var p12Asn = PKCS12.toPkcs12Asn1(privateKey, _data.certificate, _data.nopass, options);

// validate and capture different parts of pkcs12
var capture = PKCS12.validatePfx(p12Asn);
ASSERT.equal(capture.version.charCodeAt(0), 3);
ASSERT.equal(ASN1.derToOid(capture.contentType), PKI.oids.data);

// verify mac algorithm related parameters
var macAlgorithmOid = ASN1.derToOid(capture.macAlgorithm);
var macSalt = new forge.util.ByteBuffer(capture.macSalt);
var macIterations = (('macIterations' in capture) ?
parseInt(forge.util.bytesToHex(capture.macIterations), 16) : 1);
ASSERT.equal(macAlgorithmOid, algoOid);
ASSERT.equal(macSalt.length(), options.saltSize);
ASSERT.equal(macIterations, options.count);

// verify mac digest
var data = capture.content.value[0];
ASSERT.equal(data.tagClass, ASN1.Class.UNIVERSAL);
ASSERT.equal(data.type, ASN1.Type.OCTETSTRING);
var macKey = PKCS12.generateKey(_data.nopass, macSalt, 3, macIterations, md.digestLength, md);
var mac = forge.hmac.create();
mac.start(md, macKey);
mac.update(data.value);
var macValue = mac.getMac();
ASSERT.equal(macValue.getBytes(), capture.macDigest);

// finally verify that PFX file can be parsed successfully
var p12 = PKCS12.pkcs12FromAsn1(p12Asn, true, _data.nopass);
ASSERT.equal(p12.version, 3);
ASSERT.equal(p12.safeContents.length, 2);
}
});

it('should create certificate-only p12', function() {
var p12Asn = PKCS12.toPkcs12Asn1(null, _data.certificate, null, {
useMac: false,
Expand Down Expand Up @@ -290,6 +349,7 @@ var UTIL = require('../../lib/util');
});

_data = {
nopass: 'nopass',
certificate: '-----BEGIN CERTIFICATE-----\r\n' +
'MIIDtDCCApwCCQDUVBxA2DXi8zANBgkqhkiG9w0BAQUFADCBmzELMAkGA1UEBhMC\r\n' +
'REUxEjAQBgNVBAgMCUZyYW5jb25pYTEQMA4GA1UEBwwHQW5zYmFjaDEVMBMGA1UE\r\n' +
Expand Down