Skip to content

Commit 92b723a

Browse files
committed
feat(new tool): CSR Generator
Fix CorentinTh#586 and CorentinTh#736
1 parent e073b2b commit 92b723a

File tree

4 files changed

+234
-1
lines changed

4 files changed

+234
-1
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { pki } from 'node-forge';
2+
import workerScript from 'node-forge/dist/prime.worker.min?url';
3+
4+
export { generateCSR };
5+
6+
function generateRSAPairs({ bits = 2048 }) {
7+
return new Promise<pki.rsa.KeyPair>((resolve, reject) =>
8+
pki.rsa.generateKeyPair({ bits, workerScript }, (err, keyPair) => {
9+
if (err) {
10+
reject(err);
11+
return;
12+
}
13+
14+
resolve(keyPair);
15+
}),
16+
);
17+
}
18+
19+
async function generateCSR(config: {
20+
bits?: number
21+
password?: string
22+
commonName?: string
23+
countryName?: string
24+
city?: string
25+
state?: string
26+
organizationName?: string
27+
organizationalUnit?: string
28+
contactEmail?: string
29+
} = {}): Promise<{
30+
publicKeyPem: string
31+
privateKeyPem: string
32+
csrPem: string
33+
}> {
34+
const { privateKey, publicKey } = await generateRSAPairs(config);
35+
36+
// create a certification request (CSR)
37+
const csr = pki.createCertificationRequest();
38+
csr.publicKey = publicKey;
39+
csr.setSubject([{
40+
name: 'CN',
41+
value: config.commonName,
42+
}, {
43+
name: 'C',
44+
value: config.countryName,
45+
}, {
46+
shortName: 'ST',
47+
value: config.state,
48+
}, {
49+
name: 'L',
50+
value: config.city,
51+
}, {
52+
name: 'O',
53+
value: config.organizationName,
54+
}, {
55+
shortName: 'OU',
56+
value: config.organizationalUnit,
57+
}, {
58+
name: 'EMAIL',
59+
value: config.contactEmail,
60+
}]);
61+
62+
// sign certification request
63+
csr.sign(privateKey);
64+
65+
// convert certification request to PEM-format
66+
const csrPem = pki.certificationRequestToPem(csr);
67+
68+
const privateUnencryptedKeyPem = pki.privateKeyToPem(privateKey);
69+
70+
return {
71+
csrPem,
72+
publicKeyPem: pki.publicKeyToPem(publicKey),
73+
privateKeyPem: config?.password
74+
? pki.encryptRsaPrivateKey(privateKey, config?.password)
75+
: privateUnencryptedKeyPem,
76+
};
77+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<script setup lang="ts">
2+
import TextareaCopyable from '@/components/TextareaCopyable.vue';
3+
import { withDefaultOnErrorAsync } from '@/utils/defaults';
4+
import { computedRefreshableAsync } from '@/composable/computedRefreshable';
5+
import { generateCSR } from "./csr-generator.service.ts";
6+
7+
const commonName = ref('');
8+
const organizationName = ref('');
9+
const organizationUnit = ref('');
10+
const city = ref('');
11+
const state = ref('');
12+
const country = ref('');
13+
const contactEmail = ref('');
14+
const emptyCSR = { csrPem: '', privateKeyPem: '', publicKeyPem: '' };
15+
16+
const [certs, refreshCerts] = computedRefreshableAsync(
17+
() => withDefaultOnErrorAsync(() => {
18+
return generateCSR({
19+
password: password.value,
20+
commonName: commonName.value,
21+
countryName: country.value,
22+
city: city.value,
23+
state: state.value,
24+
organizationName: organizationName.value,
25+
organizationalUnit: organizationalUnit.value,
26+
contactEmail: contactEmail.value,
27+
});
28+
},
29+
), emptyCSR),
30+
emptyCSR,
31+
);
32+
</script>
33+
34+
<template>
35+
<div>
36+
<n-form-item label="Private Key passphrase:" label-placement="left">
37+
<n-input
38+
v-model:value="password"
39+
type="password"
40+
show-password-on="mousedown"
41+
placeholder="Passphrase"
42+
/>
43+
</n-form-item>
44+
</div>
45+
46+
<div>
47+
<n-form-item label="Common Name/Domain Name:" label-placement="left">
48+
<n-input
49+
v-model:value="commonName"
50+
placeholder="Common/Domain Name"
51+
/>
52+
</n-form-item>
53+
</div>
54+
55+
<div>
56+
<n-form-item label="Organization Name:" label-placement="left">
57+
<n-input
58+
v-model:value="organizationName"
59+
placeholder="Organization Name"
60+
/>
61+
</n-form-item>
62+
</div>
63+
64+
<div>
65+
<n-form-item label="Organization Unit:" label-placement="left">
66+
<n-input
67+
v-model:value="organizationalUnit"
68+
placeholder="Organization Unit"
69+
/>
70+
</n-form-item>
71+
</div>
72+
73+
<div>
74+
<n-form-item label="State:" label-placement="left">
75+
<n-input
76+
v-model:value="state"
77+
placeholder="State"
78+
/>
79+
</n-form-item>
80+
</div>
81+
82+
<div>
83+
<n-form-item label="City:" label-placement="left">
84+
<n-input
85+
v-model:value="city"
86+
placeholder="City"
87+
/>
88+
</n-form-item>
89+
</div>
90+
91+
<div>
92+
<n-form-item label="Country:" label-placement="left">
93+
<n-input
94+
v-model:value="country"
95+
placeholder="Country"
96+
/>
97+
</n-form-item>
98+
</div>
99+
100+
<div>
101+
<n-form-item label="Contact Email:" label-placement="left">
102+
<n-input
103+
v-model:value="contactEmail"
104+
placeholder="Contact Email"
105+
/>
106+
</n-form-item>
107+
</div>
108+
109+
<div>
110+
<c-button @click="refreshCerts">
111+
Refresh CSR
112+
</c-button>
113+
</div>
114+
115+
<div>
116+
<h3>Certifacate Signing Request</h3>
117+
<TextareaCopyable :value="certs.csrPem" />
118+
</div>
119+
120+
<div>
121+
<h3>Public key</h3>
122+
<TextareaCopyable :value="certs.publicKeyPem" word-wrap="true" />
123+
</div>
124+
125+
<div>
126+
<h3>Private key</h3>
127+
<TextareaCopyable :value="certs.privateKeyPem" />
128+
</div>
129+
</div>
130+
</template>

src/tools/csr-generator/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ArrowsShuffle } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Csr generator',
6+
path: '/csr-generator',
7+
description: '',
8+
keywords: ['csr', 'certificate', 'signing', 'request', 'x509', 'generator'],
9+
component: () => import('./csr-generator.vue'),
10+
icon: ArrowsShuffle,
11+
createdAt: new Date('2024-02-22'),
12+
});

src/tools/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter';
22
import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
44
import { tool as textToUnicode } from './text-to-unicode';
5+
import { tool as csrGenerator } from './csr-generator';
56
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
67
import { tool as numeronymGenerator } from './numeronym-generator';
78
import { tool as macAddressGenerator } from './mac-address-generator';
@@ -81,7 +82,20 @@ import { tool as yamlViewer } from './yaml-viewer';
8182
export const toolsByCategory: ToolCategory[] = [
8283
{
8384
name: 'Crypto',
84-
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker],
85+
components: [
86+
tokenGenerator,
87+
hashText,
88+
bcrypt,
89+
uuidGenerator,
90+
ulidGenerator,
91+
cypher,
92+
bip39,
93+
hmacGenerator,
94+
rsaKeyPairGenerator,
95+
csrGenerator,
96+
passwordStrengthAnalyser,
97+
pdfSignatureChecker,
98+
],
8599
},
86100
{
87101
name: 'Converter',

0 commit comments

Comments
 (0)