UNPKG

@sap/xssec

Version:

XS Advanced Container Security API for node.js

196 lines (162 loc) 5.8 kB
const { X509Certificate } = require("crypto"); const InvalidClientCertificateError = require("../error/validation/InvalidClientCertificateError"); const { PEM_HEADER, PEM_FOOTER } = require("./constants"); module.exports = { parsePemCertificate(pem) { // restores escaped new lines pem = pem.replaceAll("\\n", "\n"); // restores PEM header and footer if missing if (!pem.startsWith(PEM_HEADER)) pem = `${PEM_HEADER}\n${pem}`; if (!pem.endsWith(PEM_FOOTER)) pem = `${pem}\n${PEM_FOOTER}`; try { return new X509Certificate(pem); } catch (error) { throw new InvalidClientCertificateError(pem, error); } }, /** * Extracts PEM certificate from either CF PEM format header or Envoy XFCC format header * @param {string} headerValue - The x-forwarded-client-cert header value * @returns {string|null} The PEM certificate or null if not found/invalid */ extractPemFromClientCertHeader(headerValue) { if (!headerValue) { return null; } if (isXfccFormat(headerValue)) { return parseXfccToPem(headerValue); } else { // Assume it's already in PEM format return headerValue; } }, isXfccFormat, parseXfccToPem, splitXfccString, isEscaped, extractCertFromXfccEntry, unquoteAndDecodeCert }; /** * Determines if the given header value is in XFCC format. * According to Envoy documentation, the Hash property is always set in XFCC format, * making it the most reliable indicator. * @param {string} headerValue - The header value to check * @returns {boolean} true if XFCC format, false if PEM format or other */ function isXfccFormat(headerValue) { if (!headerValue || typeof headerValue !== 'string') { return false; } const hashPattern = /\bHash=/i; return hashPattern.test(headerValue); } /** * Parses XFCC (x-forwarded-client-cert) format and extracts the PEM certificate * @param {string} xfccHeader - The XFCC header value * @returns {string|null} The extracted PEM certificate or null if not found */ function parseXfccToPem(xfccHeader) { if (!xfccHeader || typeof xfccHeader !== 'string') { return null; } // Split by comma to handle multiple certificate entries const xfccEntries = splitXfccString(xfccHeader, ','); // Use the last entry in case of multiple certificates const lastEntry = xfccEntries[xfccEntries.length - 1]; return extractCertFromXfccEntry(lastEntry); } /** * Splits XFCC string by the specified delimiter while respecting quoted values * @param {string} str - The string to split * @param {string} delimiter - The delimiter character to split on * @returns {string[]} Array of split parts */ function splitXfccString(str, delimiter) { const parts = []; let current = ''; let inQuotes = false; for (let i = 0; i < str.length; i++) { const char = str[i]; // Handle escaped double quotes properly - only unescaped double quotes should toggle quote state if (char === '"' && !isEscaped(str, i)) { inQuotes = !inQuotes; } if (char === delimiter && !inQuotes) { parts.push(current.trim()); current = ''; } else { current += char; } } if (current.trim()) { parts.push(current.trim()); } return parts; } /** * Checks if a character at the given position is escaped by counting preceding backslashes * @param {string} str - The string to check * @param {number} pos - The position of the character to check * @returns {boolean} true if the character is escaped */ function isEscaped(str, pos) { let backslashCount = 0; let i = pos - 1; // Count consecutive backslashes before the character while (i >= 0 && str[i] === '\\') { backslashCount++; i--; } // Character is escaped if there's an odd number of backslashes before it return backslashCount % 2 === 1; } /** * Extracts the certificate from a single XFCC entry * @param {string} xfccEntry - A single XFCC entry * @returns {string|null} The extracted PEM certificate or null if not found */ function extractCertFromXfccEntry(xfccEntry) { if (!xfccEntry) { return null; } // Split the entry by semicolon to get field=value pairs const fields = splitXfccString(xfccEntry, ';'); for (const field of fields) { const equalIndex = field.indexOf('='); if (equalIndex === -1) continue; const fieldName = field.substring(0, equalIndex).trim(); const fieldValue = field.substring(equalIndex + 1).trim(); if (fieldName.toLowerCase() === 'cert') { return unquoteAndDecodeCert(fieldValue); } } return null; } /** * Unquotes and URL decodes the certificate value * @param {string} certValue - The quoted and encoded certificate value * @returns {string|null} The decoded PEM certificate or null if invalid */ function unquoteAndDecodeCert(certValue) { if (!certValue) { return null; } let cert = certValue.trim(); // Remove surrounding quotes if present if ((cert.startsWith('"') && cert.endsWith('"')) || (cert.startsWith("'") && cert.endsWith("'"))) { cert = cert.slice(1, -1); } // Unescape escaped double quotes according to Envoy spec: \" -> " cert = cert.replace(/\\"/g, '"'); try { // URL decode the certificate cert = decodeURIComponent(cert); return cert; } catch (error) { // If decoding fails, return the original value without URL decoding // but still with unescaped quotes return cert; } }