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.
- package/LICENSE +21 -0
- package/README.md +734 -0
- package/bin/cli.js +39 -0
- package/bin/renew.js +149 -0
- package/bin/scan.js +124 -0
- package/bin/ssl.js +181 -0
- package/package.json +80 -0
- package/src/index.js +206 -0
- package/src/modules/acme/client.js +240 -0
- package/src/modules/acme/csr.js +172 -0
- package/src/modules/acme/detectDomains.js +132 -0
- package/src/modules/acme/index.js +211 -0
- package/src/modules/acme/jose.js +125 -0
- package/src/modules/acme/renew.js +89 -0
- package/src/modules/adaptiveRateLimiter.js +154 -0
- package/src/modules/alerts.js +168 -0
- package/src/modules/bodyLimit.js +106 -0
- package/src/modules/botDetector.js +144 -0
- package/src/modules/connectionGuard.js +128 -0
- package/src/modules/csrf.js +130 -0
- package/src/modules/dashboard.js +257 -0
- package/src/modules/dashboardHtml.js +235 -0
- package/src/modules/datacenterRanges.js +121 -0
- package/src/modules/ddosGuard.js +323 -0
- package/src/modules/firewall.js +239 -0
- package/src/modules/geoBlock.js +169 -0
- package/src/modules/headers.js +130 -0
- package/src/modules/honeypot.js +149 -0
- package/src/modules/httpsRedirect.js +67 -0
- package/src/modules/integrity.js +164 -0
- package/src/modules/ipBlocker.js +372 -0
- package/src/modules/ja3.js +155 -0
- package/src/modules/logExport.js +134 -0
- package/src/modules/nosqlGuard.js +143 -0
- package/src/modules/osFirewall.js +182 -0
- package/src/modules/rateLimiter.js +196 -0
- package/src/modules/sanitizer.js +253 -0
- package/src/modules/threatDetector.js +202 -0
- package/src/scanner/checks/dependencies.js +125 -0
- package/src/scanner/checks/environment.js +118 -0
- package/src/scanner/checks/exposure.js +99 -0
- package/src/scanner/checks/headers.js +119 -0
- package/src/scanner/checks/ssl.js +151 -0
- package/src/scanner/index.js +209 -0
- package/src/utils/cidr.js +140 -0
- package/src/utils/logger.js +194 -0
- 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
|
+
};
|