fune/toolkit/components/certviewer/content/certDecoder.js
Carolina dc61c90a77 Bug 1559225 - Build a certificate chain. r=johannh,keeler
Added third party libraries using browserify, builds a certificate chain using some functions defined in https://github.com/april/certainly-something and using a dummy certificate chain. r=johannh

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

--HG--
extra : moz-landing-system : lando
2019-07-12 12:17:04 +00:00

555 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 "chrome://global/content/certviewer/utils.js";
import { strings } from "chrome://global/content/certviewer/strings.js";
import { ctLogNames } from "chrome://global/content/certviewer/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 = {
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();
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: `${scts.timestamps[
x
].timestamp.toLocaleString()} (${getTimeZone()})`,
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 = [];
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 = [];
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})`,
notAfter: `${x509.notAfter.value.toLocaleString()} (${timeZone})`,
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(),
};
};