Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new operation: Parse Certificate Signing Request (CSR) #1504

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@
"RSA Verify",
"RSA Encrypt",
"RSA Decrypt",
"Parse SSH Host Key"
"Parse SSH Host Key",
"Parse CSR"
]
},
{
Expand Down
273 changes: 273 additions & 0 deletions src/core/operations/ParseCSR.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/**
* @author jkataja
* @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": "Key type",
"type": "option",
"value": ["RSA"]
},
{
"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)}
Subject Alternative Names${formatSubjectAlternativeNames(csr)}
Public Key
Algorithm: RSA
Length: ${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: ${forge.pki.oids[csr.signatureOid]}
Signature: ${formatMultiLine(Utils.strToByteArray(csr.signature).map(b => Utils.hex(b)).join(":"))}
Extensions${formatExtensions(csr)}`;
}
}

/**
* 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 = "\n";

for (const attribute of subject.attributes) {
out += ` ${attribute.shortName} = ${attribute.value}\n`;
}

return chop(out);
}


/**
* Format Subject Alternative Names from the name `subjectAltName` extension
* @param {*} extension CSR object
* @returns Multi-line string describing Subject Alternative Names
*/
function formatSubjectAlternativeNames(csr) {
let out = "\n";

for (const attribute of csr.attributes) {
for (const extension of attribute.extensions) {
if (extension.name === "subjectAltName") {
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} name)\n`);
}
}
out += indent(2, names);
}
}
}

return chop(out);
}

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

for (const attribute of csr.attributes) {
for (const extension of attribute.extensions) {
// formatted separately
if (extension.name === "subjectAltName") {
continue;
}
out += ` ${extension.name}${(extension.critical ? " CRITICAL" : "")}:\n`;
let parts = [];
switch (extension.name) {
case "basicConstraints" :
parts = describeBasicConstraints(extension);
break;
case "keyUsage" :
parts = describeKeyUsage(extension);
break;
case "extKeyUsage" :
parts = describeExtendedKeyUsage(extension);
break;
default :
parts = ["(unable to format extension)"];
}
out += indent(4, parts);
}
}

return chop(out);
}


/**
* Format hex string onto multiple lines
* @param {*} longStr
* @returns Hex string as a multi-line hex 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 = ${extension.cA}`);
if (extension.pathLenConstraint !== undefined) constraints.push(`PathLenConstraint = ${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;
}

/**
* 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;
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ import "./tests/Typex.mjs";
import "./tests/UnescapeString.mjs";
import "./tests/Unicode.mjs";
import "./tests/YARA.mjs";
import "./tests/ParseCSR.mjs";

const testStatus = {
allTestsPassing: true,
Expand Down
Loading
Loading