Skip to content

Commit

Permalink
add Certificate Signing Request (CSR) parse action
Browse files Browse the repository at this point in the history
  • Loading branch information
jkataja committed Jan 22, 2023
1 parent 2efd075 commit e9581dc
Show file tree
Hide file tree
Showing 2 changed files with 289 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@
"RSA Verify",
"RSA Encrypt",
"RSA Decrypt",
"Parse SSH Host Key"
"Parse SSH Host Key",
"Parse CSR"
]
},
{
Expand Down
287 changes: 287 additions & 0 deletions src/core/operations/ParseCSR.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/**
* @author jkataja [none]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";
import forge from "node-forge";
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://en.wikipedia.org/wiki/Certificate_signing_request";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Input format",
"type": "option",
"value": ["PEM"]
},
{
"name": "Strict ASN.1 value lengths",
"type": "boolean",
"value": true
}
];
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";
}

const csr = forge.pki.certificationRequestFromPem(input, args[1]);

// RSA algorithm is the only one supported for CSR in node-forge as of 1.3.1
return `Version: ${1 + csr.version} (0x${Utils.hex(csr.version)})
Subject:
${formatSubject(csr.subject)}
Attributes:
${formatAttributes(csr)}
Public Key:
Key Size: ${csr.publicKey.n.bitLength()} bits
Modulus:
${formatMultiLine(chop(csr.publicKey.n.toString(16).replace(/(..)/g, "$&:")))}
Exponent: ${csr.publicKey.e} (0x${Utils.hex(csr.publicKey.e)})
Signature:
Algorithm ID: ${forge.pki.oids[csr.signatureOid]}
Signature Value:
${formatSignature(csr.signature)}
`;
}
}

const SUBJECT_PRETTY = new Map();

SUBJECT_PRETTY.set("E", "Email Address");
SUBJECT_PRETTY.set("CN", "Common Name");
SUBJECT_PRETTY.set("C", "Country");
SUBJECT_PRETTY.set("L", "Locality");
SUBJECT_PRETTY.set("ST", "State or Province");
SUBJECT_PRETTY.set("O", "Organization");
SUBJECT_PRETTY.set("OU", "Organizational Unit");

/**
* Format Subject of the request as a multi-line string
* @param {*} subject CSR Subject
* @returns Multi-line string describing Subject
*/
function formatSubject(subject) {
let out = "";

for (const key of SUBJECT_PRETTY.keys()) {
if (subject.getField(key)) {
out += ` ${SUBJECT_PRETTY.get(key)} (${key}): `.padEnd(28) + subject.getField(key).value + "\n";
}
}

return chop(out);
}

/**
* Format known attributes of a CSR
* @param {*} csr CSR object
* @returns Multi-line string describing attributes
*/
function formatAttributes(csr) {
let out = "";

for (const attribute of csr.attributes) {
switch (attribute.name) {
case "extensionRequest" :
out += ` Extensions:\n`;
for (const extension of attribute.extensions) {
const criticality = (extension.critical ? " critical" : "");
switch (extension.name) {
case "basicConstraints" :
out += ` Constraints:${criticality}\n ${describeBasicConstraints(extension).join("\n ")}\n`;
break;
case "keyUsage" :
out += ` Key Usage:${criticality}\n ${describeKeyUsage(extension).join("\n ")}\n`;
break;
case "extKeyUsage" :
out += ` Extended Key Usage:${criticality}\n ${describeExtendedKeyUsage(extension).join("\n ")}\n`;
break;
case "subjectAltName" :
out += ` Subject Alternative Names:${criticality}\n ${describeSubjectAlternativeNames(extension).join("\n ")}\n`;
break;
default :
out += ` (unable to format${criticality} "${extension.name}" extension)\n`;
}
}
break;
case "unstructuredName" :
out += ` Unstructured Name: ${attribute.value}\n`;
break;
default:
out += ` (unable to format "${attribute.name}" attribute)\n`;
}
}

return chop(out);
}

/**
* Format signature as a multi-line hex string
* @param {*} signature
* @returns Signature as a multi-line hex string
*/
function formatSignature(signature) {
return formatMultiLine(Utils.strToByteArray(signature).map(b => Utils.hex(b)).join(":"));
}

/**
* Format hex string onto multiple lines
* @param {*} longStr
* @returns Hex string as a multi-line hex string
*/
function formatMultiLine(longStr) {
let out = "";

for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) {
out += ` ${remain.substring(0, 48)}\n`;
}

return chop(out);
}

/**
* 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 = [];

if (extension.cA) constraints.push("Subject is a CA");
else constraints.push("Subject is NOT a CA");

if (extension.pathLenConstraint) constraints.push(`Maximum depth of valid certification paths = ${extension.pathLenConstraint}`);

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 = [];

if (extension.digitalSignature) usage.push("Digital signature");
if (extension.nonRepudiation) usage.push("Non-repudiation");
if (extension.keyEncipherment) usage.push("Key encipherment");
if (extension.dataEncipherment) usage.push("Data encipherment");
if (extension.keyAgreement) usage.push("Key agreement");
if (extension.keyCertSign) usage.push("Key certificate signing");
if (extension.cRLSign) usage.push("CRL signing");
if (extension.encipherOnly) usage.push("Encipher only");
if (extension.decipherOnly) usage.push("Decipher only");

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 = [];

if (extension.serverAuth) usage.push("TLS Web Server Authentication");
if (extension.clientAuth) usage.push("TLS Web Client Authentication");
if (extension.codeSigning) usage.push("Code signing");
if (extension.emailProtection) usage.push("E-mail Protection (S/MIME)");
if (extension.timeStamping) usage.push("Trusted Timestamping");
if (extension.msCodeInd) usage.push("Microsoft Individual Code Signing");
if (extension.msCodeCom) usage.push("Microsoft Commercial Code Signing");
if (extension.msCTLSign) usage.push("Microsoft Trust List Signing");
if (extension.msSGC) usage.push("Microsoft Server Gated Crypto");
if (extension.msEFS) usage.push("Microsoft Encrypted File System");
if (extension.nsSGC) usage.push("Netscape Server Gated Crypto");

if (usage.length === 0) usage.push("(none)");

return usage;
}

/**
* Describe Subject Alternative Names
* @param {*} extension CSR extension with the name `subjectAltName`
* @returns Array of strings describing Subject Alternative Names
*/
function describeSubjectAlternativeNames(extension) {
const names = [];

for (const altName of extension.altNames) {
switch (altName.type) {
case 1:
names.push(`EMAIL: ${altName.value}`);
break;
case 2:
names.push(`DNS: ${altName.value}`);
break;
case 6:
names.push(`URI: ${altName.value}`);
break;
case 7:
names.push(`IP: ${altName.ip}`);
break;
default:
names.push(`(unable to format type ${altName.type} data)`);
}
}

if (names.length === 0) names.push("(none)");

return names;
}

/**
* 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;

0 comments on commit e9581dc

Please sign in to comment.