UNPKG

@reclaimprotocol/tls

Version:

WebCrypto Based Cross Platform TLS

184 lines (183 loc) 6.71 kB
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 { 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, additionalRootCAs) { const rootCAs = [ ...loadRootCAs(), ...(additionalRootCAs || []) ]; const commonNames = [ ...chain[0].getSubjectField('CN'), ...chain[0].getAlternativeDNSNames() ]; if (!commonNames.some(cn => matchHostname(host, cn))) { throw new Error(`Certificate is not for host ${host}`); } const tmpChain = [...chain]; let currentCert = tmpChain.shift(); let rootIssuer; // look for issuers until we hit the end or find root CA that signed one of them while (!rootIssuer) { const cn = currentCert.getSubjectField('CN'); if (!currentCert.isWithinValidity()) { throw new Error(`Certificate ${cn} is not within validity period`); } rootIssuer = rootCAs.find(r => r.isIssuer(currentCert)); if (!rootIssuer) { const issuer = findIssuer(tmpChain, currentCert); //in case there are orphan certificates in chain if (!issuer) { break; } if (!(await issuer.cert.verifyIssued(currentCert))) { throw new Error(`Certificate ${cn} issue verification failed`); } currentCert = issuer.cert; //remove issuer cert from chain tmpChain.splice(issuer.index, 1); } } if (!rootIssuer) { throw new Error('Root CA not found. Could not verify certificate'); } const verified = await rootIssuer.verifyIssued(currentCert); if (!verified) { throw new Error('Root CA did not issue certificate'); } if (!rootIssuer.isWithinValidity()) { throw new Error('CA certificate is not within validity period'); } function findIssuer(chain, cert) { for (const [i, element] of chain.entries()) { if (element.isIssuer(cert)) { return { cert: element, index: i }; } } return null; } } /** * 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; }