@reclaimprotocol/tls
Version:
TLS 1.2/1.3 for any JavaScript Environment
189 lines (188 loc) • 7.07 kB
JavaScript
import './additional-root-cas.js';
import { crypto } from "../crypto/index.js";
import { SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_SIGNATURE_ALGS, SUPPORTED_SIGNATURE_ALGS_MAP } from "./constants.js";
import { getHash } from "./decryption-utils.js";
import { areUint8ArraysEqual, asciiToUint8Array, concatenateUint8Arrays } from "./generics.js";
import { MOZILLA_ROOT_CA_LIST } from "./mozilla-root-cas.js";
import { expectReadWithLength, packWithLength } from "./packets.js";
import { defaultFetchCertificateBytes, loadX509FromDer, loadX509FromPem } from "./x509.js";
const CERT_VERIFY_TXT = asciiToUint8Array('TLS 1.3, server CertificateVerify');
let ROOT_CAS;
export function parseCertificates(data, { version }) {
// context, kina irrelevant
const ctx = version === 'TLS1_3' ? read(1)[0] : 0;
// the data itself
data = readWLength(3);
const certificates = [];
while (data.length) {
// the certificate data
const cert = readWLength(3);
const certObj = loadX509FromDer(cert);
certificates.push(certObj);
if (version === 'TLS1_3') {
// extensions
readWLength(2);
}
}
return { certificates, ctx };
function read(bytes) {
const result = data.slice(0, bytes);
data = data.slice(bytes);
return result;
}
function readWLength(bytesLength = 2) {
const content = expectReadWithLength(data, bytesLength);
data = data.slice(content.length + bytesLength);
return content;
}
}
export function parseServerCertificateVerify(data) {
// data = readWLength(2)
const algorithmBytes = read(2);
const algorithm = SUPPORTED_SIGNATURE_ALGS.find(alg => (areUint8ArraysEqual(SUPPORTED_SIGNATURE_ALGS_MAP[alg]
.identifier, algorithmBytes)));
if (!algorithm) {
throw new Error(`Unsupported signature algorithm '${algorithmBytes}'`);
}
const signature = readWLength(2);
return { algorithm, signature };
function read(bytes) {
const result = data.slice(0, bytes);
data = data.slice(bytes);
return result;
}
function readWLength(bytesLength = 2) {
const content = expectReadWithLength(data, bytesLength);
data = data.slice(content.length + bytesLength);
return content;
}
}
export async function verifyCertificateSignature({ signature, algorithm, publicKey, signatureData, }) {
const { algorithm: cryptoAlg } = SUPPORTED_SIGNATURE_ALGS_MAP[algorithm];
const pubKey = await crypto.importKey(cryptoAlg, publicKey.buffer, 'public');
const verified = await crypto.verify(cryptoAlg, {
data: signatureData,
signature,
publicKey: pubKey
});
if (!verified) {
throw new Error(`${algorithm} signature verification failed`);
}
}
export async function getSignatureDataTls13(hellos, cipherSuite) {
const handshakeHash = await getHash(hellos, cipherSuite);
return concatenateUint8Arrays([
new Uint8Array(64).fill(0x20),
CERT_VERIFY_TXT,
new Uint8Array([0]),
handshakeHash
]);
}
export async function getSignatureDataTls12({ clientRandom, serverRandom, curveType, publicKey, }) {
const publicKeyBytes = await crypto.exportKey(publicKey);
return concatenateUint8Arrays([
clientRandom,
serverRandom,
concatenateUint8Arrays([
new Uint8Array([3]),
SUPPORTED_NAMED_CURVE_MAP[curveType].identifier,
]),
packWithLength(publicKeyBytes)
// pub key is packed with 1 byte length
.slice(1)
]);
}
export async function verifyCertificateChain(chain, host, logger, fetchCertificateBytes = defaultFetchCertificateBytes, additionalRootCAs) {
const rootCAs = [
...loadRootCAs(),
...(additionalRootCAs || [])
];
const leaf = chain[0];
const commonNames = [
...leaf.getSubjectField('CN'),
...leaf.getAlternativeDNSNames()
];
if (!commonNames.some(cn => matchHostname(host, cn))) {
throw new Error(`Certificate is not for host ${host}`);
}
chain = [...chain]; // clone to allow appending fetched certs
for (let i = 0; i < chain.length; i++) {
const cert = chain[i];
const cn = cert.getSubjectField('CN');
if (!cert.isWithinValidity()) {
throw new Error(`Certificate ${cn} (i: ${i}) is outside validity`);
}
// look in our chain for issuer
let issuer = findIssuer(chain.slice(i + 1), cert);
// if not found, check in our root CAs
if (!issuer) {
issuer = findIssuer(rootCAs, cert);
}
// if not found, we'll try fetching it via AIA extension
if (!issuer) {
const aiaExt = cert.getAIAExtension();
if (!aiaExt) {
throw new Error(`Missing issuer for certificate ${cn} (i: ${i})`);
}
if (TLS_INTERMEDIATE_CA_CACHE?.[aiaExt]) {
issuer = TLS_INTERMEDIATE_CA_CACHE[aiaExt];
}
else {
logger.debug({ aiaExt, cn }, 'fetching issuer certificate via AIA extension');
const bytes = await fetchCertificateBytes(aiaExt);
issuer = await loadX509FromPem(bytes);
// we'll need to verify this cert below too
chain.push(issuer);
TLS_INTERMEDIATE_CA_CACHE[aiaExt] = issuer;
}
}
if (!issuer.isWithinValidity()) {
throw new Error(`Issuer Cert ${cn} is not within validity period`);
}
if (!(await issuer.verifyIssued(cert))) {
const icn = issuer.getSubjectField('CN');
throw new Error(`Verification of ${cn} failed by issuer ${icn} (i: ${i})`);
}
}
}
function findIssuer(chain, cert) {
for (const element of chain) {
if (element.isIssuer(cert)) {
return element;
}
}
}
/**
* Checks if a hostname matches a common name
* @param host the hostname, eg. "google.com"
* @param commonName the common name from the certificate,
* eg. "*.google.com", "google.com"
*/
function matchHostname(host, commonName) {
// write a regex to match the common name
// and check if it matches the hostname
const hostComps = host.split('.');
const cnComps = commonName.split('.');
if (cnComps.length !== hostComps.length) {
// can ignore the first component if it's a wildcard
if (cnComps[0] === '*'
&& cnComps.length === hostComps.length + 1) {
cnComps.shift();
}
else {
return false;
}
}
return hostComps.every((comp, i) => (comp === cnComps[i]
|| cnComps[i] === '*'));
}
function loadRootCAs() {
if (ROOT_CAS) {
return ROOT_CAS;
}
ROOT_CAS = MOZILLA_ROOT_CA_LIST.map(loadX509FromPem);
if (typeof TLS_ADDITIONAL_ROOT_CA_LIST !== 'undefined') {
ROOT_CAS.push(...TLS_ADDITIONAL_ROOT_CA_LIST.map(loadX509FromPem));
}
return ROOT_CAS;
}