-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add Certificate Signing Request (CSR) parse action
- Loading branch information
Showing
4 changed files
with
486 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
/** | ||
* @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": "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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.