2023-01-22 01:48:49 +01:00
|
|
|
/**
|
|
|
|
* @author jkataja
|
|
|
|
* @copyright Crown Copyright 2023
|
|
|
|
* @license Apache-2.0
|
|
|
|
*/
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
import r from "jsrsasign";
|
2023-01-22 01:48:49 +01:00
|
|
|
import Operation from "../Operation.mjs";
|
2024-06-09 00:07:16 +01:00
|
|
|
import { formatDnObj } from "../lib/PublicKey.mjs";
|
2023-01-22 01:48:49 +01:00
|
|
|
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";
|
2024-04-05 18:10:57 +01:00
|
|
|
this.infoURL = "https://wikipedia.org/wiki/Certificate_signing_request";
|
2023-01-22 01:48:49 +01:00
|
|
|
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";
|
|
|
|
}
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
// 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)}`;
|
2023-01-22 01:48:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-09 00:07:16 +01:00
|
|
|
* Format signature of a CSR
|
|
|
|
* @param {*} sigAlg string
|
|
|
|
* @param {*} sigHex string
|
|
|
|
* @returns Multi-line string describing CSR Signature
|
2023-01-22 01:48:49 +01:00
|
|
|
*/
|
2024-06-09 00:07:16 +01:00
|
|
|
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:
|
2024-06-12 18:52:55 +01:00
|
|
|
R: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[0]))}
|
|
|
|
S: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[1]))}\n`;
|
2024-06-09 00:07:16 +01:00
|
|
|
} else if (new RegExp("withrsa", "i").test(sigAlg)) {
|
|
|
|
out += ` Signature: ${formatHexOntoMultiLine(sigHex)}\n`;
|
2024-06-12 18:52:55 +01:00
|
|
|
} else {
|
|
|
|
out += ` Signature: ${formatHexOntoMultiLine(ensureHexIsPositiveInTwosComplement(sigHex))}\n`;
|
2023-01-22 01:48:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return chop(out);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-09 00:07:16 +01:00
|
|
|
* Format Subject Public Key from PEM encoded public key string
|
|
|
|
* @param {*} publicKeyPEM string
|
|
|
|
* @returns Multi-line string describing Subject Public Key Info
|
2023-01-22 01:48:49 +01:00
|
|
|
*/
|
2024-06-09 00:07:16 +01:00
|
|
|
function formatSubjectPublicKey(publicKeyPEM) {
|
2023-01-22 01:48:49 +01:00
|
|
|
let out = "\n";
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
const publicKey = r.KEYUTIL.getKey(publicKeyPEM);
|
|
|
|
if (publicKey instanceof r.RSAKey) {
|
|
|
|
out += ` Algorithm: RSA
|
|
|
|
Length: ${publicKey.n.bitLength()} bits
|
2024-06-12 18:52:55 +01:00
|
|
|
Modulus: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.n))}
|
2024-06-09 00:07:16 +01:00
|
|
|
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
|
2024-06-12 18:52:55 +01:00
|
|
|
Pub: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.y))}
|
|
|
|
P: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.p))}
|
|
|
|
Q: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.q))}
|
|
|
|
G: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.g))}\n`;
|
2024-06-09 00:07:16 +01:00
|
|
|
} else {
|
|
|
|
out += `unsupported public key algorithm\n`;
|
2023-01-22 01:48:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return chop(out);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Format known extensions of a CSR
|
2024-06-09 00:07:16 +01:00
|
|
|
* @param {*} csrParam object
|
|
|
|
* @returns Multi-line string describing CSR Requested Extensions
|
2023-01-22 01:48:49 +01:00
|
|
|
*/
|
2024-06-09 00:07:16 +01:00
|
|
|
function formatRequestedExtensions(csrParam) {
|
|
|
|
const formattedExtensions = new Array(4).fill("");
|
2023-01-22 01:48:49 +01:00
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
if (Object.hasOwn(csrParam, "extreq")) {
|
|
|
|
for (const extension of csrParam.extreq) {
|
2023-01-22 01:48:49 +01:00
|
|
|
let parts = [];
|
2024-06-09 00:07:16 +01:00
|
|
|
switch (extension.extname) {
|
2023-01-22 01:48:49 +01:00
|
|
|
case "basicConstraints" :
|
|
|
|
parts = describeBasicConstraints(extension);
|
2024-06-09 00:07:16 +01:00
|
|
|
formattedExtensions[0] = ` Basic Constraints:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
|
2023-01-22 01:48:49 +01:00
|
|
|
break;
|
|
|
|
case "keyUsage" :
|
|
|
|
parts = describeKeyUsage(extension);
|
2024-06-09 00:07:16 +01:00
|
|
|
formattedExtensions[1] = ` Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
|
2023-01-22 01:48:49 +01:00
|
|
|
break;
|
|
|
|
case "extKeyUsage" :
|
|
|
|
parts = describeExtendedKeyUsage(extension);
|
2024-06-09 00:07:16 +01:00
|
|
|
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)}`;
|
2023-01-22 01:48:49 +01:00
|
|
|
break;
|
|
|
|
default :
|
2024-06-09 00:07:16 +01:00
|
|
|
parts = ["(unsuported extension)"];
|
|
|
|
formattedExtensions.push(` ${extension.extname}:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`);
|
2023-01-22 01:48:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
let out = "\n";
|
|
|
|
|
|
|
|
formattedExtensions.forEach((formattedExtension) => {
|
|
|
|
if (formattedExtension !== undefined && formattedExtension !== null && formattedExtension.length !== 0) {
|
|
|
|
out += formattedExtension;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-01-22 01:48:49 +01:00
|
|
|
return chop(out);
|
|
|
|
}
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
/**
|
|
|
|
* 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" : "";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-12 18:52:55 +01:00
|
|
|
* Format string input as a comma separated hex string on multiple lines
|
|
|
|
* @param {*} hex String
|
2024-06-09 00:07:16 +01:00
|
|
|
* @returns Multi-line string describing the Hex input
|
|
|
|
*/
|
2024-06-12 18:52:55 +01:00
|
|
|
function formatHexOntoMultiLine(hex) {
|
|
|
|
if (hex.length % 2 !== 0) {
|
2024-06-12 19:00:14 +01:00
|
|
|
hex = "0" + hex;
|
2024-06-12 18:52:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2024-06-12 19:00:14 +01:00
|
|
|
return ensureHexIsPositiveInTwosComplement(int.toString(16));
|
2024-06-12 18:52:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2024-06-09 00:07:16 +01:00
|
|
|
|
2024-06-12 18:52:55 +01:00
|
|
|
// prepend 00 if most significant bit is 1 (sign bit)
|
|
|
|
if (hex.length >=2 && (parseInt(hex.substring(0, 2), 16) & 128)) {
|
|
|
|
hex = "00" + hex;
|
2024-06-09 00:07:16 +01:00
|
|
|
}
|
|
|
|
|
2024-06-12 19:00:14 +01:00
|
|
|
return hex;
|
2024-06-09 00:07:16 +01:00
|
|
|
}
|
2023-01-22 01:48:49 +01:00
|
|
|
|
|
|
|
/**
|
2024-06-09 00:07:16 +01:00
|
|
|
* Format string onto multiple lines
|
2023-01-22 01:48:49 +01:00
|
|
|
* @param {*} longStr
|
2024-06-09 00:07:16 +01:00
|
|
|
* @returns String as a multi-line string
|
2023-01-22 01:48:49 +01:00
|
|
|
*/
|
|
|
|
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 = [];
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
constraints.push(`CA = ${Object.hasOwn(extension, "cA") && extension.cA ? "true" : "false"}`);
|
|
|
|
if (Object.hasOwn(extension, "pathLen")) constraints.push(`PathLenConstraint = ${extension.pathLen}`);
|
2023-01-22 01:48:49 +01:00
|
|
|
|
|
|
|
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 = [];
|
|
|
|
|
2024-06-12 18:52:55 +01:00
|
|
|
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",
|
|
|
|
};
|
2024-06-09 00:07:16 +01:00
|
|
|
|
|
|
|
if (Object.hasOwn(extension, "names")) {
|
|
|
|
extension.names.forEach((ku) => {
|
2024-06-12 18:52:55 +01:00
|
|
|
if (Object.hasOwn(kuIdentifierToName, ku)) {
|
|
|
|
usage.push(kuIdentifierToName[ku]);
|
2024-06-09 00:07:16 +01:00
|
|
|
} else {
|
|
|
|
usage.push(`unknown key usage (${ku})`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-01-22 01:48:49 +01:00
|
|
|
|
|
|
|
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 = [];
|
|
|
|
|
2024-06-12 18:52:55 +01:00
|
|
|
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
|
|
|
|
};
|
2024-06-09 00:07:16 +01:00
|
|
|
|
|
|
|
if (Object.hasOwn(extension, "array")) {
|
|
|
|
extension.array.forEach((eku) => {
|
2024-06-12 18:52:55 +01:00
|
|
|
if (Object.hasOwn(ekuIdentifierToName, eku)) {
|
|
|
|
usage.push(ekuIdentifierToName[eku]);
|
2024-06-09 00:07:16 +01:00
|
|
|
} else {
|
2024-06-12 18:52:55 +01:00
|
|
|
usage.push(eku);
|
2024-06-09 00:07:16 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-01-22 01:48:49 +01:00
|
|
|
|
|
|
|
if (usage.length === 0) usage.push("(none)");
|
|
|
|
|
|
|
|
return usage;
|
|
|
|
}
|
|
|
|
|
2024-06-09 00:07:16 +01:00
|
|
|
/**
|
|
|
|
* 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:
|
2024-06-12 18:52:55 +01:00
|
|
|
names.push(`(unable to format SAN '${key}':${altName[key]})\n`);
|
2024-06-09 00:07:16 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return names;
|
|
|
|
}
|
|
|
|
|
2023-01-22 01:48:49 +01:00
|
|
|
/**
|
|
|
|
* 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;
|