Skip to content

Commit 566a633

Browse files
committed
add Certificate Signing Request (CSR) parse action
1 parent 1bc8872 commit 566a633

File tree

4 files changed

+486
-1
lines changed

4 files changed

+486
-1
lines changed

src/core/config/Categories.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@
161161
"RSA Verify",
162162
"RSA Encrypt",
163163
"RSA Decrypt",
164-
"Parse SSH Host Key"
164+
"Parse SSH Host Key",
165+
"Parse CSR"
165166
]
166167
},
167168
{

src/core/operations/ParseCSR.mjs

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/**
2+
* @author jkataja
3+
* @copyright Crown Copyright 2023
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import forge from "node-forge";
9+
import Utils from "../Utils.mjs";
10+
11+
/**
12+
* Parse CSR operation
13+
*/
14+
class ParseCSR extends Operation {
15+
16+
/**
17+
* ParseCSR constructor
18+
*/
19+
constructor() {
20+
super();
21+
22+
this.name = "Parse CSR";
23+
this.module = "PublicKey";
24+
this.description = "Parse Certificate Signing Request (CSR) for an X.509 certificate";
25+
this.infoURL = "https://en.wikipedia.org/wiki/Certificate_signing_request";
26+
this.inputType = "string";
27+
this.outputType = "string";
28+
this.args = [
29+
{
30+
"name": "Input format",
31+
"type": "option",
32+
"value": ["PEM"]
33+
},
34+
{
35+
"name": "Strict ASN.1 value lengths",
36+
"type": "boolean",
37+
"value": true
38+
}
39+
];
40+
this.checks = [
41+
{
42+
"pattern": "^-+BEGIN CERTIFICATE REQUEST-+\\r?\\n[\\da-z+/\\n\\r]+-+END CERTIFICATE REQUEST-+\\r?\\n?$",
43+
"flags": "i",
44+
"args": ["PEM"]
45+
}
46+
];
47+
}
48+
49+
/**
50+
* @param {string} input
51+
* @param {Object[]} args
52+
* @returns {string} Human-readable description of a Certificate Signing Request (CSR).
53+
*/
54+
run(input, args) {
55+
if (!input.length) {
56+
return "No input";
57+
}
58+
59+
const csr = forge.pki.certificationRequestFromPem(input, args[1]);
60+
61+
// RSA algorithm is the only one supported for CSR in node-forge as of 1.3.1
62+
return `Version: ${1 + csr.version} (0x${Utils.hex(csr.version)})
63+
Subject${formatSubject(csr.subject)}
64+
Subject Alternative Names${formatSubjectAlternativeNames(csr)}
65+
Public Key
66+
Algorithm: RSA
67+
Length: ${csr.publicKey.n.bitLength()} bits
68+
Modulus: ${formatMultiLine(chop(csr.publicKey.n.toString(16).replace(/(..)/g, "$&:")))}
69+
Exponent: ${csr.publicKey.e} (0x${Utils.hex(csr.publicKey.e)})
70+
Signature
71+
Algorithm: ${forge.pki.oids[csr.signatureOid]}
72+
Signature: ${formatMultiLine(Utils.strToByteArray(csr.signature).map(b => Utils.hex(b)).join(":"))}
73+
Extensions${formatExtensions(csr)}`;
74+
}
75+
}
76+
77+
/**
78+
* Format Subject of the request as a multi-line string
79+
* @param {*} subject CSR Subject
80+
* @returns Multi-line string describing Subject
81+
*/
82+
function formatSubject(subject) {
83+
let out = "\n";
84+
85+
for (const attribute of subject.attributes) {
86+
out += ` ${attribute.shortName} = ${attribute.value}\n`;
87+
}
88+
89+
return chop(out);
90+
}
91+
92+
93+
/**
94+
* Format Subject Alternative Names from the name `subjectAltName` extension
95+
* @param {*} extension CSR object
96+
* @returns Multi-line string describing Subject Alternative Names
97+
*/
98+
function formatSubjectAlternativeNames(csr) {
99+
let out = "\n";
100+
101+
for (const attribute of csr.attributes) {
102+
for (const extension of attribute.extensions) {
103+
if (extension.name === "subjectAltName") {
104+
const names = [];
105+
for (const altName of extension.altNames) {
106+
switch (altName.type) {
107+
case 1:
108+
names.push(`EMAIL: ${altName.value}`);
109+
break;
110+
case 2:
111+
names.push(`DNS: ${altName.value}`);
112+
break;
113+
case 6:
114+
names.push(`URI: ${altName.value}`);
115+
break;
116+
case 7:
117+
names.push(`IP: ${altName.ip}`);
118+
break;
119+
default:
120+
names.push(`(unable to format type ${altName.type} name)\n`);
121+
}
122+
}
123+
out += indent(2, names);
124+
}
125+
}
126+
}
127+
128+
return chop(out);
129+
}
130+
131+
/**
132+
* Format known extensions of a CSR
133+
* @param {*} csr CSR object
134+
* @returns Multi-line string describing attributes
135+
*/
136+
function formatExtensions(csr) {
137+
let out = "\n";
138+
139+
for (const attribute of csr.attributes) {
140+
for (const extension of attribute.extensions) {
141+
// formatted separately
142+
if (extension.name === "subjectAltName") {
143+
continue;
144+
}
145+
out += ` ${extension.name}${(extension.critical ? " CRITICAL" : "")}:\n`;
146+
let parts = [];
147+
switch (extension.name) {
148+
case "basicConstraints" :
149+
parts = describeBasicConstraints(extension);
150+
break;
151+
case "keyUsage" :
152+
parts = describeKeyUsage(extension);
153+
break;
154+
case "extKeyUsage" :
155+
parts = describeExtendedKeyUsage(extension);
156+
break;
157+
default :
158+
parts = ["(unable to format extension)"];
159+
}
160+
out += indent(4, parts);
161+
}
162+
}
163+
164+
return chop(out);
165+
}
166+
167+
168+
/**
169+
* Format hex string onto multiple lines
170+
* @param {*} longStr
171+
* @returns Hex string as a multi-line hex string
172+
*/
173+
function formatMultiLine(longStr) {
174+
const lines = [];
175+
176+
for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) {
177+
lines.push(remain.substring(0, 48));
178+
}
179+
180+
return lines.join("\n ");
181+
}
182+
183+
/**
184+
* Describe Basic Constraints
185+
* @see RFC 5280 4.2.1.9. Basic Constraints https://www.ietf.org/rfc/rfc5280.txt
186+
* @param {*} extension CSR extension with the name `basicConstraints`
187+
* @returns Array of strings describing Basic Constraints
188+
*/
189+
function describeBasicConstraints(extension) {
190+
const constraints = [];
191+
192+
constraints.push(`CA = ${extension.cA}`);
193+
if (extension.pathLenConstraint !== undefined) constraints.push(`PathLenConstraint = ${extension.pathLenConstraint}`);
194+
195+
return constraints;
196+
}
197+
198+
/**
199+
* Describe Key Usage extension permitted use cases
200+
* @see RFC 5280 4.2.1.3. Key Usage https://www.ietf.org/rfc/rfc5280.txt
201+
* @param {*} extension CSR extension with the name `keyUsage`
202+
* @returns Array of strings describing Key Usage extension permitted use cases
203+
*/
204+
function describeKeyUsage(extension) {
205+
const usage = [];
206+
207+
if (extension.digitalSignature) usage.push("Digital signature");
208+
if (extension.nonRepudiation) usage.push("Non-repudiation");
209+
if (extension.keyEncipherment) usage.push("Key encipherment");
210+
if (extension.dataEncipherment) usage.push("Data encipherment");
211+
if (extension.keyAgreement) usage.push("Key agreement");
212+
if (extension.keyCertSign) usage.push("Key certificate signing");
213+
if (extension.cRLSign) usage.push("CRL signing");
214+
if (extension.encipherOnly) usage.push("Encipher only");
215+
if (extension.decipherOnly) usage.push("Decipher only");
216+
217+
if (usage.length === 0) usage.push("(none)");
218+
219+
return usage;
220+
}
221+
222+
/**
223+
* Describe Extended Key Usage extension permitted use cases
224+
* @see RFC 5280 4.2.1.12. Extended Key Usage https://www.ietf.org/rfc/rfc5280.txt
225+
* @param {*} extension CSR extension with the name `extendedKeyUsage`
226+
* @returns Array of strings describing Extended Key Usage extension permitted use cases
227+
*/
228+
function describeExtendedKeyUsage(extension) {
229+
const usage = [];
230+
231+
if (extension.serverAuth) usage.push("TLS Web Server Authentication");
232+
if (extension.clientAuth) usage.push("TLS Web Client Authentication");
233+
if (extension.codeSigning) usage.push("Code signing");
234+
if (extension.emailProtection) usage.push("E-mail Protection (S/MIME)");
235+
if (extension.timeStamping) usage.push("Trusted Timestamping");
236+
if (extension.msCodeInd) usage.push("Microsoft Individual Code Signing");
237+
if (extension.msCodeCom) usage.push("Microsoft Commercial Code Signing");
238+
if (extension.msCTLSign) usage.push("Microsoft Trust List Signing");
239+
if (extension.msSGC) usage.push("Microsoft Server Gated Crypto");
240+
if (extension.msEFS) usage.push("Microsoft Encrypted File System");
241+
if (extension.nsSGC) usage.push("Netscape Server Gated Crypto");
242+
243+
if (usage.length === 0) usage.push("(none)");
244+
245+
return usage;
246+
}
247+
248+
/**
249+
* Join an array of strings and add leading spaces to each line.
250+
* @param {*} n How many leading spaces
251+
* @param {*} parts Array of strings
252+
* @returns Joined and indented string.
253+
*/
254+
function indent(n, parts) {
255+
const fluff = " ".repeat(n);
256+
return fluff + parts.join("\n" + fluff) + "\n";
257+
}
258+
259+
/**
260+
* Remove last character from a string.
261+
* @param {*} s String
262+
* @returns Chopped string.
263+
*/
264+
function chop(s) {
265+
return s.substring(0, s.length - 1);
266+
}
267+
268+
export default ParseCSR;

tests/operations/index.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import "./tests/LevenshteinDistance.mjs";
134134
import "./tests/SwapCase.mjs";
135135
import "./tests/HKDF.mjs";
136136
import "./tests/GenerateDeBruijnSequence.mjs";
137+
import "./tests/ParseCSR.mjs";
137138

138139
// Cannot test operations that use the File type yet
139140
// import "./tests/SplitColourChannels.mjs";

0 commit comments

Comments
 (0)