Skip to content

Commit e9581dc

Browse files
committed
add Certificate Signing Request (CSR) parse action
1 parent 2efd075 commit e9581dc

File tree

2 files changed

+289
-1
lines changed

2 files changed

+289
-1
lines changed

src/core/config/Categories.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@
160160
"RSA Verify",
161161
"RSA Encrypt",
162162
"RSA Decrypt",
163-
"Parse SSH Host Key"
163+
"Parse SSH Host Key",
164+
"Parse CSR"
164165
]
165166
},
166167
{

src/core/operations/ParseCSR.mjs

+287
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/**
2+
* @author jkataja [none]
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+
/**
51+
* @param {string} input
52+
* @param {Object[]} args
53+
* @returns {string} Human-readable description of a Certificate Signing Request (CSR).
54+
*/
55+
run(input, args) {
56+
if (!input.length) {
57+
return "No input";
58+
}
59+
60+
const csr = forge.pki.certificationRequestFromPem(input, args[1]);
61+
62+
// RSA algorithm is the only one supported for CSR in node-forge as of 1.3.1
63+
return `Version: ${1 + csr.version} (0x${Utils.hex(csr.version)})
64+
65+
Subject:
66+
${formatSubject(csr.subject)}
67+
68+
Attributes:
69+
${formatAttributes(csr)}
70+
71+
Public Key:
72+
Key Size: ${csr.publicKey.n.bitLength()} bits
73+
Modulus:
74+
${formatMultiLine(chop(csr.publicKey.n.toString(16).replace(/(..)/g, "$&:")))}
75+
Exponent: ${csr.publicKey.e} (0x${Utils.hex(csr.publicKey.e)})
76+
77+
Signature:
78+
Algorithm ID: ${forge.pki.oids[csr.signatureOid]}
79+
Signature Value:
80+
${formatSignature(csr.signature)}
81+
`;
82+
}
83+
}
84+
85+
const SUBJECT_PRETTY = new Map();
86+
87+
SUBJECT_PRETTY.set("E", "Email Address");
88+
SUBJECT_PRETTY.set("CN", "Common Name");
89+
SUBJECT_PRETTY.set("C", "Country");
90+
SUBJECT_PRETTY.set("L", "Locality");
91+
SUBJECT_PRETTY.set("ST", "State or Province");
92+
SUBJECT_PRETTY.set("O", "Organization");
93+
SUBJECT_PRETTY.set("OU", "Organizational Unit");
94+
95+
/**
96+
* Format Subject of the request as a multi-line string
97+
* @param {*} subject CSR Subject
98+
* @returns Multi-line string describing Subject
99+
*/
100+
function formatSubject(subject) {
101+
let out = "";
102+
103+
for (const key of SUBJECT_PRETTY.keys()) {
104+
if (subject.getField(key)) {
105+
out += ` ${SUBJECT_PRETTY.get(key)} (${key}): `.padEnd(28) + subject.getField(key).value + "\n";
106+
}
107+
}
108+
109+
return chop(out);
110+
}
111+
112+
/**
113+
* Format known attributes of a CSR
114+
* @param {*} csr CSR object
115+
* @returns Multi-line string describing attributes
116+
*/
117+
function formatAttributes(csr) {
118+
let out = "";
119+
120+
for (const attribute of csr.attributes) {
121+
switch (attribute.name) {
122+
case "extensionRequest" :
123+
out += ` Extensions:\n`;
124+
for (const extension of attribute.extensions) {
125+
const criticality = (extension.critical ? " critical" : "");
126+
switch (extension.name) {
127+
case "basicConstraints" :
128+
out += ` Constraints:${criticality}\n ${describeBasicConstraints(extension).join("\n ")}\n`;
129+
break;
130+
case "keyUsage" :
131+
out += ` Key Usage:${criticality}\n ${describeKeyUsage(extension).join("\n ")}\n`;
132+
break;
133+
case "extKeyUsage" :
134+
out += ` Extended Key Usage:${criticality}\n ${describeExtendedKeyUsage(extension).join("\n ")}\n`;
135+
break;
136+
case "subjectAltName" :
137+
out += ` Subject Alternative Names:${criticality}\n ${describeSubjectAlternativeNames(extension).join("\n ")}\n`;
138+
break;
139+
default :
140+
out += ` (unable to format${criticality} "${extension.name}" extension)\n`;
141+
}
142+
}
143+
break;
144+
case "unstructuredName" :
145+
out += ` Unstructured Name: ${attribute.value}\n`;
146+
break;
147+
default:
148+
out += ` (unable to format "${attribute.name}" attribute)\n`;
149+
}
150+
}
151+
152+
return chop(out);
153+
}
154+
155+
/**
156+
* Format signature as a multi-line hex string
157+
* @param {*} signature
158+
* @returns Signature as a multi-line hex string
159+
*/
160+
function formatSignature(signature) {
161+
return formatMultiLine(Utils.strToByteArray(signature).map(b => Utils.hex(b)).join(":"));
162+
}
163+
164+
/**
165+
* Format hex string onto multiple lines
166+
* @param {*} longStr
167+
* @returns Hex string as a multi-line hex string
168+
*/
169+
function formatMultiLine(longStr) {
170+
let out = "";
171+
172+
for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) {
173+
out += ` ${remain.substring(0, 48)}\n`;
174+
}
175+
176+
return chop(out);
177+
}
178+
179+
/**
180+
* Describe Basic Constraints
181+
* @see RFC 5280 4.2.1.9. Basic Constraints https://www.ietf.org/rfc/rfc5280.txt
182+
* @param {*} extension CSR extension with the name `basicConstraints`
183+
* @returns Array of strings describing Basic Constraints
184+
*/
185+
function describeBasicConstraints(extension) {
186+
const constraints = [];
187+
188+
if (extension.cA) constraints.push("Subject is a CA");
189+
else constraints.push("Subject is NOT a CA");
190+
191+
if (extension.pathLenConstraint) constraints.push(`Maximum depth of valid certification paths = ${extension.pathLenConstraint}`);
192+
193+
return constraints;
194+
}
195+
196+
/**
197+
* Describe Key Usage extension permitted use cases
198+
* @see RFC 5280 4.2.1.3. Key Usage https://www.ietf.org/rfc/rfc5280.txt
199+
* @param {*} extension CSR extension with the name `keyUsage`
200+
* @returns Array of strings describing Key Usage extension permitted use cases
201+
*/
202+
function describeKeyUsage(extension) {
203+
const usage = [];
204+
205+
if (extension.digitalSignature) usage.push("Digital signature");
206+
if (extension.nonRepudiation) usage.push("Non-repudiation");
207+
if (extension.keyEncipherment) usage.push("Key encipherment");
208+
if (extension.dataEncipherment) usage.push("Data encipherment");
209+
if (extension.keyAgreement) usage.push("Key agreement");
210+
if (extension.keyCertSign) usage.push("Key certificate signing");
211+
if (extension.cRLSign) usage.push("CRL signing");
212+
if (extension.encipherOnly) usage.push("Encipher only");
213+
if (extension.decipherOnly) usage.push("Decipher only");
214+
215+
if (usage.length === 0) usage.push("(none)");
216+
217+
return usage;
218+
}
219+
220+
/**
221+
* Describe Extended Key Usage extension permitted use cases
222+
* @see RFC 5280 4.2.1.12. Extended Key Usage https://www.ietf.org/rfc/rfc5280.txt
223+
* @param {*} extension CSR extension with the name `extendedKeyUsage`
224+
* @returns Array of strings describing Extended Key Usage extension permitted use cases
225+
*/
226+
function describeExtendedKeyUsage(extension) {
227+
const usage = [];
228+
229+
if (extension.serverAuth) usage.push("TLS Web Server Authentication");
230+
if (extension.clientAuth) usage.push("TLS Web Client Authentication");
231+
if (extension.codeSigning) usage.push("Code signing");
232+
if (extension.emailProtection) usage.push("E-mail Protection (S/MIME)");
233+
if (extension.timeStamping) usage.push("Trusted Timestamping");
234+
if (extension.msCodeInd) usage.push("Microsoft Individual Code Signing");
235+
if (extension.msCodeCom) usage.push("Microsoft Commercial Code Signing");
236+
if (extension.msCTLSign) usage.push("Microsoft Trust List Signing");
237+
if (extension.msSGC) usage.push("Microsoft Server Gated Crypto");
238+
if (extension.msEFS) usage.push("Microsoft Encrypted File System");
239+
if (extension.nsSGC) usage.push("Netscape Server Gated Crypto");
240+
241+
if (usage.length === 0) usage.push("(none)");
242+
243+
return usage;
244+
}
245+
246+
/**
247+
* Describe Subject Alternative Names
248+
* @param {*} extension CSR extension with the name `subjectAltName`
249+
* @returns Array of strings describing Subject Alternative Names
250+
*/
251+
function describeSubjectAlternativeNames(extension) {
252+
const names = [];
253+
254+
for (const altName of extension.altNames) {
255+
switch (altName.type) {
256+
case 1:
257+
names.push(`EMAIL: ${altName.value}`);
258+
break;
259+
case 2:
260+
names.push(`DNS: ${altName.value}`);
261+
break;
262+
case 6:
263+
names.push(`URI: ${altName.value}`);
264+
break;
265+
case 7:
266+
names.push(`IP: ${altName.ip}`);
267+
break;
268+
default:
269+
names.push(`(unable to format type ${altName.type} data)`);
270+
}
271+
}
272+
273+
if (names.length === 0) names.push("(none)");
274+
275+
return names;
276+
}
277+
278+
/**
279+
* Remove last character from a string.
280+
* @param {*} s String
281+
* @returns Chopped string.
282+
*/
283+
function chop(s) {
284+
return s.substring(0, s.length - 1);
285+
}
286+
287+
export default ParseCSR;

0 commit comments

Comments
 (0)