blob: 7c79bad979140871fb0bc9047526f9258c89ca6e [file] [log] [blame]
b.liue9582032025-04-17 19:18:16 +08001#ifndef _PX5G_OPENSSL_HPP
2#define _PX5G_OPENSSL_HPP
3
4// #define OPENSSL_API_COMPAT 0x10102000L
5#include <fcntl.h>
6#include <openssl/bn.h>
7#include <openssl/err.h>
8#include <openssl/pem.h>
9#include <openssl/rsa.h>
10#include <unistd.h>
11#include <memory>
12#include <stdexcept>
13#include <string>
14
15static constexpr auto rsa_min_modulus_bits = 512;
16
17using EVP_PKEY_ptr = std::unique_ptr<EVP_PKEY, decltype(&::EVP_PKEY_free)>;
18
19using X509_NAME_ptr = std::unique_ptr<X509_NAME, decltype(&::X509_NAME_free)>;
20
21auto checkend(const std::string& crtpath, time_t seconds = 0, bool use_pem = true) -> bool;
22
23auto gen_eckey(int curve) -> EVP_PKEY_ptr;
24
25auto gen_rsakey(int keysize, BN_ULONG exponent = RSA_F4) -> EVP_PKEY_ptr;
26
27void write_key(const EVP_PKEY_ptr& pkey, const std::string& keypath = "", bool use_pem = true);
28
29auto subject2name(const std::string& subject) -> X509_NAME_ptr;
30
31void selfsigned(const EVP_PKEY_ptr& pkey,
32 int days,
33 const std::string& subject = "",
34 const std::string& crtpath = "",
35 bool use_pem = true);
36
37// ------------------------- implementation: ----------------------------------
38
39inline auto print_error(const char* str, const size_t /*len*/, void* errmsg) -> int
40{
41 *static_cast<std::string*>(errmsg) += str;
42 return 0;
43}
44
45// wrapper for clang-tidy:
46inline auto _BIO_new_fp(FILE* stream, const bool use_pem, const bool close = false) -> BIO*
47{
48 return BIO_new_fp(stream, // NOLINTNEXTLINE(hicpp-signed-bitwise) macros:
49 (use_pem ? BIO_FP_TEXT : 0) | (close ? BIO_CLOSE : BIO_NOCLOSE));
50}
51
52auto checkend(const std::string& crtpath, const time_t seconds, const bool use_pem) -> bool
53{
54 BIO* bio = crtpath.empty() ? _BIO_new_fp(stdin, use_pem)
55 : BIO_new_file(crtpath.c_str(), (use_pem ? "r" : "rb"));
56
57 X509* x509 = nullptr;
58
59 if (bio != nullptr) {
60 x509 = use_pem ? PEM_read_bio_X509_AUX(bio, nullptr, nullptr, nullptr)
61 : d2i_X509_bio(bio, nullptr);
62 BIO_free(bio);
63 }
64
65 if (x509 == nullptr) {
66 std::string errmsg{"checkend error: unable to load certificate\n"};
67 ERR_print_errors_cb(print_error, &errmsg);
68 throw std::runtime_error(errmsg);
69 }
70
71 time_t checktime = time(nullptr) + seconds;
72 auto cmp = X509_cmp_time(X509_get0_notAfter(x509), &checktime);
73
74 X509_free(x509);
75
76 return (cmp >= 0);
77}
78
79auto gen_eckey(const int curve) -> EVP_PKEY_ptr
80{
81 EC_GROUP* group = curve != 0 ? EC_GROUP_new_by_curve_name(curve) : nullptr;
82
83 if (group == nullptr) {
84 std::string errmsg{"gen_eckey error: cannot build group for curve id "};
85 errmsg += std::to_string(curve) + "\n";
86 ERR_print_errors_cb(print_error, &errmsg);
87 throw std::runtime_error(errmsg);
88 }
89
90 EC_GROUP_set_asn1_flag(group, OPENSSL_EC_NAMED_CURVE);
91
92 EC_GROUP_set_point_conversion_form(group, POINT_CONVERSION_UNCOMPRESSED);
93
94 auto* eckey = EC_KEY_new();
95
96 if (eckey != nullptr) {
97 if ((EC_KEY_set_group(eckey, group) == 0) || (EC_KEY_generate_key(eckey) == 0)) {
98 EC_KEY_free(eckey);
99 eckey = nullptr;
100 }
101 }
102
103 EC_GROUP_free(group);
104
105 if (eckey == nullptr) {
106 std::string errmsg{"gen_eckey error: cannot build key with curve id "};
107 errmsg += std::to_string(curve) + "\n";
108 ERR_print_errors_cb(print_error, &errmsg);
109 throw std::runtime_error(errmsg);
110 }
111
112 EVP_PKEY_ptr pkey{EVP_PKEY_new(), ::EVP_PKEY_free};
113
114 // EVP_PKEY_assign_EC_KEY is a macro casting eckey to char *:
115 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
116 if (!EVP_PKEY_assign_EC_KEY(pkey.get(), eckey)) {
117 EC_KEY_free(eckey);
118 std::string errmsg{"gen_eckey error: cannot assign EC key to EVP\n"};
119 ERR_print_errors_cb(print_error, &errmsg);
120 throw std::runtime_error(errmsg);
121 }
122
123 return pkey;
124}
125
126auto gen_rsakey(const int keysize, const BN_ULONG exponent) -> EVP_PKEY_ptr
127{
128 if (keysize < rsa_min_modulus_bits || keysize > OPENSSL_RSA_MAX_MODULUS_BITS) {
129 std::string errmsg{"gen_rsakey error: RSA keysize ("};
130 errmsg += std::to_string(keysize) + ") out of range [512..";
131 errmsg += std::to_string(OPENSSL_RSA_MAX_MODULUS_BITS) + "]";
132 throw std::runtime_error(errmsg);
133 }
134 auto* bignum = BN_new();
135
136 if (bignum == nullptr) {
137 std::string errmsg{"gen_rsakey error: cannot get big number struct\n"};
138 ERR_print_errors_cb(print_error, &errmsg);
139 throw std::runtime_error(errmsg);
140 }
141
142 auto* rsa = RSA_new();
143
144 if (rsa != nullptr) {
145 if ((BN_set_word(bignum, exponent) == 0) ||
146 (RSA_generate_key_ex(rsa, keysize, bignum, nullptr) == 0))
147 {
148 RSA_free(rsa);
149 rsa = nullptr;
150 }
151 }
152
153 BN_free(bignum);
154
155 if (rsa == nullptr) {
156 std::string errmsg{"gen_rsakey error: cannot create RSA key with size"};
157 errmsg += std::to_string(keysize) + " and exponent ";
158 errmsg += std::to_string(exponent) + "\n";
159 ERR_print_errors_cb(print_error, &errmsg);
160 throw std::runtime_error(errmsg);
161 }
162
163 EVP_PKEY_ptr pkey{EVP_PKEY_new(), ::EVP_PKEY_free};
164
165 // EVP_PKEY_assign_RSA is a macro casting rsa to char *:
166 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
167 if (!EVP_PKEY_assign_RSA(pkey.get(), rsa)) {
168 RSA_free(rsa);
169 std::string errmsg{"gen_rsakey error: cannot assign RSA key to EVP\n"};
170 ERR_print_errors_cb(print_error, &errmsg);
171 throw std::runtime_error(errmsg);
172 }
173
174 return pkey;
175}
176
177void write_key(const EVP_PKEY_ptr& pkey, const std::string& keypath, const bool use_pem)
178{
179 BIO* bio = nullptr;
180
181 if (keypath.empty()) {
182 bio = _BIO_new_fp(stdout, use_pem);
183 }
184
185 else { // BIO_new_file(keypath.c_str(), (use_pem ? "w" : "wb") );
186
187 static constexpr auto mask = 0600;
188 // auto fd = open(keypath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mask);
189 // creat has no cloexec, alt. triggers cppcoreguidelines-pro-type-vararg
190 // NOLINTNEXTLINE(android-cloexec-creat)
191 auto fd = creat(keypath.c_str(), mask); // the same without va_args.
192
193 if (fd >= 0) {
194 auto* fp = fdopen(fd, (use_pem ? "w" : "wb"));
195
196 if (fp != nullptr) {
197 bio = _BIO_new_fp(fp, use_pem, true);
198 if (bio == nullptr) {
199 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) fp owns fd:
200 fclose(fp);
201 }
202 }
203 else {
204 close(fd);
205 }
206 }
207 }
208
209 if (bio == nullptr) {
210 std::string errmsg{"write_key error: cannot open "};
211 errmsg += keypath.empty() ? "stdout" : keypath;
212 errmsg += "\n";
213 ERR_print_errors_cb(print_error, &errmsg);
214 throw std::runtime_error(errmsg);
215 }
216
217 int len = 0;
218
219 auto* key = pkey.get();
220 switch (EVP_PKEY_base_id(key)) { // use same format as px5g:
221 case EVP_PKEY_EC:
222 len = use_pem ? PEM_write_bio_ECPrivateKey(bio, EVP_PKEY_get0_EC_KEY(key), nullptr,
223 nullptr, 0, nullptr, nullptr)
224 : i2d_ECPrivateKey_bio(bio, EVP_PKEY_get0_EC_KEY(key));
225 break;
226 case EVP_PKEY_RSA:
227 len = use_pem ? PEM_write_bio_RSAPrivateKey(bio, EVP_PKEY_get0_RSA(key), nullptr,
228 nullptr, 0, nullptr, nullptr)
229 : i2d_RSAPrivateKey_bio(bio, EVP_PKEY_get0_RSA(key));
230 break;
231 default:
232 len = use_pem
233 ? PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, nullptr)
234 : i2d_PrivateKey_bio(bio, key);
235 }
236
237 BIO_free_all(bio);
238
239 if (len == 0) {
240 std::string errmsg{"write_key error: cannot write EVP pkey to "};
241 errmsg += keypath.empty() ? "stdout" : keypath;
242 errmsg += "\n";
243 ERR_print_errors_cb(print_error, &errmsg);
244 throw std::runtime_error(errmsg);
245 }
246}
247
248auto subject2name(const std::string& subject) -> X509_NAME_ptr
249{
250 if (!subject.empty() && subject[0] != '/') {
251 throw std::runtime_error("subject2name errror: not starting with /");
252 }
253
254 X509_NAME_ptr name = {X509_NAME_new(), ::X509_NAME_free};
255
256 if (!name) {
257 std::string errmsg{"subject2name error: cannot create X509 name \n"};
258 ERR_print_errors_cb(print_error, &errmsg);
259 throw std::runtime_error(errmsg);
260 }
261
262 if (subject.empty()) {
263 return name;
264 }
265
266 int prev = 1;
267 std::string type{};
268 char chr = '=';
269 for (int i = 0; subject[i] != 0;) {
270 ++i;
271 if (subject[i] == '\\' && subject[++i] == '\0') {
272 throw std::runtime_error("subject2name errror: escape at the end");
273 }
274 if (subject[i] != chr && subject[i] != '\0') {
275 continue;
276 }
277 if (chr == '=') {
278 type = subject.substr(prev, i - prev);
279 chr = '/';
280 }
281 else {
282 auto nid = OBJ_txt2nid(type.c_str());
283 if (nid == NID_undef) {
284 // skip unknown entries (silently?).
285 }
286 else {
287 const auto* val = // X509_NAME_add_entry_by_NID wants it unsigned:
288 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
289 reinterpret_cast<const unsigned char*>(&subject[prev]);
290
291 int len = i - prev;
292
293 if (X509_NAME_add_entry_by_NID(
294 name.get(), nid,
295 MBSTRING_ASC, // NOLINT(hicpp-signed-bitwise) is macro
296 val, len, -1, 0) == 0)
297 {
298 std::string errmsg{"subject2name error: cannot add "};
299 errmsg += "/" + type + "=" + subject.substr(prev, len) + "\n";
300 ERR_print_errors_cb(print_error, &errmsg);
301 throw std::runtime_error(errmsg);
302 }
303 }
304 chr = '=';
305 }
306 prev = i + 1;
307 }
308
309 return name;
310}
311
312void selfsigned(const EVP_PKEY_ptr& pkey,
313 const int days,
314 const std::string& subject,
315 const std::string& crtpath,
316 const bool use_pem)
317{
318 auto* x509 = X509_new();
319
320 if (x509 == nullptr) {
321 std::string errmsg{"selfsigned error: cannot create X509 structure\n"};
322 ERR_print_errors_cb(print_error, &errmsg);
323 throw std::runtime_error(errmsg);
324 }
325
326 auto freeX509_and_throw = [&x509](const std::string& what) {
327 X509_free(x509);
328 std::string errmsg{"selfsigned error: cannot set "};
329 errmsg += what + " in X509 certificate\n";
330 ERR_print_errors_cb(print_error, &errmsg);
331 throw std::runtime_error(errmsg);
332 };
333
334 if (X509_set_version(x509, 2) == 0) {
335 freeX509_and_throw("version");
336 }
337
338 if (X509_set_pubkey(x509, pkey.get()) == 0) {
339 freeX509_and_throw("pubkey");
340 }
341
342 if ((X509_gmtime_adj(X509_getm_notBefore(x509), 0) == nullptr) ||
343 (X509_time_adj_ex(X509_getm_notAfter(x509), days, 0, nullptr) == nullptr))
344 {
345 freeX509_and_throw("times");
346 }
347
348 X509_NAME_ptr name{nullptr, ::X509_NAME_free};
349
350 try {
351 name = subject2name(subject);
352 }
353 catch (...) {
354 X509_free(x509);
355 throw;
356 }
357
358 if (X509_set_subject_name(x509, name.get()) == 0) {
359 freeX509_and_throw("subject");
360 }
361
362 if (X509_set_issuer_name(x509, name.get()) == 0) {
363 freeX509_and_throw("issuer");
364 }
365
366 auto* bignum = BN_new();
367
368 if (bignum == nullptr) {
369 freeX509_and_throw("serial (creating big number struct)");
370 }
371
372 static const auto BITS = 159;
373 if (BN_rand(bignum, BITS, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY) == 0) {
374 BN_free(bignum);
375 freeX509_and_throw("serial (creating random number)");
376 }
377
378 if (BN_to_ASN1_INTEGER(bignum, X509_get_serialNumber(x509)) == nullptr) {
379 BN_free(bignum);
380 freeX509_and_throw("random serial");
381 }
382
383 BN_free(bignum);
384
385 if (X509_sign(x509, pkey.get(), EVP_sha256()) == 0) {
386 freeX509_and_throw("signing digest");
387 }
388
389 BIO* bio = crtpath.empty() ? _BIO_new_fp(stdout, use_pem)
390 : BIO_new_file(crtpath.c_str(), (use_pem ? "w" : "wb"));
391
392 int len = 0;
393
394 if (bio != nullptr) {
395 len = use_pem ? PEM_write_bio_X509(bio, x509) : i2d_X509_bio(bio, x509);
396 BIO_free_all(bio);
397 }
398
399 X509_free(x509);
400
401 if (len == 0) {
402 std::string errmsg{"selfsigned error: cannot write certificate to "};
403 errmsg += crtpath.empty() ? "stdout" : crtpath;
404 errmsg += "\n";
405 ERR_print_errors_cb(print_error, &errmsg);
406 throw std::runtime_error(errmsg);
407 }
408}
409
410#endif