/** * @author jkataja * @copyright Crown Copyright 2023 * @license Apache-2.0 */ import r from "jsrsasign"; import Operation from "../Operation.mjs"; import { formatDnObj } from "../lib/PublicKey.mjs"; import Utils from "../Utils.mjs"; /** * Parse CSR operation */ class ParseCSR extends Operation { /** * ParseCSR constructor */ constructor() { super(); this.name = "Parse CSR"; this.module = "PublicKey"; this.description = "Parse Certificate Signing Request (CSR) for an X.509 certificate"; this.infoURL = "https://wikipedia.org/wiki/Certificate_signing_request"; this.inputType = "string"; this.outputType = "string"; this.args = [ { "name": "Input format", "type": "option", "value": ["PEM"] } ]; this.checks = [ { "pattern": "^-+BEGIN CERTIFICATE REQUEST-+\\r?\\n[\\da-z+/\\n\\r]+-+END CERTIFICATE REQUEST-+\\r?\\n?$", "flags": "i", "args": ["PEM"] } ]; } /** * @param {string} input * @param {Object[]} args * @returns {string} Human-readable description of a Certificate Signing Request (CSR). */ run(input, args) { if (!input.length) { return "No input"; } // Parse the CSR into JSON parameters const csrParam = new r.KJUR.asn1.csr.CSRUtil.getParam(input); return `Subject\n${formatDnObj(csrParam.subject, 2)} Public Key${formatSubjectPublicKey(csrParam.sbjpubkey)} Signature${formatSignature(csrParam.sigalg, csrParam.sighex)} Requested Extensions${formatRequestedExtensions(csrParam)}`; } } /** * Format signature of a CSR * @param {*} sigAlg string * @param {*} sigHex string * @returns Multi-line string describing CSR Signature */ function formatSignature(sigAlg, sigHex) { let out = `\n`; out += ` Algorithm: ${sigAlg}\n`; if (new RegExp("withdsa", "i").test(sigAlg)) { const d = new r.KJUR.crypto.DSA(); const sigParam = d.parseASN1Signature(sigHex); out += ` Signature: R: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[0]))} S: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[1]))}\n`; } else if (new RegExp("withrsa", "i").test(sigAlg)) { out += ` Signature: ${formatHexOntoMultiLine(sigHex)}\n`; } else { out += ` Signature: ${formatHexOntoMultiLine(ensureHexIsPositiveInTwosComplement(sigHex))}\n`; } return chop(out); } /** * Format Subject Public Key from PEM encoded public key string * @param {*} publicKeyPEM string * @returns Multi-line string describing Subject Public Key Info */ function formatSubjectPublicKey(publicKeyPEM) { let out = "\n"; const publicKey = r.KEYUTIL.getKey(publicKeyPEM); if (publicKey instanceof r.RSAKey) { out += ` Algorithm: RSA Length: ${publicKey.n.bitLength()} bits Modulus: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.n))} Exponent: ${publicKey.e} (0x${Utils.hex(publicKey.e)})\n`; } else if (publicKey instanceof r.KJUR.crypto.ECDSA) { out += ` Algorithm: ECDSA Length: ${publicKey.ecparams.keylen} bits Pub: ${formatHexOntoMultiLine(publicKey.pubKeyHex)} ASN1 OID: ${r.KJUR.crypto.ECDSA.getName(publicKey.getShortNISTPCurveName())} NIST CURVE: ${publicKey.getShortNISTPCurveName()}\n`; } else if (publicKey instanceof r.KJUR.crypto.DSA) { out += ` Algorithm: DSA Length: ${publicKey.p.toString(16).length * 4} bits Pub: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.y))} P: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.p))} Q: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.q))} G: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.g))}\n`; } else { out += `unsupported public key algorithm\n`; } return chop(out); } /** * Format known extensions of a CSR * @param {*} csrParam object * @returns Multi-line string describing CSR Requested Extensions */ function formatRequestedExtensions(csrParam) { const formattedExtensions = new Array(4).fill(""); if (Object.hasOwn(csrParam, "extreq")) { for (const extension of csrParam.extreq) { let parts = []; switch (extension.extname) { case "basicConstraints" : parts = describeBasicConstraints(extension); formattedExtensions[0] = ` Basic Constraints:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`; break; case "keyUsage" : parts = describeKeyUsage(extension); formattedExtensions[1] = ` Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`; break; case "extKeyUsage" : parts = describeExtendedKeyUsage(extension); formattedExtensions[2] = ` Extended Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`; break; case "subjectAltName" : parts = describeSubjectAlternativeName(extension); formattedExtensions[3] = ` Subject Alternative Name:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`; break; default : parts = ["(unsuported extension)"]; formattedExtensions.push(` ${extension.extname}:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`); } } } let out = "\n"; formattedExtensions.forEach((formattedExtension) => { if (formattedExtension !== undefined && formattedExtension !== null && formattedExtension.length !== 0) { out += formattedExtension; } }); return chop(out); } /** * Format extension critical tag * @param {*} extension Object * @returns String describing whether the extension is critical or not */ function formatExtensionCriticalTag(extension) { return Object.hasOwn(extension, "critical") && extension.critical ? " critical" : ""; } /** * Format string input as a comma separated hex string on multiple lines * @param {*} hex String * @returns Multi-line string describing the Hex input */ function formatHexOntoMultiLine(hex) { if (hex.length % 2 !== 0) { hex = "0" + hex; } return formatMultiLine(chop(hex.replace(/(..)/g, "$&:"))); } /** * Convert BigInt to abs value in Hex * @param {*} int BigInt * @returns String representing absolute value in Hex */ function absBigIntToHex(int) { int = int < 0n ? -int : int; return ensureHexIsPositiveInTwosComplement(int.toString(16)); } /** * Ensure Hex String remains positive in 2's complement * @param {*} hex String * @returns Hex String ensuring value remains positive in 2's complement */ function ensureHexIsPositiveInTwosComplement(hex) { if (hex.length % 2 !== 0) { return "0" + hex; } // prepend 00 if most significant bit is 1 (sign bit) if (hex.length >=2 && (parseInt(hex.substring(0, 2), 16) & 128)) { hex = "00" + hex; } return hex; } /** * Format string onto multiple lines * @param {*} longStr * @returns String as a multi-line string */ function formatMultiLine(longStr) { const lines = []; for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) { lines.push(remain.substring(0, 48)); } return lines.join("\n "); } /** * Describe Basic Constraints * @see RFC 5280 4.2.1.9. Basic Constraints https://www.ietf.org/rfc/rfc5280.txt * @param {*} extension CSR extension with the name `basicConstraints` * @returns Array of strings describing Basic Constraints */ function describeBasicConstraints(extension) { const constraints = []; constraints.push(`CA = ${Object.hasOwn(extension, "cA") && extension.cA ? "true" : "false"}`); if (Object.hasOwn(extension, "pathLen")) constraints.push(`PathLenConstraint = ${extension.pathLen}`); return constraints; } /** * Describe Key Usage extension permitted use cases * @see RFC 5280 4.2.1.3. Key Usage https://www.ietf.org/rfc/rfc5280.txt * @param {*} extension CSR extension with the name `keyUsage` * @returns Array of strings describing Key Usage extension permitted use cases */ function describeKeyUsage(extension) { const usage = []; const kuIdentifierToName = { digitalSignature: "Digital Signature", nonRepudiation: "Non-repudiation", keyEncipherment: "Key encipherment", dataEncipherment: "Data encipherment", keyAgreement: "Key agreement", keyCertSign: "Key certificate signing", cRLSign: "CRL signing", encipherOnly: "Encipher Only", decipherOnly: "Decipher Only", }; if (Object.hasOwn(extension, "names")) { extension.names.forEach((ku) => { if (Object.hasOwn(kuIdentifierToName, ku)) { usage.push(kuIdentifierToName[ku]); } else { usage.push(`unknown key usage (${ku})`); } }); } if (usage.length === 0) usage.push("(none)"); return usage; } /** * Describe Extended Key Usage extension permitted use cases * @see RFC 5280 4.2.1.12. Extended Key Usage https://www.ietf.org/rfc/rfc5280.txt * @param {*} extension CSR extension with the name `extendedKeyUsage` * @returns Array of strings describing Extended Key Usage extension permitted use cases */ function describeExtendedKeyUsage(extension) { const usage = []; const ekuIdentifierToName = { "serverAuth": "TLS Web Server Authentication", "clientAuth": "TLS Web Client Authentication", "codeSigning": "Code signing", "emailProtection": "E-mail Protection (S/MIME)", "timeStamping": "Trusted Timestamping", "1.3.6.1.4.1.311.2.1.21": "Microsoft Individual Code Signing", // msCodeInd "1.3.6.1.4.1.311.2.1.22": "Microsoft Commercial Code Signing", // msCodeCom "1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing", // msCTLSign "1.3.6.1.4.1.311.10.3.3": "Microsoft Server Gated Crypto", // msSGC "1.3.6.1.4.1.311.10.3.4": "Microsoft Encrypted File System", // msEFS "1.3.6.1.4.1.311.20.2.2": "Microsoft Smartcard Login", // msSmartcardLogin "2.16.840.1.113730.4.1": "Netscape Server Gated Crypto", // nsSGC }; if (Object.hasOwn(extension, "array")) { extension.array.forEach((eku) => { if (Object.hasOwn(ekuIdentifierToName, eku)) { usage.push(ekuIdentifierToName[eku]); } else { usage.push(eku); } }); } if (usage.length === 0) usage.push("(none)"); return usage; } /** * Format Subject Alternative Names from the name `subjectAltName` extension * @see RFC 5280 4.2.1.6. Subject Alternative Name https://www.ietf.org/rfc/rfc5280.txt * @param {*} extension object * @returns Array of strings describing Subject Alternative Name extension */ function describeSubjectAlternativeName(extension) { const names = []; if (Object.hasOwn(extension, "extname") && extension.extname === "subjectAltName") { if (Object.hasOwn(extension, "array")) { for (const altName of extension.array) { Object.keys(altName).forEach((key) => { switch (key) { case "rfc822": names.push(`EMAIL: ${altName[key]}`); break; case "dns": names.push(`DNS: ${altName[key]}`); break; case "uri": names.push(`URI: ${altName[key]}`); break; case "ip": names.push(`IP: ${altName[key]}`); break; case "dn": names.push(`DIR: ${altName[key].str}`); break; case "other" : names.push(`Other: ${altName[key].oid}::${altName[key].value.utf8str.str}`); break; default: names.push(`(unable to format SAN '${key}':${altName[key]})\n`); } }); } } } return names; } /** * Join an array of strings and add leading spaces to each line. * @param {*} n How many leading spaces * @param {*} parts Array of strings * @returns Joined and indented string. */ function indent(n, parts) { const fluff = " ".repeat(n); return fluff + parts.join("\n" + fluff) + "\n"; } /** * Remove last character from a string. * @param {*} s String * @returns Chopped string. */ function chop(s) { return s.substring(0, s.length - 1); } export default ParseCSR;