-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
2 changed files
with
289 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,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; |