gecko-dev/toolkit/components/certviewer/content/certDecoder.js
Cameron Porter 1c1943da7f Bug 1601035 - Check for the key identifier in an authority x509 extension before trying to use it since it is optional for some certificates. Check that there are x509 extensions before trying to get critical or unsupported extensions. r=johannh
When viewing certificates with certificateviewer that have an authority x509 extension
but do not have an authority key identifier, parsing the certificate fails.
Since the authority key identifier is optional in RFC 5280,
check if it exists before trying to set the key identifier.

Also check that x509 extensions are defined before setting up the critical and unsupported extensions.

Differential Revision: https://phabricator.services.mozilla.com/D59224

--HG--
extra : moz-landing-system : lando
2020-01-10 13:19:24 +00:00

561 lines
16 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { fromBER } = asn1js.asn1js;
const { Certificate } = pkijs.pkijs;
import {
b64urltodec,
b64urltohex,
getObjPath,
hash,
hashify,
} from "./utils.js";
import { strings } from "./strings.js";
import { ctLogNames } from "./ctlognames.js";
const getTimeZone = () => {
let timeZone = new Date().toString().match(/\(([A-Za-z\s].*)\)/);
if (timeZone === null) {
// America/Chicago
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
} else if (timeZone.length > 1) {
timeZone = timeZone[1]; // Central Daylight Time
} else {
timeZone = "Local Time"; // not sure if this is right, but let's go with it for now
}
return timeZone;
};
const getPublicKeyInfo = x509 => {
let spki = Object.assign(
{
crv: undefined,
e: undefined,
kty: undefined,
n: undefined,
keysize: undefined,
x: undefined,
xy: undefined,
y: undefined,
},
x509.subjectPublicKeyInfo
);
if (spki.kty === "RSA") {
spki.e = b64urltodec(spki.e); // exponent
spki.keysize = b64urltohex(spki.n).length * 8; // key size in bits
spki.n = hashify(b64urltohex(spki.n)); // modulus
} else if (spki.kty === "EC") {
spki.kty = "Elliptic Curve";
spki.keysize = parseInt(spki.crv.split("-")[1]); // this is a bit hacky
spki.x = hashify(b64urltohex(spki.x)); // x coordinate
spki.y = hashify(b64urltohex(spki.y)); // y coordinate
spki.xy = `04:${spki.x}:${spki.y}`; // 04 (uncompressed) public key
}
return spki;
};
const getX509Ext = (extensions, v) => {
for (var extension in extensions) {
if (extensions[extension].extnID === v) {
return extensions[extension];
}
}
return {
extnValue: undefined,
parsedValue: undefined,
};
};
const getKeyUsages = (x509, criticalExtensions) => {
let keyUsages = {
critical: criticalExtensions.includes("2.5.29.15"),
purposes: [],
};
let keyUsagesBS = getX509Ext(x509.extensions, "2.5.29.15").parsedValue;
if (keyUsagesBS !== undefined) {
// parse the bit string, shifting as necessary
let unusedBits = keyUsagesBS.valueBlock.unusedBits;
keyUsagesBS = parseInt(keyUsagesBS.valueBlock.valueHex, 16) >> unusedBits;
// iterate through the bit string
strings.keyUsages.slice(unusedBits - 1).forEach(usage => {
if (keyUsagesBS & 1) {
keyUsages.purposes.push(usage);
}
keyUsagesBS = keyUsagesBS >> 1;
});
// reverse the order for legibility
keyUsages.purposes.reverse();
}
return keyUsages;
};
const parseSubsidiary = distinguishedNames => {
const subsidiary = {
cn: "",
dn: [],
entries: [],
};
distinguishedNames.forEach(dn => {
const name = strings.names[dn.type];
const value = dn.value.valueBlock.value;
if (name === undefined) {
subsidiary.dn.push(`OID.${dn.type}=${value}`);
subsidiary.entries.push([`OID.${dn.type}`, value]);
} else if (name.short === undefined) {
subsidiary.dn.push(`OID.${dn.type}=${value}`);
subsidiary.entries.push([name.long, value]);
} else {
subsidiary.dn.push(`${name.short}=${value}`);
subsidiary.entries.push([name.long, value]);
// add the common name for tab display
if (name.short === "cn") {
subsidiary.cn = value;
}
}
});
// turn path into a string
subsidiary.dn = subsidiary.dn.join(", ");
return subsidiary;
};
const getSubjectAltNames = (x509, criticalExtensions) => {
let san = getX509Ext(x509.extensions, "2.5.29.17").parsedValue;
if (san && san.hasOwnProperty("altNames")) {
san = Object.keys(san.altNames).map(x => {
const type = san.altNames[x].type;
switch (type) {
case 4: // directory
return [
strings.san[type],
parseSubsidiary(san.altNames[x].value.typesAndValues).dn,
];
case 7: // ip address
let address = san.altNames[x].value.valueBlock.valueHex;
if (address.length === 8) {
// ipv4
return [
strings.san[type],
address
.match(/.{1,2}/g)
.map(x => parseInt(x, 16))
.join("."),
];
} else if (address.length === 32) {
// ipv6
return [
strings.san[type],
address
.toLowerCase()
.match(/.{1,4}/g)
.join(":")
.replace(/\b:?(?:0+:?){2,}/, "::"),
];
}
return [strings.san[type], "Unknown IP address"];
default:
return [strings.san[type], san.altNames[x].value];
}
});
} else {
san = [];
}
san = {
altNames: san,
critical: criticalExtensions.includes("2.5.29.17"),
};
return san;
};
const getBasicConstraints = (x509, criticalExtensions) => {
let basicConstraints;
const basicConstraintsExt = getX509Ext(x509.extensions, "2.5.29.19");
if (basicConstraintsExt && basicConstraintsExt.parsedValue) {
basicConstraints = {
cA:
basicConstraintsExt.parsedValue.cA !== undefined &&
basicConstraintsExt.parsedValue.cA,
critical: criticalExtensions.includes("2.5.29.19"),
};
}
return basicConstraints;
};
const getEKeyUsages = (x509, criticalExtensions) => {
let eKeyUsages = getX509Ext(x509.extensions, "2.5.29.37").parsedValue;
if (eKeyUsages) {
eKeyUsages = {
critical: criticalExtensions.includes("2.5.29.37"),
purposes: eKeyUsages.keyPurposes.map(x => strings.eKU[x] || x),
};
}
return eKeyUsages;
};
const getSubjectKeyID = (x509, criticalExtensions) => {
let sKID = getX509Ext(x509.extensions, "2.5.29.14").parsedValue;
if (sKID) {
sKID = {
critical: criticalExtensions.includes("2.5.29.14"),
id: hashify(sKID.valueBlock.valueHex),
};
}
return sKID;
};
const getAuthorityKeyID = (x509, criticalExtensions) => {
let aKID = getX509Ext(x509.extensions, "2.5.29.35").parsedValue;
if (!aKID || !aKID.keyIdentifier) {
return null;
}
aKID = {
critical: criticalExtensions.includes("2.5.29.35"),
id: hashify(aKID.keyIdentifier.valueBlock.valueHex),
};
return aKID;
};
const getCRLPoints = (x509, criticalExtensions) => {
let crlPoints = getX509Ext(x509.extensions, "2.5.29.31").parsedValue;
if (crlPoints) {
crlPoints = {
critical: criticalExtensions.includes("2.5.29.31"),
points: crlPoints.distributionPoints.map(
x => x.distributionPoint[0].value
),
};
}
return crlPoints;
};
const getOcspStaple = (x509, criticalExtensions) => {
let ocspStaple = getX509Ext(x509.extensions, "1.3.6.1.5.5.7.1.24").extnValue;
if (ocspStaple && ocspStaple.valueBlock.valueHex === "3003020105") {
ocspStaple = {
critical: criticalExtensions.includes("1.3.6.1.5.5.7.1.24"),
required: true,
};
} else {
ocspStaple = {
critical: criticalExtensions.includes("1.3.6.1.5.5.7.1.24"),
required: false,
};
}
return ocspStaple;
};
const getAuthorityInfoAccess = (x509, criticalExtensions) => {
let aia = getX509Ext(x509.extensions, "1.3.6.1.5.5.7.1.1").parsedValue;
if (aia) {
aia = aia.accessDescriptions.map(x => {
return {
location: x.accessLocation.value,
method: strings.aia[x.accessMethod],
};
});
}
aia = {
descriptions: aia,
critical: criticalExtensions.includes("1.3.6.1.5.5.7.1.1"),
};
return aia;
};
const getSCTs = (x509, criticalExtensions) => {
let scts = getX509Ext(x509.extensions, "1.3.6.1.4.1.11129.2.4.2").parsedValue;
if (scts) {
scts = Object.keys(scts.timestamps).map(x => {
let logId = scts.timestamps[x].logID.toLowerCase();
let sctsTimestamp = scts.timestamps[x].timestamp;
return {
logId: hashify(logId),
name: ctLogNames.hasOwnProperty(logId) ? ctLogNames[logId] : undefined,
signatureAlgorithm: `${scts.timestamps[x].hashAlgorithm.replace(
"sha",
"SHA-"
)} ${scts.timestamps[x].signatureAlgorithm.toUpperCase()}`,
timestamp: `${sctsTimestamp.toLocaleString()} (${getTimeZone()})`,
timestampUTC: sctsTimestamp.toUTCString(),
version: scts.timestamps[x].version + 1,
};
});
} else {
scts = [];
}
scts = {
critical: criticalExtensions.includes("1.3.6.1.4.1.11129.2.4.2"),
timestamps: scts,
};
return scts;
};
const getCertificatePolicies = (x509, criticalExtensions) => {
let cp = getX509Ext(x509.extensions, "2.5.29.32").parsedValue;
if (cp && cp.hasOwnProperty("certificatePolicies")) {
cp = cp.certificatePolicies.map(x => {
let id = x.policyIdentifier;
let name = strings.cps.hasOwnProperty(id)
? strings.cps[id].name
: undefined;
let qualifiers = undefined;
let value = strings.cps.hasOwnProperty(id)
? strings.cps[id].value
: undefined;
// ansi organization identifiers
if (id.startsWith("2.16.840.")) {
value = id;
id = "2.16.840";
name = strings.cps["2.16.840"].name;
}
// statement identifiers
if (id.startsWith("1.3.6.1.4.1")) {
value = id;
id = "1.3.6.1.4.1";
name = strings.cps["1.3.6.1.4.1"].name;
}
if (x.hasOwnProperty("policyQualifiers")) {
qualifiers = x.policyQualifiers.map(qualifier => {
let id = qualifier.policyQualifierId;
let name = strings.cps.hasOwnProperty(id)
? strings.cps[id].name
: undefined;
let value = qualifier.qualifier.valueBlock.value;
// sometimes they are multiple qualifier subblocks, and for now we'll
// only return the first one because it's getting really messy at this point
if (Array.isArray(value) && value.length === 1) {
value = value[0].valueBlock.value;
} else if (Array.isArray(value) && value.length > 1) {
value = "(currently unsupported)";
}
return {
id,
name,
value,
};
});
}
return {
id,
name,
qualifiers,
value,
};
});
}
cp = {
critical: criticalExtensions.includes("2.5.29.32"),
policies: cp,
};
return cp;
};
const getMicrosoftCryptographicExtensions = (x509, criticalExtensions) => {
// now let's parse the Microsoft cryptographic extensions
let msCrypto = {
caVersion: getX509Ext(x509.extensions, "1.3.6.1.4.1.311.21.1").parsedValue,
certificatePolicies: getX509Ext(x509.extensions, "1.3.6.1.4.1.311.21.10")
.parsedValue,
certificateTemplate: getX509Ext(x509.extensions, "1.3.6.1.4.1.311.21.7")
.parsedValue,
certificateType: getX509Ext(x509.extensions, "1.3.6.1.4.1.311.20.2")
.parsedValue,
previousHash: getX509Ext(x509.extensions, "1.3.6.1.4.1.311.21.2")
.parsedValue,
};
if (
msCrypto.caVersion &&
Number.isInteger(msCrypto.caVersion.keyIndex) &&
Number.isInteger(msCrypto.caVersion.certificateIndex)
) {
msCrypto.caVersion = {
critical: criticalExtensions.includes("1.3.6.1.4.1.311.21.1"),
caRenewals: msCrypto.caVersion.certificateIndex,
keyReuses:
msCrypto.caVersion.certificateIndex - msCrypto.caVersion.keyIndex,
};
}
if (msCrypto.certificatePolicies) {
msCrypto.certificatePolicies = {
critical: criticalExtensions.includes("1.3.6.1.4.1.311.21.10"),
purposes: msCrypto.certificatePolicies.certificatePolicies.map(
x => strings.eKU[x.policyIdentifier] || x.policyIdentifier
),
};
}
if (msCrypto.certificateTemplate) {
msCrypto.certificateTemplate = {
critical: criticalExtensions.includes("1.3.6.1.4.1.311.21.7"),
id: msCrypto.certificateTemplate.extnID,
major: msCrypto.certificateTemplate.templateMajorVersion,
minor: msCrypto.certificateTemplate.templateMinorVersion,
};
}
if (msCrypto.certificateType) {
msCrypto.certificateType = {
critical: criticalExtensions.includes("1.3.6.1.4.1.311.20.2"),
type:
strings.microsoftCertificateTypes[
msCrypto.certificateType.valueBlock.value
] || "Unknown",
};
}
if (msCrypto.previousHash) {
msCrypto.previousHash = {
critical: criticalExtensions.includes("1.3.6.1.4.1.311.21.2"),
previousHash: hashify(msCrypto.previousHash.valueBlock.valueHex),
};
}
msCrypto.exists = !!(
msCrypto.caVersion ||
msCrypto.certificatePolicies ||
msCrypto.certificateTemplate ||
msCrypto.certificateType ||
msCrypto.previousHash
);
return msCrypto;
};
export const parse = async certificate => {
// certificate could be an array of BER or an array of buffers
const supportedExtensions = [
"1.3.6.1.4.1.311.20.2", // microsoft certificate type
"1.3.6.1.4.1.311.21.2", // microsoft certificate previous hash
"1.3.6.1.4.1.311.21.7", // microsoft certificate template
"1.3.6.1.4.1.311.21.1", // microsoft certification authority renewal
"1.3.6.1.4.1.311.21.10", // microsoft certificate policies
"1.3.6.1.4.1.11129.2.4.2", // embedded scts
"1.3.6.1.5.5.7.1.1", // authority info access
"1.3.6.1.5.5.7.1.24", // ocsp stapling
"1.3.101.77", // ct redaction - deprecated and not displayed
"2.5.29.14", // subject key identifier
"2.5.29.15", // key usages
"2.5.29.17", // subject alt names
"2.5.29.19", // basic constraints
"2.5.29.31", // crl points
"2.5.29.32", // certificate policies
"2.5.29.35", // authority key identifier
"2.5.29.37", // extended key usage
];
let timeZone = getTimeZone();
// parse the certificate
const asn1 = fromBER(certificate);
let x509 = new Certificate({ schema: asn1.result });
x509 = x509.toJSON();
// convert the cert to PEM
const certBTOA = window
.btoa(String.fromCharCode.apply(null, new Uint8Array(certificate)))
.match(/.{1,64}/g)
.join("\r\n");
// get which extensions are critical
const criticalExtensions = [];
if (x509.extensions) {
x509.extensions.forEach(ext => {
if (ext.hasOwnProperty("critical") && ext.critical === true) {
criticalExtensions.push(ext.extnID);
}
});
}
const spki = getPublicKeyInfo(x509);
const keyUsages = getKeyUsages(x509, criticalExtensions);
const san = getSubjectAltNames(x509, criticalExtensions);
const basicConstraints = getBasicConstraints(x509, criticalExtensions);
const eKeyUsages = getEKeyUsages(x509, criticalExtensions);
const sKID = getSubjectKeyID(x509, criticalExtensions);
const aKID = getAuthorityKeyID(x509, criticalExtensions);
const crlPoints = getCRLPoints(x509, criticalExtensions);
const ocspStaple = getOcspStaple(x509, criticalExtensions);
const aia = getAuthorityInfoAccess(x509, criticalExtensions);
const scts = getSCTs(x509, criticalExtensions);
const cp = getCertificatePolicies(x509, criticalExtensions);
const msCrypto = getMicrosoftCryptographicExtensions(
x509,
criticalExtensions
);
// determine which extensions weren't supported
let unsupportedExtensions = [];
if (x509.extensions) {
x509.extensions.forEach(ext => {
if (!supportedExtensions.includes(ext.extnID)) {
unsupportedExtensions.push(ext.extnID);
}
});
}
// the output shell
return {
ext: {
aia,
aKID,
basicConstraints,
crlPoints,
cp,
eKeyUsages,
keyUsages,
msCrypto,
ocspStaple,
scts,
sKID,
san,
},
files: {
der: undefined, // TODO: implement!
pem: encodeURI(
`-----BEGIN CERTIFICATE-----\r\n${certBTOA}\r\n-----END CERTIFICATE-----\r\n`
),
},
fingerprint: {
sha1: await hash("SHA-1", certificate),
sha256: await hash("SHA-256", certificate),
},
issuer: parseSubsidiary(x509.issuer.typesAndValues),
notBefore: `${x509.notBefore.value.toLocaleString()} (${timeZone})`,
notBeforeUTC: x509.notBefore.value.toUTCString(),
notAfter: `${x509.notAfter.value.toLocaleString()} (${timeZone})`,
notAfterUTC: x509.notAfter.value.toUTCString(),
subject: parseSubsidiary(x509.subject.typesAndValues),
serialNumber: hashify(getObjPath(x509, "serialNumber.valueBlock.valueHex")),
signature: {
name: strings.signature[getObjPath(x509, "signature.algorithmId")],
type: getObjPath(x509, "signature.algorithmId"),
},
subjectPublicKeyInfo: spki,
unsupportedExtensions,
version: (x509.version + 1).toString(),
};
};