forked from mirrors/gecko-dev
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
555 lines
16 KiB
JavaScript
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(),
|
|
};
|
|
};
|