yusufkhon-guard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +734 -0
  3. package/bin/cli.js +39 -0
  4. package/bin/renew.js +149 -0
  5. package/bin/scan.js +124 -0
  6. package/bin/ssl.js +181 -0
  7. package/package.json +80 -0
  8. package/src/index.js +206 -0
  9. package/src/modules/acme/client.js +240 -0
  10. package/src/modules/acme/csr.js +172 -0
  11. package/src/modules/acme/detectDomains.js +132 -0
  12. package/src/modules/acme/index.js +211 -0
  13. package/src/modules/acme/jose.js +125 -0
  14. package/src/modules/acme/renew.js +89 -0
  15. package/src/modules/adaptiveRateLimiter.js +154 -0
  16. package/src/modules/alerts.js +168 -0
  17. package/src/modules/bodyLimit.js +106 -0
  18. package/src/modules/botDetector.js +144 -0
  19. package/src/modules/connectionGuard.js +128 -0
  20. package/src/modules/csrf.js +130 -0
  21. package/src/modules/dashboard.js +257 -0
  22. package/src/modules/dashboardHtml.js +235 -0
  23. package/src/modules/datacenterRanges.js +121 -0
  24. package/src/modules/ddosGuard.js +323 -0
  25. package/src/modules/firewall.js +239 -0
  26. package/src/modules/geoBlock.js +169 -0
  27. package/src/modules/headers.js +130 -0
  28. package/src/modules/honeypot.js +149 -0
  29. package/src/modules/httpsRedirect.js +67 -0
  30. package/src/modules/integrity.js +164 -0
  31. package/src/modules/ipBlocker.js +372 -0
  32. package/src/modules/ja3.js +155 -0
  33. package/src/modules/logExport.js +134 -0
  34. package/src/modules/nosqlGuard.js +143 -0
  35. package/src/modules/osFirewall.js +182 -0
  36. package/src/modules/rateLimiter.js +196 -0
  37. package/src/modules/sanitizer.js +253 -0
  38. package/src/modules/threatDetector.js +202 -0
  39. package/src/scanner/checks/dependencies.js +125 -0
  40. package/src/scanner/checks/environment.js +118 -0
  41. package/src/scanner/checks/exposure.js +99 -0
  42. package/src/scanner/checks/headers.js +119 -0
  43. package/src/scanner/checks/ssl.js +151 -0
  44. package/src/scanner/index.js +209 -0
  45. package/src/utils/cidr.js +140 -0
  46. package/src/utils/logger.js +194 -0
  47. package/src/utils/privilege.js +212 -0
package/src/index.js ADDED
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file index.js
5
+ * @description `yusufkhon-guard` kutubxonasining asosiy kirish nuqtasi.
6
+ * Barcha modullarni eksport qiladi hamda ularni bitta qulay
7
+ * `guard()` initializer orqali birlashtirib beradi.
8
+ * @module yusufkhon-guard
9
+ * @author Yusufkhon
10
+ * @license MIT
11
+ */
12
+
13
+ const securityHeaders = require('./modules/headers');
14
+ const rateLimiter = require('./modules/rateLimiter');
15
+ const sanitizer = require('./modules/sanitizer');
16
+ const firewall = require('./modules/firewall');
17
+ const httpsRedirect = require('./modules/httpsRedirect');
18
+ const threatDetector = require('./modules/threatDetector');
19
+ const osFirewall = require('./modules/osFirewall');
20
+ const dashboard = require('./modules/dashboard');
21
+ const acme = require('./modules/acme');
22
+ const geoBlock = require('./modules/geoBlock');
23
+ const datacenterRanges = require('./modules/datacenterRanges');
24
+ const botDetector = require('./modules/botDetector');
25
+ const ja3 = require('./modules/ja3');
26
+ const adaptiveRateLimiter = require('./modules/adaptiveRateLimiter');
27
+ const honeypot = require('./modules/honeypot');
28
+ const nosqlGuard = require('./modules/nosqlGuard');
29
+ const csrf = require('./modules/csrf');
30
+ const bodyLimit = require('./modules/bodyLimit');
31
+ const ddosGuard = require('./modules/ddosGuard');
32
+ const connectionGuard = require('./modules/connectionGuard');
33
+ const { IntegrityMonitor } = require('./modules/integrity');
34
+ const { Alerter } = require('./modules/alerts');
35
+ const { RotatingJsonLogger } = require('./modules/logExport');
36
+ const { IpBlocker } = require('./modules/ipBlocker');
37
+ const { scan, formatReport } = require('./scanner');
38
+ const privilege = require('./utils/privilege');
39
+ const cidr = require('./utils/cidr');
40
+ const { Logger, defaultLogger } = require('./utils/logger');
41
+
42
+ /**
43
+ * @typedef {Object} GuardOptions
44
+ * @property {import('./modules/bodyLimit').BodyLimitOptions} [bodyLimit] - Payload hajmi cheklovi (opsional; berilsa yoqiladi).
45
+ * @property {import('./modules/ddosGuard').DdosGuardOptions|true} [ddos] - L7 DDoS himoyasi (opsional; Attack Mode, concurrency, shedding, challenge).
46
+ * @property {import('./modules/httpsRedirect').HttpsRedirectOptions|false} [https] - HTTPS'ga yo'naltirish (berilmasa — o'chirilgan).
47
+ * @property {import('./modules/headers').HeadersOptions|false} [headers] - Xavfsizlik sarlavhalari (`false` — o'chirilgan).
48
+ * @property {import('./modules/geoBlock').GeoBlockOptions} [geoBlock] - Geo-IP / datacenter / VPN himoyasi (opsional).
49
+ * @property {import('./modules/honeypot').HoneypotOptions|true} [honeypot] - HoneyPot tuzoqlari (opsional; `true` — standart tuzoqlar).
50
+ * @property {import('./modules/botDetector').BotDetectorOptions|true} [botDetector] - Bot/skript aniqlash (opsional).
51
+ * @property {import('./modules/firewall').FirewallOptions|false} [firewall] - "Blue team" aktiv himoya (`false` — o'chirilgan).
52
+ * @property {import('./modules/adaptiveRateLimiter').AdaptiveOptions} [adaptiveRateLimit] - Aqlli rate-limiting (berilsa, oddiy rateLimit o'rniga ishlaydi).
53
+ * @property {import('./modules/rateLimiter').RateLimiterOptions|false} [rateLimit] - Oddiy rate-limiting (`false` — o'chirilgan).
54
+ * @property {import('./modules/csrf').CsrfOptions} [csrf] - CSRF himoyasi (opsional).
55
+ * @property {import('./modules/nosqlGuard').NosqlGuardOptions|true} [nosql] - NoSQL/JSON himoyasi (opsional).
56
+ * @property {import('./modules/sanitizer').SanitizerOptions|false} [sanitize] - Input tozalash (`false` — o'chirilgan).
57
+ * @property {import('./utils/logger').Logger} [logger] - Umumiy logger (barcha modullarga uzatiladi).
58
+ */
59
+
60
+ /**
61
+ * Barcha himoya qatlamlarini bitta massivda qaytaruvchi umumiy initializer.
62
+ * Natijani to'g'ridan-to'g'ri `app.use(...)` ga berish mumkin.
63
+ *
64
+ * Standart holatda faqat 4 ta asosiy modul yoqiladi (headers, firewall,
65
+ * rateLimit, sanitize). Qolganlari (bodyLimit, geoBlock, honeypot, botDetector,
66
+ * adaptiveRateLimit, csrf, nosql, https) — mos sozlama berilgandagina qo'shiladi.
67
+ *
68
+ * @example
69
+ * const guard = require('yusufkhon-guard');
70
+ * app.use(guard({
71
+ * bodyLimit: { maxBodySize: '512kb' },
72
+ * honeypot: true,
73
+ * botDetector:{ block: true },
74
+ * geoBlock: { allowCountries: ['UZ','US'], blockDatacenter: true },
75
+ * csrf: { allowedOrigins: ['https://mysite.uz'] },
76
+ * nosql: true,
77
+ * firewall: { persistPath: './logs/bans.json' },
78
+ * }));
79
+ *
80
+ * @param {GuardOptions} [options={}] - Umumiy sozlamalar.
81
+ * @returns {import('express').RequestHandler[] & { firewall?: import('./modules/firewall').Firewall }} Middleware'lar massivi (firewall nusxasi biriktirilgan).
82
+ */
83
+ function guard(options = {}) {
84
+ const logger = options.logger || defaultLogger;
85
+
86
+ /** @type {import('express').RequestHandler[]} */
87
+ const stack = [];
88
+ let firewallInstance;
89
+
90
+ // Firewall nusxasini oldindan yaratamiz (boshqa modullar unga strike/ban qo'shishi uchun).
91
+ let firewallMw;
92
+ if (options.firewall !== false) {
93
+ firewallMw = firewall({ logger, ...(options.firewall || {}) });
94
+ firewallInstance = firewallMw.firewall;
95
+ }
96
+
97
+ // 0. Payload hajmi cheklovi — body parserdan oldin ishlashi kerak.
98
+ if (options.bodyLimit) {
99
+ stack.push(bodyLimit({ logger, firewall: firewallInstance, ...(options.bodyLimit === true ? {} : options.bodyLimit) }));
100
+ }
101
+
102
+ // 1. HTTPS'ga yo'naltirish.
103
+ if (options.https) {
104
+ stack.push(httpsRedirect(options.https === true ? {} : options.https));
105
+ }
106
+
107
+ // 2. Xavfsizlik sarlavhalari.
108
+ if (options.headers !== false) {
109
+ stack.push(securityHeaders(options.headers || {}));
110
+ }
111
+
112
+ // 2.5. L7 DDoS himoyasi — hujum paytida og'ir ishlardan oldin chetlash/challenge.
113
+ let ddosMw;
114
+ if (options.ddos) {
115
+ ddosMw = ddosGuard({ logger, firewall: firewallInstance, ...(options.ddos === true ? {} : options.ddos) });
116
+ stack.push(ddosMw);
117
+ }
118
+
119
+ // 3. Geo-IP / datacenter / VPN himoyasi.
120
+ if (options.geoBlock) {
121
+ stack.push(geoBlock({ logger, firewall: firewallInstance, ...options.geoBlock }));
122
+ }
123
+
124
+ // 4. HoneyPot tuzoqlari — bloklangan IP'ni darhol to'sadi.
125
+ if (options.honeypot) {
126
+ stack.push(honeypot({ logger, firewall: firewallInstance, osFirewall, ...(options.honeypot === true ? {} : options.honeypot) }));
127
+ }
128
+
129
+ // 5. Bot / skript aniqlash.
130
+ if (options.botDetector) {
131
+ stack.push(botDetector({ logger, firewall: firewallInstance, ...(options.botDetector === true ? {} : options.botDetector) }));
132
+ }
133
+
134
+ // 6. Aktiv himoya (blue team).
135
+ if (firewallMw) stack.push(firewallMw);
136
+
137
+ // 7. Rate limiting — aqlli (adaptive) yoki oddiy.
138
+ if (options.adaptiveRateLimit) {
139
+ stack.push(adaptiveRateLimiter({ logger, firewall: firewallInstance, ...(options.adaptiveRateLimit === true ? {} : options.adaptiveRateLimit) }));
140
+ } else if (options.rateLimit !== false) {
141
+ stack.push(rateLimiter({ logger, ...(options.rateLimit || {}) }));
142
+ }
143
+
144
+ // 8. CSRF himoyasi.
145
+ if (options.csrf) {
146
+ stack.push(csrf({ logger, ...options.csrf }));
147
+ }
148
+
149
+ // 9. NoSQL / JSON himoyasi.
150
+ if (options.nosql) {
151
+ stack.push(nosqlGuard({ logger, ...(options.nosql === true ? {} : options.nosql) }));
152
+ }
153
+
154
+ // 10. Input sanitization.
155
+ if (options.sanitize !== false) {
156
+ stack.push(sanitizer({ logger, ...(options.sanitize || {}) }));
157
+ }
158
+
159
+ // Firewall va DDoS nusxalarini massivga biriktiramiz (hodisa/monitoring uchun).
160
+ if (firewallInstance) {
161
+ Object.defineProperty(stack, 'firewall', { value: firewallInstance, enumerable: false });
162
+ }
163
+ if (ddosMw) {
164
+ Object.defineProperty(stack, 'ddos', { value: ddosMw, enumerable: false });
165
+ }
166
+
167
+ logger.success(`yusufkhon-guard faollashtirildi (${stack.length} ta modul).`);
168
+
169
+ return stack;
170
+ }
171
+
172
+ // Asosiy eksport — `guard` funksiyasi.
173
+ module.exports = guard;
174
+
175
+ // Nomlangan (named) eksportlar — modullardan alohida foydalanish uchun.
176
+ module.exports.guard = guard;
177
+ module.exports.securityHeaders = securityHeaders;
178
+ module.exports.rateLimiter = rateLimiter;
179
+ module.exports.sanitizer = sanitizer;
180
+ module.exports.firewall = firewall;
181
+ module.exports.httpsRedirect = httpsRedirect;
182
+ module.exports.threatDetector = threatDetector;
183
+ module.exports.osFirewall = osFirewall;
184
+ module.exports.dashboard = dashboard;
185
+ module.exports.acme = acme;
186
+ module.exports.geoBlock = geoBlock;
187
+ module.exports.datacenterRanges = datacenterRanges;
188
+ module.exports.botDetector = botDetector;
189
+ module.exports.ja3 = ja3;
190
+ module.exports.adaptiveRateLimiter = adaptiveRateLimiter;
191
+ module.exports.honeypot = honeypot;
192
+ module.exports.nosqlGuard = nosqlGuard;
193
+ module.exports.csrf = csrf;
194
+ module.exports.bodyLimit = bodyLimit;
195
+ module.exports.ddosGuard = ddosGuard;
196
+ module.exports.connectionGuard = connectionGuard;
197
+ module.exports.IntegrityMonitor = IntegrityMonitor;
198
+ module.exports.Alerter = Alerter;
199
+ module.exports.RotatingJsonLogger = RotatingJsonLogger;
200
+ module.exports.IpBlocker = IpBlocker;
201
+ module.exports.scan = scan;
202
+ module.exports.formatReport = formatReport;
203
+ module.exports.privilege = privilege;
204
+ module.exports.cidr = cidr;
205
+ module.exports.Logger = Logger;
206
+ module.exports.logger = defaultLogger;
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file client.js
5
+ * @description ACME v2 (RFC 8555) protokol mijozi. Let's Encrypt bilan
6
+ * hisob yaratish, buyurtma (order) ochish, HTTP-01 challenge'ni
7
+ * bajarish, CSR'ni yakunlash (finalize) va sertifikatni yuklab
8
+ * olish jarayonlarini boshqaradi. Global `fetch` (Node 18+) va
9
+ * ichki `crypto`'dan foydalanadi — zero-dependency.
10
+ * @module modules/acme/client
11
+ */
12
+
13
+ const jose = require('./jose');
14
+ const { defaultLogger } = require('../../utils/logger');
15
+
16
+ /**
17
+ * Let's Encrypt katalog (directory) URL manzillari.
18
+ * @readonly
19
+ */
20
+ const DIRECTORY_URLS = {
21
+ staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
22
+ production: 'https://acme-v02.api.letsencrypt.org/directory',
23
+ };
24
+
25
+ /**
26
+ * Kutish (sleep) yordamchisi.
27
+ * @private
28
+ * @param {number} ms
29
+ * @returns {Promise<void>}
30
+ */
31
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
32
+
33
+ /**
34
+ * ACME v2 mijozi.
35
+ * @class
36
+ */
37
+ class AcmeClient {
38
+ /**
39
+ * @param {Object} options
40
+ * @param {string} [options.directoryUrl] - To'g'ridan-to'g'ri katalog URL'i.
41
+ * @param {'staging'|'production'} [options.environment='staging'] - Muhit (directoryUrl berilmasa).
42
+ * @param {import('crypto').KeyObject} options.accountKey - Hisob yopiq kaliti (ES256).
43
+ * @param {import('../../utils/logger').Logger} [options.logger=defaultLogger] - Logger.
44
+ */
45
+ constructor(options) {
46
+ if (!options || !options.accountKey) {
47
+ throw new Error('AcmeClient: `accountKey` majburiy (jose.generateAccountKey()).');
48
+ }
49
+ this.directoryUrl =
50
+ options.directoryUrl || DIRECTORY_URLS[options.environment || 'staging'];
51
+ this.accountKey = options.accountKey;
52
+ this.publicJwk = jose.publicJwk(
53
+ require('crypto').createPublicKey(options.accountKey)
54
+ );
55
+ this.thumbprint = jose.jwkThumbprint(this.publicJwk);
56
+ this.logger = options.logger || defaultLogger;
57
+
58
+ /** @type {Object|null} */
59
+ this.directory = null;
60
+ /** @type {string|null} */
61
+ this.nonce = null;
62
+ /** @type {string|null} Hisob URL'i (kid). */
63
+ this.kid = null;
64
+ }
65
+
66
+ /**
67
+ * Katalogni yuklaydi (barcha ACME endpoint URL'lari).
68
+ * @returns {Promise<Object>}
69
+ */
70
+ async init() {
71
+ const res = await fetch(this.directoryUrl);
72
+ if (!res.ok) throw new Error(`ACME katalogini yuklab bo'lmadi: HTTP ${res.status}`);
73
+ this.directory = await res.json();
74
+ return this.directory;
75
+ }
76
+
77
+ /**
78
+ * Yangi Replay-Nonce oladi.
79
+ * @returns {Promise<string>}
80
+ */
81
+ async fetchNonce() {
82
+ const res = await fetch(this.directory.newNonce, { method: 'HEAD' });
83
+ const nonce = res.headers.get('replay-nonce');
84
+ if (!nonce) throw new Error('Replay-Nonce olinmadi.');
85
+ this.nonce = nonce;
86
+ return nonce;
87
+ }
88
+
89
+ /**
90
+ * Imzolangan (JWS) POST so'rovini yuboradi. Nonce'ni avtomat boshqaradi.
91
+ * @private
92
+ * @param {string} url - ACME URL.
93
+ * @param {Object|string} payload - Yuk (POST-as-GET uchun "").
94
+ * @returns {Promise<{ status: number, headers: Headers, body: any, text: string }>}
95
+ */
96
+ async signedRequest(url, payload) {
97
+ if (!this.nonce) await this.fetchNonce();
98
+
99
+ const jws = jose.signedJws({
100
+ privateKey: this.accountKey,
101
+ url,
102
+ nonce: this.nonce,
103
+ payload,
104
+ kid: this.kid || undefined,
105
+ jwk: this.kid ? undefined : this.publicJwk,
106
+ });
107
+
108
+ const res = await fetch(url, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/jose+json' },
111
+ body: jws,
112
+ });
113
+
114
+ // Har bir javob yangi nonce beradi.
115
+ const newNonce = res.headers.get('replay-nonce');
116
+ if (newNonce) this.nonce = newNonce;
117
+
118
+ const text = await res.text();
119
+ let body;
120
+ const ct = res.headers.get('content-type') || '';
121
+ if (ct.includes('json')) {
122
+ try { body = JSON.parse(text); } catch { body = text; }
123
+ } else {
124
+ body = text;
125
+ }
126
+
127
+ if (res.status >= 400) {
128
+ const detail = body && body.detail ? body.detail : text;
129
+ const err = new Error(`ACME xato (${res.status}): ${detail}`);
130
+ err.status = res.status;
131
+ err.body = body;
132
+ throw err;
133
+ }
134
+
135
+ return { status: res.status, headers: res.headers, body, text };
136
+ }
137
+
138
+ /**
139
+ * Yangi hisob yaratadi (yoki mavjudini qaytaradi) va `kid`ni o'rnatadi.
140
+ * @param {Object} params
141
+ * @param {string} [params.email] - Bog'lanish uchun email.
142
+ * @param {boolean} [params.termsAgreed=true] - Xizmat shartlariga rozilik.
143
+ * @returns {Promise<string>} Hisob URL'i (kid).
144
+ */
145
+ async createAccount({ email, termsAgreed = true } = {}) {
146
+ const payload = { termsOfServiceAgreed: termsAgreed };
147
+ if (email) payload.contact = [`mailto:${email}`];
148
+
149
+ const res = await this.signedRequest(this.directory.newAccount, payload);
150
+ this.kid = res.headers.get('location');
151
+ if (!this.kid) throw new Error('Hisob URL (kid) olinmadi.');
152
+ this.logger.success('ACME hisob tayyor.');
153
+ return this.kid;
154
+ }
155
+
156
+ /**
157
+ * Yangi sertifikat buyurtmasini ochadi.
158
+ * @param {string[]} domains
159
+ * @returns {Promise<{ order: Object, orderUrl: string }>}
160
+ */
161
+ async createOrder(domains) {
162
+ const identifiers = domains.map((value) => ({ type: 'dns', value }));
163
+ const res = await this.signedRequest(this.directory.newOrder, { identifiers });
164
+ return { order: res.body, orderUrl: res.headers.get('location') };
165
+ }
166
+
167
+ /**
168
+ * Avtorizatsiya (authorization) obyektini oladi.
169
+ * @param {string} url
170
+ * @returns {Promise<Object>}
171
+ */
172
+ async getAuthorization(url) {
173
+ const res = await this.signedRequest(url, '');
174
+ return res.body;
175
+ }
176
+
177
+ /**
178
+ * HTTP-01 challenge uchun kalit avtorizatsiyasini hisoblaydi.
179
+ * @param {string} token
180
+ * @returns {string} `token.thumbprint`.
181
+ */
182
+ keyAuthorization(token) {
183
+ return `${token}.${this.thumbprint}`;
184
+ }
185
+
186
+ /**
187
+ * Challenge'ni "tayyor" deb belgilaydi (ACME'ni tekshirishga undaydi).
188
+ * @param {string} challengeUrl
189
+ * @returns {Promise<Object>}
190
+ */
191
+ async completeChallenge(challengeUrl) {
192
+ const res = await this.signedRequest(challengeUrl, {});
193
+ return res.body;
194
+ }
195
+
196
+ /**
197
+ * Resurs holati kerakli qiymatga yetguncha (yoki xato bo'lguncha) so'rab turadi.
198
+ * @param {string} url - Kuzatiladigan resurs URL'i.
199
+ * @param {string[]} until - Kutilayotgan holatlar (masalan, ['valid']).
200
+ * @param {Object} [opts]
201
+ * @param {number} [opts.tries=15] - Urinishlar soni.
202
+ * @param {number} [opts.intervalMs=2000] - Urinishlar orasidagi kutish.
203
+ * @returns {Promise<Object>}
204
+ */
205
+ async poll(url, until, { tries = 15, intervalMs = 2000 } = {}) {
206
+ for (let i = 0; i < tries; i++) {
207
+ const res = await this.signedRequest(url, '');
208
+ const status = res.body && res.body.status;
209
+ if (until.includes(status)) return res.body;
210
+ if (status === 'invalid') {
211
+ throw new Error(`ACME holati "invalid": ${JSON.stringify(res.body)}`);
212
+ }
213
+ await sleep(intervalMs);
214
+ }
215
+ throw new Error(`ACME holati kutilgan (${until.join('/')}) qiymatga yetmadi: ${url}`);
216
+ }
217
+
218
+ /**
219
+ * Buyurtmani CSR bilan yakunlaydi.
220
+ * @param {string} finalizeUrl
221
+ * @param {string} csrDerBase64Url
222
+ * @returns {Promise<Object>}
223
+ */
224
+ async finalizeOrder(finalizeUrl, csrDerBase64Url) {
225
+ const res = await this.signedRequest(finalizeUrl, { csr: csrDerBase64Url });
226
+ return res.body;
227
+ }
228
+
229
+ /**
230
+ * Yakuniy sertifikat zanjirini (PEM) yuklab oladi.
231
+ * @param {string} certificateUrl
232
+ * @returns {Promise<string>} PEM sertifikat zanjiri.
233
+ */
234
+ async downloadCertificate(certificateUrl) {
235
+ const res = await this.signedRequest(certificateUrl, '');
236
+ return typeof res.body === 'string' ? res.body : res.text;
237
+ }
238
+ }
239
+
240
+ module.exports = { AcmeClient, DIRECTORY_URLS };
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file csr.js
5
+ * @description Minimal ASN.1 DER kodlagich va PKCS#10 (CSR) quruvchisi.
6
+ * Node.js `crypto` yordamida ECDSA P-256 kalit yaratadi va Subject
7
+ * Alternative Name (SAN) li sertifikat so'rovnomasini (CSR) tuzadi.
8
+ * ACME `finalize` bosqichida base64url(DER) ko'rinishida yuboriladi.
9
+ *
10
+ * Ochiq kalit (SubjectPublicKeyInfo) to'g'ridan-to'g'ri Node'ning
11
+ * `spki` DER eksportidan olinadi — shu bois EC nuqtasini qo'lda
12
+ * kodlash shart emas, faqat CSR "qobig'i" ASN.1 bilan yig'iladi.
13
+ * @module modules/acme/csr
14
+ */
15
+
16
+ const crypto = require('crypto');
17
+
18
+ // --- Minimal ASN.1 DER kodlagich ---
19
+
20
+ /**
21
+ * DER uzunlik (length) maydonini kodlaydi.
22
+ * @private
23
+ * @param {number} len
24
+ * @returns {Buffer}
25
+ */
26
+ function encodeLength(len) {
27
+ if (len < 0x80) return Buffer.from([len]);
28
+ const bytes = [];
29
+ let n = len;
30
+ while (n > 0) {
31
+ bytes.unshift(n & 0xff);
32
+ n >>= 8;
33
+ }
34
+ return Buffer.from([0x80 | bytes.length, ...bytes]);
35
+ }
36
+
37
+ /**
38
+ * Tag + uzunlik + kontentdan TLV (Type-Length-Value) tuzadi.
39
+ * @private
40
+ * @param {number} tag
41
+ * @param {Buffer} content
42
+ * @returns {Buffer}
43
+ */
44
+ function tlv(tag, content) {
45
+ return Buffer.concat([Buffer.from([tag]), encodeLength(content.length), content]);
46
+ }
47
+
48
+ /** SEQUENCE (0x30). @param {...Buffer} parts @returns {Buffer} */
49
+ const seq = (...parts) => tlv(0x30, Buffer.concat(parts));
50
+ /** SET (0x31). @param {...Buffer} parts @returns {Buffer} */
51
+ const set = (...parts) => tlv(0x31, Buffer.concat(parts));
52
+ /** INTEGER (0x02). @param {number} n @returns {Buffer} */
53
+ const int = (n) => tlv(0x02, Buffer.from([n]));
54
+ /** UTF8String (0x0c). @param {string} s @returns {Buffer} */
55
+ const utf8 = (s) => tlv(0x0c, Buffer.from(s, 'utf8'));
56
+ /** OCTET STRING (0x04). @param {Buffer} b @returns {Buffer} */
57
+ const octet = (b) => tlv(0x04, b);
58
+
59
+ /**
60
+ * Nuqtali OID matnini DER baytlariga kodlaydi.
61
+ * @private
62
+ * @param {string} oidStr - Masalan, "1.2.840.10045.4.3.2".
63
+ * @returns {Buffer}
64
+ */
65
+ function oid(oidStr) {
66
+ const parts = oidStr.split('.').map(Number);
67
+ const first = 40 * parts[0] + parts[1];
68
+ const body = [first];
69
+ for (let i = 2; i < parts.length; i++) {
70
+ let val = parts[i];
71
+ const stack = [val & 0x7f];
72
+ val >>= 7;
73
+ while (val > 0) {
74
+ stack.unshift((val & 0x7f) | 0x80);
75
+ val >>= 7;
76
+ }
77
+ body.push(...stack);
78
+ }
79
+ return tlv(0x06, Buffer.from(body));
80
+ }
81
+
82
+ // --- Ma'lum OID'lar ---
83
+ const OID_CN = '2.5.4.3'; // commonName
84
+ const OID_EXTENSION_REQUEST = '1.2.840.113549.1.9.14';
85
+ const OID_SAN = '2.5.29.17'; // subjectAltName
86
+ const OID_ECDSA_SHA256 = '1.2.840.10045.4.3.2';
87
+
88
+ /**
89
+ * SubjectAltName kengaytmasi qiymatini (extnValue) tuzadi.
90
+ * @private
91
+ * @param {string[]} domains
92
+ * @returns {Buffer} DER SEQUENCE OF GeneralName.
93
+ */
94
+ function buildSanValue(domains) {
95
+ // dNSName ::= [2] IMPLICIT IA5String -> kontekst primitiv tag 0x82.
96
+ const names = domains.map((d) => tlv(0x82, Buffer.from(d, 'ascii')));
97
+ return seq(...names);
98
+ }
99
+
100
+ /**
101
+ * PKCS#10 uchun attributes ([0] IMPLICIT) — extensionRequest (SAN) bilan.
102
+ * @private
103
+ * @param {string[]} domains
104
+ * @returns {Buffer}
105
+ */
106
+ function buildAttributes(domains) {
107
+ const sanExtension = seq(oid(OID_SAN), octet(buildSanValue(domains)));
108
+ const extensions = seq(sanExtension); // SEQUENCE OF Extension
109
+ const attribute = seq(oid(OID_EXTENSION_REQUEST), set(extensions));
110
+ // [0] IMPLICIT SET OF Attribute -> kontekst konstruktiv tag 0xA0.
111
+ return tlv(0xa0, attribute);
112
+ }
113
+
114
+ /**
115
+ * @typedef {Object} CsrResult
116
+ * @property {string} csrDerBase64Url - ACME `finalize` uchun base64url(DER).
117
+ * @property {Buffer} csrDer - Xom DER.
118
+ * @property {string} csrPem - PEM ko'rinish (tekshirish/saqlash uchun).
119
+ * @property {string} privateKeyPem - Sertifikatning yopiq kaliti (PKCS#8 PEM).
120
+ * @property {crypto.KeyObject} privateKey - Yopiq kalit obyekti.
121
+ */
122
+
123
+ /**
124
+ * Berilgan domenlar uchun ECDSA P-256 kalit yaratadi va SAN'li CSR tuzadi.
125
+ *
126
+ * @param {string[]} domains - Sertifikat qamrab oladigan domenlar (kamida bitta).
127
+ * @returns {CsrResult}
128
+ */
129
+ function createCsr(domains) {
130
+ if (!Array.isArray(domains) || domains.length === 0) {
131
+ throw new Error('createCsr(): kamida bitta domen kerak.');
132
+ }
133
+
134
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
135
+
136
+ // Subject: CN = birinchi domen.
137
+ const subject = seq(set(seq(oid(OID_CN), utf8(domains[0]))));
138
+
139
+ // SubjectPublicKeyInfo — Node'ning tayyor SPKI DER eksporti.
140
+ const spki = publicKey.export({ type: 'spki', format: 'der' });
141
+
142
+ // CertificationRequestInfo ::= SEQ { version(0), subject, SPKI, [0] attributes }
143
+ const cri = seq(int(0), subject, spki, buildAttributes(domains));
144
+
145
+ // CRI ni domen kaliti bilan imzolaymiz (ECDSA-SHA256, DER imzo).
146
+ const signature = crypto.sign('SHA256', cri, { key: privateKey, dsaEncoding: 'der' });
147
+
148
+ const sigAlg = seq(oid(OID_ECDSA_SHA256));
149
+ // BIT STRING: birinchi bayt — ishlatilmagan bitlar soni (0).
150
+ const sigBitString = tlv(0x03, Buffer.concat([Buffer.from([0x00]), signature]));
151
+
152
+ const csrDer = seq(cri, sigAlg, sigBitString);
153
+
154
+ const csrPem =
155
+ '-----BEGIN CERTIFICATE REQUEST-----\n' +
156
+ (csrDer.toString('base64').match(/.{1,64}/g) || []).join('\n') +
157
+ '\n-----END CERTIFICATE REQUEST-----\n';
158
+
159
+ return {
160
+ csrDerBase64Url: csrDer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
161
+ csrDer,
162
+ csrPem,
163
+ privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
164
+ privateKey,
165
+ };
166
+ }
167
+
168
+ module.exports = {
169
+ createCsr,
170
+ // Ichki yordamchilar (test uchun ochiq).
171
+ _internal: { encodeLength, tlv, seq, set, int, utf8, octet, oid, buildSanValue },
172
+ };