mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 00:20:08 +01:00
When subject and verifier are represented as strings, escape special characters (such as '+') to guarantee unambiguity. Previously, different distinguished names could result in the same string when encoded. In particular, inserting a '+' in a single-value Relative Distinguished Name (e.g., L or OU) would produce a string that is indistinguishable from a multi-value Relative Distinguished Name. Third-party code that correctly interprets the generated string representation as a multi-value Relative Distinguished Name could then be vulnerable to an injection attack, e.g., when an attacker includes a single-value RDN with type OU and value 'HR + CN=example.com', the string representation produced by unpatched versions of Node.js would be 'OU=HR + CN=example.com', which represents a multi-value RDN. Node.js itself is not vulnerable to this attack because the current implementation that parses such strings into objects does not handle '+' at all. This oversight leads to incorrect results, but at the same time appears to prevent injection attacks (as described above). With this change, the JavaScript objects representing the subject and issuer Relative Distinguished Names are constructed in C++ directly, instead of (incorrectly) encoding them as strings and then (incorrectly) decoding the strings in JavaScript. This addresses CVE-2021-44533. CVE-ID: CVE-2021-44533 PR-URL: https://github.com/nodejs-private/node-private/pull/300 Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
644 lines
14 KiB
JavaScript
644 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
const asn1 = require('asn1.js');
|
|
const crypto = require('crypto');
|
|
const { writeFileSync } = require('fs');
|
|
const rfc5280 = require('asn1.js-rfc5280');
|
|
const BN = asn1.bignum;
|
|
|
|
const oid = {
|
|
commonName: [2, 5, 4, 3],
|
|
countryName: [2, 5, 4, 6],
|
|
localityName: [2, 5, 4, 7],
|
|
rsaEncryption: [1, 2, 840, 113549, 1, 1, 1],
|
|
sha256WithRSAEncryption: [1, 2, 840, 113549, 1, 1, 11],
|
|
xmppAddr: [1, 3, 6, 1, 5, 5, 7, 8, 5],
|
|
srvName: [1, 3, 6, 1, 5, 5, 7, 8, 7],
|
|
ocsp: [1, 3, 6, 1, 5, 5, 7, 48, 1],
|
|
caIssuers: [1, 3, 6, 1, 5, 5, 7, 48, 2],
|
|
privateUnrecognized: [1, 3, 9999, 12, 34]
|
|
};
|
|
|
|
const digest = 'SHA256';
|
|
|
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
|
modulusLength: 4096,
|
|
publicKeyEncoding: {
|
|
type: 'pkcs1',
|
|
format: 'der'
|
|
}
|
|
});
|
|
|
|
writeFileSync('server-key.pem', privateKey.export({
|
|
type: 'pkcs8',
|
|
format: 'pem'
|
|
}));
|
|
|
|
const now = Date.now();
|
|
const days = 3650;
|
|
|
|
function utilType(name, fn) {
|
|
return asn1.define(name, function() {
|
|
this[fn]();
|
|
});
|
|
}
|
|
|
|
const Null_ = utilType('Null_', 'null_');
|
|
const null_ = Null_.encode('der');
|
|
|
|
const IA5String = utilType('IA5String', 'ia5str');
|
|
const PrintableString = utilType('PrintableString', 'printstr');
|
|
const UTF8String = utilType('UTF8String', 'utf8str');
|
|
|
|
const subjectCommonName = PrintableString.encode('evil.example.com', 'der');
|
|
|
|
const sans = [
|
|
{ type: 'dNSName', value: 'good.example.com, DNS:evil.example.com' },
|
|
{ type: 'uniformResourceIdentifier', value: 'http://example.com/' },
|
|
{ type: 'uniformResourceIdentifier', value: 'http://example.com/?a=b&c=d' },
|
|
{ type: 'uniformResourceIdentifier', value: 'http://example.com/a,b' },
|
|
{ type: 'uniformResourceIdentifier', value: 'http://example.com/a%2Cb' },
|
|
{
|
|
type: 'uniformResourceIdentifier',
|
|
value: 'http://example.com/a, DNS:good.example.com'
|
|
},
|
|
{ type: 'dNSName', value: Buffer.from('exämple.com', 'latin1') },
|
|
{ type: 'dNSName', value: '"evil.example.com"' },
|
|
{ type: 'iPAddress', value: Buffer.from('08080808', 'hex') },
|
|
{ type: 'iPAddress', value: Buffer.from('08080404', 'hex') },
|
|
{ type: 'iPAddress', value: Buffer.from('0008080404', 'hex') },
|
|
{ type: 'iPAddress', value: Buffer.from('000102030405', 'hex') },
|
|
{
|
|
type: 'iPAddress',
|
|
value: Buffer.from('0a0b0c0d0e0f0000000000007a7b7c7d', 'hex')
|
|
},
|
|
{ type: 'rfc822Name', value: 'foo@example.com' },
|
|
{ type: 'rfc822Name', value: 'foo@example.com, DNS:good.example.com' },
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Hannover', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('München', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Berlin, DNS:good.example.com', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Berlin, DNS:good.example.com\0evil.example.com', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode(
|
|
'Berlin, DNS:good.example.com\\\0evil.example.com', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Berlin\r\n', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'directoryName',
|
|
value: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{
|
|
type: oid.countryName,
|
|
value: PrintableString.encode('DE', 'der')
|
|
}
|
|
],
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Berlin/CN=good.example.com', 'der')
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
{
|
|
type: 'registeredID',
|
|
value: oid.sha256WithRSAEncryption
|
|
},
|
|
{
|
|
type: 'registeredID',
|
|
value: oid.privateUnrecognized
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.xmppAddr,
|
|
value: UTF8String.encode('abc123', 'der')
|
|
}
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.xmppAddr,
|
|
value: UTF8String.encode('abc123, DNS:good.example.com', 'der')
|
|
}
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.xmppAddr,
|
|
value: UTF8String.encode('good.example.com\0abc123', 'der')
|
|
}
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.privateUnrecognized,
|
|
value: UTF8String.encode('abc123', 'der')
|
|
}
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.srvName,
|
|
value: IA5String.encode('abc123', 'der')
|
|
}
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.srvName,
|
|
value: UTF8String.encode('abc123', 'der')
|
|
}
|
|
},
|
|
{
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.srvName,
|
|
value: IA5String.encode('abc\0def', 'der')
|
|
}
|
|
}
|
|
];
|
|
|
|
for (let i = 0; i < sans.length; i++) {
|
|
const san = sans[i];
|
|
|
|
const tbs = {
|
|
version: 'v3',
|
|
serialNumber: new BN('01', 16),
|
|
signature: {
|
|
algorithm: oid.sha256WithRSAEncryption,
|
|
parameters: null_
|
|
},
|
|
issuer: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{ type: oid.commonName, value: subjectCommonName }
|
|
]
|
|
]
|
|
},
|
|
validity: {
|
|
notBefore: { type: 'utcTime', value: now },
|
|
notAfter: { type: 'utcTime', value: now + days * 86400000 }
|
|
},
|
|
subject: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{ type: oid.commonName, value: subjectCommonName }
|
|
]
|
|
]
|
|
},
|
|
subjectPublicKeyInfo: {
|
|
algorithm: {
|
|
algorithm: oid.rsaEncryption,
|
|
parameters: null_
|
|
},
|
|
subjectPublicKey: {
|
|
unused: 0,
|
|
data: publicKey
|
|
}
|
|
},
|
|
extensions: [
|
|
{
|
|
extnID: 'subjectAlternativeName',
|
|
critical: false,
|
|
extnValue: [san]
|
|
}
|
|
]
|
|
};
|
|
|
|
// Self-sign the certificate.
|
|
const tbsDer = rfc5280.TBSCertificate.encode(tbs, 'der');
|
|
const signature = crypto.createSign(digest).update(tbsDer).sign(privateKey);
|
|
|
|
// Construct the signed certificate.
|
|
const cert = {
|
|
tbsCertificate: tbs,
|
|
signatureAlgorithm: {
|
|
algorithm: oid.sha256WithRSAEncryption,
|
|
parameters: null_
|
|
},
|
|
signature: {
|
|
unused: 0,
|
|
data: signature
|
|
}
|
|
};
|
|
|
|
// Store the signed certificate.
|
|
const pem = rfc5280.Certificate.encode(cert, 'pem', {
|
|
label: 'CERTIFICATE'
|
|
});
|
|
writeFileSync(`./alt-${i}-cert.pem`, `${pem}\n`);
|
|
}
|
|
|
|
const infoAccessExtensions = [
|
|
[
|
|
{
|
|
accessMethod: oid.ocsp,
|
|
accessLocation: {
|
|
type: 'uniformResourceIdentifier',
|
|
value: 'http://good.example.com/\nOCSP - URI:http://evil.example.com/',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
accessMethod: oid.caIssuers,
|
|
accessLocation: {
|
|
type: 'uniformResourceIdentifier',
|
|
value: 'http://ca.example.com/\nOCSP - URI:http://evil.example.com',
|
|
},
|
|
},
|
|
{
|
|
accessMethod: oid.ocsp,
|
|
accessLocation: {
|
|
type: 'dNSName',
|
|
value: 'good.example.com\nOCSP - URI:http://ca.nodejs.org/ca.cert',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
accessMethod: oid.privateUnrecognized,
|
|
accessLocation: {
|
|
type: 'uniformResourceIdentifier',
|
|
value: 'http://ca.example.com/',
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
accessMethod: oid.ocsp,
|
|
accessLocation: {
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.xmppAddr,
|
|
value: UTF8String.encode('good.example.com', 'der'),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
accessMethod: oid.ocsp,
|
|
accessLocation: {
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.privateUnrecognized,
|
|
value: UTF8String.encode('abc123', 'der')
|
|
},
|
|
},
|
|
},
|
|
{
|
|
accessMethod: oid.ocsp,
|
|
accessLocation: {
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.srvName,
|
|
value: IA5String.encode('abc123', 'der')
|
|
}
|
|
}
|
|
},
|
|
],
|
|
[
|
|
{
|
|
accessMethod: oid.ocsp,
|
|
accessLocation: {
|
|
type: 'otherName',
|
|
value: {
|
|
'type-id': oid.xmppAddr,
|
|
value: UTF8String.encode('good.example.com\0abc123', 'der'),
|
|
},
|
|
},
|
|
},
|
|
],
|
|
];
|
|
|
|
for (let i = 0; i < infoAccessExtensions.length; i++) {
|
|
const infoAccess = infoAccessExtensions[i];
|
|
|
|
const tbs = {
|
|
version: 'v3',
|
|
serialNumber: new BN('01', 16),
|
|
signature: {
|
|
algorithm: oid.sha256WithRSAEncryption,
|
|
parameters: null_
|
|
},
|
|
issuer: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{ type: oid.commonName, value: subjectCommonName }
|
|
]
|
|
]
|
|
},
|
|
validity: {
|
|
notBefore: { type: 'utcTime', value: now },
|
|
notAfter: { type: 'utcTime', value: now + days * 86400000 }
|
|
},
|
|
subject: {
|
|
type: 'rdnSequence',
|
|
value: [
|
|
[
|
|
{ type: oid.commonName, value: subjectCommonName }
|
|
]
|
|
]
|
|
},
|
|
subjectPublicKeyInfo: {
|
|
algorithm: {
|
|
algorithm: oid.rsaEncryption,
|
|
parameters: null_
|
|
},
|
|
subjectPublicKey: {
|
|
unused: 0,
|
|
data: publicKey
|
|
}
|
|
},
|
|
extensions: [
|
|
{
|
|
extnID: 'authorityInformationAccess',
|
|
critical: false,
|
|
extnValue: infoAccess
|
|
}
|
|
]
|
|
};
|
|
|
|
// Self-sign the certificate.
|
|
const tbsDer = rfc5280.TBSCertificate.encode(tbs, 'der');
|
|
const signature = crypto.createSign(digest).update(tbsDer).sign(privateKey);
|
|
|
|
// Construct the signed certificate.
|
|
const cert = {
|
|
tbsCertificate: tbs,
|
|
signatureAlgorithm: {
|
|
algorithm: oid.sha256WithRSAEncryption,
|
|
parameters: null_
|
|
},
|
|
signature: {
|
|
unused: 0,
|
|
data: signature
|
|
}
|
|
};
|
|
|
|
// Store the signed certificate.
|
|
const pem = rfc5280.Certificate.encode(cert, 'pem', {
|
|
label: 'CERTIFICATE'
|
|
});
|
|
writeFileSync(`./info-${i}-cert.pem`, `${pem}\n`);
|
|
}
|
|
|
|
const subjects = [
|
|
[
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('Somewhere') }
|
|
],
|
|
[
|
|
{ type: oid.commonName, value: UTF8String.encode('evil.example.com') }
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Somewhere\0evil.example.com'),
|
|
}
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Somewhere\nCN=evil.example.com')
|
|
}
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Somewhere, CN = evil.example.com')
|
|
}
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Somewhere/CN=evil.example.com')
|
|
}
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('M\u00fcnchen\\\nCN=evil.example.com')
|
|
}
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('Somewhere') },
|
|
{ type: oid.commonName, value: UTF8String.encode('evil.example.com') },
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{
|
|
type: oid.localityName,
|
|
value: UTF8String.encode('Somewhere + CN=evil.example.com'),
|
|
}
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('L1') },
|
|
{ type: oid.localityName, value: UTF8String.encode('L2') },
|
|
],
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('L3') },
|
|
]
|
|
],
|
|
[
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('L1') },
|
|
],
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('L2') },
|
|
],
|
|
[
|
|
{ type: oid.localityName, value: UTF8String.encode('L3') },
|
|
],
|
|
],
|
|
];
|
|
|
|
for (let i = 0; i < subjects.length; i++) {
|
|
const tbs = {
|
|
version: 'v3',
|
|
serialNumber: new BN('01', 16),
|
|
signature: {
|
|
algorithm: oid.sha256WithRSAEncryption,
|
|
parameters: null_
|
|
},
|
|
issuer: {
|
|
type: 'rdnSequence',
|
|
value: subjects[i]
|
|
},
|
|
validity: {
|
|
notBefore: { type: 'utcTime', value: now },
|
|
notAfter: { type: 'utcTime', value: now + days * 86400000 }
|
|
},
|
|
subject: {
|
|
type: 'rdnSequence',
|
|
value: subjects[i]
|
|
},
|
|
subjectPublicKeyInfo: {
|
|
algorithm: {
|
|
algorithm: oid.rsaEncryption,
|
|
parameters: null_
|
|
},
|
|
subjectPublicKey: {
|
|
unused: 0,
|
|
data: publicKey
|
|
}
|
|
}
|
|
};
|
|
|
|
// Self-sign the certificate.
|
|
const tbsDer = rfc5280.TBSCertificate.encode(tbs, 'der');
|
|
const signature = crypto.createSign(digest).update(tbsDer).sign(privateKey);
|
|
|
|
// Construct the signed certificate.
|
|
const cert = {
|
|
tbsCertificate: tbs,
|
|
signatureAlgorithm: {
|
|
algorithm: oid.sha256WithRSAEncryption,
|
|
parameters: null_
|
|
},
|
|
signature: {
|
|
unused: 0,
|
|
data: signature
|
|
}
|
|
};
|
|
|
|
// Store the signed certificate.
|
|
const pem = rfc5280.Certificate.encode(cert, 'pem', {
|
|
label: 'CERTIFICATE'
|
|
});
|
|
writeFileSync(`./subj-${i}-cert.pem`, `${pem}\n`);
|
|
}
|