1 2 /** Small helpers for openssl 3 * 4 * This module contains all the OpenSSL related helpers to wrap 5 * functionality of the D language binding provided by the dub module 6 * 'openssl'. 7 * 8 * See: https://github.com/D-Programming-Deimos/openssl 9 */ 10 module acme.openssl_helpers; 11 12 import deimos.openssl.conf; 13 import deimos.openssl.evp; 14 import deimos.openssl.err; 15 import deimos.openssl.pem; 16 import deimos.openssl.x509; 17 import deimos.openssl.x509v3; 18 19 import std.conv; 20 import std.string; 21 import std.typecons; 22 23 import acme.exception; 24 25 /* ----------------------------------------------------------------------- */ 26 27 /** Get the contents of a big number as string 28 * 29 * Param: 30 * bn - pointer to a big number structure 31 * Returns: 32 * a string representing the BIGNUM 33 */ 34 string getBigNumber(BIGNUM* bn) 35 { 36 BIO * bio = BIO_new(BIO_s_mem()); 37 scope(exit) BIO_free(bio); 38 BN_print(bio, bn); 39 char[2048] buffer; 40 auto rc = BIO_gets(bio, buffer.ptr, buffer.length); 41 auto num = buffer[0..rc].to!string; 42 return num; 43 } 44 45 /** Get the content bytes of a big number as string 46 * 47 * Param: 48 * bn - pointer to a big number structure 49 * Returns: 50 * a string representing the BIGNUM 51 */ 52 ubyte[] getBigNumberBytes(const BIGNUM* bn) 53 { 54 /* Get number of bytes to store a BIGNUM */ 55 int numBytes = BN_num_bytes(bn); 56 ubyte[] buffer; 57 buffer.length = numBytes; 58 59 /* Copy bytes of BIGNUM to our buffer */ 60 BN_bn2bin(bn, buffer.ptr); 61 62 return buffer; 63 } 64 65 66 /* ----------------------------------------------------------------------- */ 67 68 /** Export BIO contents as an array of chars 69 * 70 * Param: 71 * bio - pointer to a BIO structure 72 * Returns: 73 * An array of chars representing the BIO structure 74 */ 75 char[] toVector(BIO * bio) 76 { 77 enum buffSize = 1024; 78 char[buffSize] buffer; 79 char[] rc; 80 81 int count = 0; 82 do 83 { 84 count = BIO_read(bio, buffer.ptr, buffer.length); 85 if (count > 0) 86 { 87 rc ~= buffer[0..count]; 88 } 89 } 90 while (count > 0); 91 92 return rc; 93 } 94 95 /** Export BIO contents as an array of immutable chars (string) 96 * 97 * Param: 98 * bio - pointer to a BIO structure 99 * Returns: 100 * An array of immutable chars representing the BIO structure 101 */ 102 string toString(BIO *bio) 103 { 104 char[] v = toVector(bio); 105 return to!string(v); 106 } 107 108 /* ----------------------------------------------------------------------- */ 109 110 /** Encode data as Base64 111 * 112 * We use openssl to do this since we're already linking to it. As an 113 * alternative we could also use the phobos routines. 114 * 115 * Params: 116 * t - data to encode as base64 117 * Returns: 118 * An array of chars with the base64 encoded data. 119 */ 120 char[] base64Encode(T)(T t) 121 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 122 { 123 BIO * bio = BIO_new(BIO_s_mem()); 124 BIO * b64 = BIO_new(BIO_f_base64()); 125 126 // OpenSSL inserts new lines by default to make it look like PEM format. 127 // Turn that off. 128 BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 129 130 BIO_push(b64, bio); 131 if (BIO_write(b64, cast(void*)(t.ptr), t.length.to!int) < 0 || 132 BIO_flush(b64) < 0) 133 { 134 throw new AcmeException("Can't encode data as base64."); 135 } 136 return toVector(bio); 137 } 138 139 /** Encode data as URl-safe Base64 140 * 141 * We need url safe base64 encoding and openssl only gives us regular 142 * base64, so we convert it here. Also trim trailing '=' from data 143 * (see RFC). 144 * 145 * The following replacements are done: 146 * * '+' is converted to '-' 147 * * '/' is converted to '_' 148 * * '=' terminates the output at this point, stripping all '=' chars 149 * 150 * Params: 151 * t - data to encode as base64 152 * Returns: 153 * An array of chars with the base64 encoded data. 154 */ 155 char[] base64EncodeUrlSafe(T)(T t) 156 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 157 { 158 /* Do a Standard Base64 Encode */ 159 char[] s = base64Encode(t); 160 161 /* Do the replacements */ 162 foreach (i, ref v; s) 163 { 164 if (s[i] == '+') { s[i] = '-'; } 165 else if (s[i] == '/') { s[i] = '_'; } 166 else if (s[i] == '=') { s.length = i; break; } 167 } 168 return s; 169 } 170 171 /** Encode BIGNUM data as URl-safe Base64 172 * 173 * We need url safe base64 encoding and openssl only gives us regular 174 * base64, so we convert it here. Also trim trailing '=' from data 175 * (see RFC). 176 * 177 * The following replacements are done: 178 * * '+' is converted to '-' 179 * * '/' is converted to '_' 180 * * '=' terminates the output at this point, stripping all '=' chars 181 * 182 * Params: 183 * bn - pointer to BIGNUM to encode as base64 184 * Returns: 185 * An array of chars with the base64 encoded data. 186 */ 187 char[] base64EncodeUrlSafe(const BIGNUM* bn) 188 { 189 /* Get contents bytes of a BIGNUM */ 190 ubyte[] buffer = getBigNumberBytes(bn); 191 192 /* Encode the buffer as URL-safe base64 string */ 193 return base64EncodeUrlSafe(buffer); 194 } 195 196 /** Calculate the SHA256 of a string 197 * 198 * We use openssl to do this since we're already linking to it. We could 199 * also use functions from the phobos library. 200 * 201 * Param: 202 * s - string to calculate hash from 203 * Returns: 204 * ubyte[SHA256_DIGEST_LENGTH] for the hash 205 */ 206 ubyte[SHA256_DIGEST_LENGTH] sha256Encode(const char[] s) 207 { 208 ubyte[SHA256_DIGEST_LENGTH] hash; 209 SHA256_CTX sha256; 210 if (!SHA256_Init(&sha256) || 211 !SHA256_Update(&sha256, s.ptr, s.length) || 212 !SHA256_Final(hash.ptr, &sha256)) 213 { 214 throw new AcmeException("Error hashing string data"); 215 } 216 return hash; 217 } 218 219 /** Convert certificate from DER format to PEM format 220 * 221 * Params: 222 * der - DER encoded certificate 223 * Returns: 224 * a PEM-encoded certificate 225 */ 226 string convertDERtoPEM(const char[] der) 227 { 228 /* Write DER to BIO buffer */ 229 BIO* derBio = BIO_new(BIO_s_mem()); 230 BIO_write(derBio, cast(const(void)*)der.ptr, der.length.to!int); 231 232 /* Add conversion filter */ 233 X509* x509 = d2i_X509_bio(derBio, null); 234 235 /* Write DER through filter to as PEM to other BIO buffer */ 236 BIO* pemBio = BIO_new(BIO_s_mem()); 237 PEM_write_bio_X509(pemBio, x509); 238 239 /* Output data as data string */ 240 return toString(pemBio); 241 } 242 243 extern(C) ASN1_TIME * C_X509_get_notAfter(const char* certPtr, int certLen); 244 245 /** Extract expiry date from a PEM encoded Zertificate 246 * 247 * Params: 248 * cert - PEM encoded certificate to query 249 * extractor - function or delegate process an ASN1_TIME* argument. 250 */ 251 T extractExpiryData(T, alias extractor)(const(char[]) cert) 252 { 253 ASN1_TIME * t = C_X509_get_notAfter(cert.ptr, cert.length.to!int); 254 /+ 255 BIO* bio = BIO_new(BIO_s_mem()); 256 if (BIO_write(bio, cast(const(void)*) cert.ptr, cert.length.to!int) <= 0) 257 { 258 throw new AcmeException("Can't write PEM data to BIO struct."); 259 } 260 X509* x509 = PEM_read_bio_X509(bio, null, null, null); 261 262 ASN1_TIME * t = X509_get_notAfter(x509); 263 +/ 264 T rc = extractor(t); 265 return rc; 266 } 267 268 /* ----------------------------------------------------------------------- */ 269 270 /// Return tuple of makeCertificateSigningRequest 271 alias tupleCsrPkey = Tuple!(string, "csr", string, "pkey"); 272 273 /** Create a CSR with our domains 274 * 275 * Params: 276 * domainNames - Names of domains, first element is subject of cert 277 * Returns: 278 * tupleCsrPkey containing CSr and PKey 279 */ 280 tupleCsrPkey makeCertificateSigningRequest(string[] domainNames) 281 { 282 if (domainNames.length < 1) { 283 throw new AcmeException("We need at least one domain name."); 284 } 285 286 BIGNUM* bn = BN_new(); 287 if (!BN_set_word(bn, RSA_F4)) { 288 throw new AcmeException("Can't set word."); 289 } 290 EVP_PKEY * pkey; 291 pkey = EVP_PKEY_new(); 292 293 RSA* rsa = RSA_new(); 294 enum bits = 2048; 295 if (!RSA_generate_key_ex(rsa, bits, bn, null)) 296 { 297 throw new AcmeException("Can't generate key."); 298 } 299 EVP_PKEY_assign_RSA(pkey, rsa); 300 301 /* Set first element of domainNames as cert CN subject */ 302 X509_REQ* x509_req = X509_REQ_new(); 303 auto name = domainNames[0]; 304 305 X509_REQ_set_version(x509_req, 1); 306 X509_REQ_set_pubkey(x509_req, pkey); 307 308 X509_NAME* cn = X509_REQ_get_subject_name(x509_req); 309 assert (cn !is null, "Can get X509_REQ_get_subject_name"); 310 auto rc_cn = X509_NAME_add_entry_by_txt( 311 cn, 312 "CN", 313 MBSTRING_ASC, 314 cast(const ubyte*)(name.toStringz), 315 -1, -1, 0); 316 if (!rc_cn) 317 { 318 throw new AcmeException("Can't add CN entry."); 319 } 320 321 /* Add other domainName as extension */ 322 if (domainNames.length > 1) 323 { 324 // We have multiple Subject Alternative Names 325 auto extensions = sk_X509_EXTENSION_new_null(); 326 if (!extensions) 327 { 328 throw new AcmeException("Unable to allocate Subject Alternative Name extensions"); 329 } 330 331 foreach (i, ref v ; domainNames) 332 { 333 auto cstr = ("DNS:" ~ v).toStringz; 334 auto nid = X509V3_EXT_conf_nid(null, null, NID_subject_alt_name, cast(char*)cstr); 335 if (!sk_X509_EXTENSION_push(extensions, nid)) 336 { 337 throw new AcmeException("Unable to add Subject Alternative Name to extensions"); 338 } 339 } 340 341 if (X509_REQ_add_extensions(x509_req, extensions) != 1) { 342 throw new AcmeException("Unable to add Subject Alternative Names to CSR"); 343 } 344 345 sk_X509_EXTENSION_pop_free(extensions, &X509_EXTENSION_free); 346 } 347 348 // EVP_PKEY* key = EVP_PKEY_new(); 349 // if (!EVP_PKEY_assign_RSA(key, rsa)) 350 // { 351 // throw new AcmeException("Can't set RSA key."); 352 // } 353 //rsa = null; // rsa will be freed when key is freed. 354 355 BIO* keyBio = BIO_new(BIO_s_mem()); 356 if (PEM_write_bio_PrivateKey(keyBio, pkey, null, null, 0, null, null) != 1) { 357 throw new AcmeException("Can't copy private key to BIO."); 358 } 359 string privateKey = toString(keyBio); 360 361 if (!X509_REQ_set_pubkey(x509_req, pkey)) { 362 throw new AcmeException("Can't set subkey."); 363 } 364 365 if (!X509_REQ_sign(x509_req, pkey, EVP_sha256())) { 366 throw new AcmeException("Can't sign."); 367 } 368 369 BIO* reqBio = BIO_new(BIO_s_mem()); 370 if (i2d_X509_REQ_bio(reqBio, x509_req) < 0) { 371 throw new AcmeException("Can't setup sign request"); 372 } 373 374 tupleCsrPkey rc = tuple(base64EncodeUrlSafe(toVector(reqBio)).to!string, privateKey); 375 return rc; 376 } 377 378 /* ----------------------------------------------------------------------- */ 379 380 /** Sign a given string with an SHA256 hash 381 * 382 * Param: 383 * s - string to sign 384 * privateKey - signing key to use 385 * 386 * Returns: 387 * A SHA256 signature on provided data 388 * See: https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying 389 */ 390 char[] signDataWithSHA256(char[] s, EVP_PKEY* privateKey) 391 { 392 size_t signatureLength = 0; 393 394 EVP_MD_CTX* context = EVP_MD_CTX_create(); 395 const EVP_MD * sha256 = EVP_get_digestbyname("SHA256"); 396 if ( !sha256 || 397 EVP_DigestInit_ex(context, sha256, null) != 1 || 398 EVP_DigestSignInit(context, null, sha256, null, privateKey) != 1 || 399 EVP_DigestSignUpdate(context, s.toStringz, s.length) != 1 || 400 EVP_DigestSignFinal(context, null, &signatureLength) != 1) 401 { 402 throw new AcmeException("Error creating SHA256 digest"); 403 } 404 405 ubyte[] signature; 406 signature.length = signatureLength; 407 if (EVP_DigestSignFinal(context, signature.ptr, &signatureLength) != 1) 408 { 409 throw new AcmeException("Error creating SHA256 digest in final signature"); 410 } 411 412 return base64EncodeUrlSafe(signature); 413 } 414 415 version (HAS_WORKING_SSL) 416 { 417 /** Initialize SSL library 418 * 419 * Do any kind of initialization here. 420 * Returns: 421 * true or false 422 */ 423 bool SSL_OpenLibrary() 424 { 425 /* Load the human readable error strings for libcrypto */ 426 OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, null); 427 428 /* Load all digest and cipher algorithms */ 429 //OpenSSL_add_all_algorithms(); // Is a macro for 430 OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS 431 | OPENSSL_INIT_ADD_ALL_DIGESTS 432 | OPENSSL_INIT_LOAD_CONFIG, null); 433 return true; 434 } 435 436 /** Teardown SSL library 437 * 438 * Reverse anything done in SSL_OpenLibrary(). 439 */ 440 void SSL_CloseLibrary() 441 { 442 /* Clean up */ 443 OPENSSL_cleanup(); 444 } 445 446 /* http://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl 447 * http://www.codepool.biz/how-to-use-openssl-to-generate-x-509-certificate-request.html 448 */ 449 450 /** Make a x509 pkey 451 * 452 * Create a RSA private keys with 2048 bits 453 * Returns: pointer to EVP_PKEY structure 454 * @internal 455 */ 456 EVP_PKEY* SSL_x509_make_pkey(int bits = 4096) 457 { 458 EVP_PKEY * pkey; 459 pkey = EVP_PKEY_new(); 460 RSA * rsa; 461 rsa = RSA_generate_key( 462 bits, /* number of bits for the key - 2048 is a sensible value */ 463 RSA_F4, /* exponent - RSA_F4 is defined as 0x10001L */ 464 null, /* callback - can be null if we aren't displaying progress */ 465 null /* callback argument - not needed in this case */ 466 ); 467 EVP_PKEY_assign_RSA(pkey, rsa); 468 return pkey; 469 } 470 471 /** Add extension using V3 code: we can set the config file as null, 472 * because we wont reference any other sections. 473 * @param cert pointer to X509 cert 474 * @param nid Extention ID 475 * @param value value of nid 476 * Returns: bool_t: 0 == False, !=0 True 477 */ 478 private bool add_ext(X509* cert, int nid, char[] value) 479 { 480 X509_EXTENSION *ex; 481 X509V3_CTX ctx; 482 /* This sets the 'context' of the extensions. */ 483 /* No configuration database */ 484 X509V3_set_ctx_nodb(&ctx); 485 /* Issuer and subject certs: both the target since it is self signed, 486 * no request and no CRL 487 */ 488 X509V3_set_ctx(&ctx, cert, cert, null, null, 0); 489 ex = X509V3_EXT_conf_nid(cast(LHASH_OF!(CONF_VALUE)*)null, &ctx, nid, cast(char*)value.toStringz); 490 if (!ex) 491 return false; 492 493 X509_add_ext(cert, ex, -1); 494 X509_EXTENSION_free(ex); 495 return true; 496 } 497 498 /** Make a x509 cert 499 * 500 * Creates a X509 Zertifikate with direkt library calls. 501 * @param pkey pointer to pkey struct to store 502 * @param dev_serial pointer to device serial string 503 * Returns: pointer to selfsigned x509 structure 504 * @todo Add leica fields to x509 cert 505 * @todo Add error handling to X509 Cert creation code. 506 * @internal 507 */ 508 X509* SSL_x509_make_cert(EVP_PKEY* pkey, char[] dev_serial) 509 { 510 X509 * x509; 511 x509 = X509_new(); 512 513 ASN1_INTEGER_set(X509_get_serialNumber(x509), 1); 514 515 X509_gmtime_adj(X509_get_notBefore(x509), 0); // now! 516 X509_gmtime_adj(X509_get_notAfter(x509), 50 * 31536000L); // 99 years 517 518 X509_set_pubkey(x509, pkey); 519 520 X509_NAME * name; 521 name = X509_get_subject_name(x509); 522 523 X509_NAME_add_entry_by_txt(name, cast(char*)("ST".ptr), MBSTRING_ASC, cast(ubyte*)("Niedersachsen".ptr), -1, -1, 0); 524 X509_NAME_add_entry_by_txt(name, cast(char*)("L".ptr), MBSTRING_ASC, cast(ubyte*)("Hannover".ptr), -1, -1, 0); 525 //OU Filed BREAKS precessing of CSR on LCG. Also see CON-289 - keep info at minimum for reduced size 526 //X509_NAME_add_entry_by_txt(name, "OU", MBSTRING_ASC, (unsigned char *)"IT", -1, -1, 0); 527 X509_NAME_add_entry_by_txt(name, cast(char*)("O".ptr), MBSTRING_ASC, cast(ubyte*)("Vahanus ".ptr), -1, -1, 0); 528 X509_NAME_add_entry_by_txt(name, cast(char*)("C".ptr), MBSTRING_ASC, cast(ubyte*)("DE".ptr), -1, -1, 0); 529 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(dev_serial.toStringz), -1, -1, 0); 530 531 X509_set_issuer_name(x509, name); 532 533 /* Add various extensions: standard extensions */ 534 add_ext(x509, NID_basic_constraints, "critical,CA:TRUE".dup); 535 add_ext(x509, NID_key_usage, "critical,keyCertSign,cRLSign".dup); 536 537 add_ext(x509, NID_subject_key_identifier, "hash".dup); 538 539 /* Some Netscape specific extensions */ 540 add_ext(x509, NID_netscape_cert_type, "sslCA".dup); 541 542 add_ext(x509, NID_netscape_comment, "example comment extension".dup); 543 544 version(none) { 545 /* Maybe even add our own extension based on existing */ 546 { 547 int nid; 548 nid = OBJ_create("1.2.3.4", "MyAlias", "My Test Alias Extension"); 549 X509V3_EXT_add_alias(nid, NID_netscape_comment); 550 add_ext(x509, nid, "example comment alias"); 551 } 552 } 553 X509_sign(x509, pkey, EVP_sha1()); 554 return x509; 555 } 556 557 /** Make a x509 CSR (cert signing request) 558 * @param pkey pointer to pkey struct to store 559 * @param dev_serial pointer to device serial string 560 * Returns: pointer to X509_REQ structure 561 */ 562 X509_REQ* SSL_x509_make_csr(EVP_PKEY* pkey, string[] domainNames) 563 { 564 assert(domainNames.length >= 1, "No domain names given."); 565 auto cnStr = domainNames[0].toStringz; 566 string[] extStrs; extStrs.length = domainNames.length - 1; 567 568 X509_REQ * x509_req; 569 x509_req = X509_REQ_new(); 570 assert(x509_req.req_info !is null, "The allocated X509_REQ* has req_info member set to NULL. This shouldn't be."); 571 572 X509_REQ_set_version(x509_req, 1); 573 X509_REQ_set_pubkey(x509_req, pkey); 574 575 X509_NAME * name; 576 name = X509_REQ_get_subject_name(x509_req); 577 assert (name !is null, "Can't read the req subject name struct."); 578 579 /* Setup some fields for the CSR */ 580 version(none) { 581 X509_NAME_add_entry_by_txt(name, cast(char*)("ST".ptr), MBSTRING_ASC, cast(ubyte*)("Niedersachsen".ptr), -1, -1, 0); 582 X509_NAME_add_entry_by_txt(name, cast(char*)("L".ptr), MBSTRING_ASC, cast(ubyte*)("Hannover".ptr), -1, -1, 0); 583 //OU Filed BREAKS precessing of CSR on LCG. Also see CON-289 - keep info at minimum for reduced size 584 //X509_NAME_add_entry_by_txt(name, "OU", MBSTRING_ASC, (unsigned char *)"IT", -1, -1, 0); 585 X509_NAME_add_entry_by_txt(name, cast(char*)("O".ptr), MBSTRING_ASC, cast(ubyte*)("Vahanus ".ptr), -1, -1, 0); 586 X509_NAME_add_entry_by_txt(name, cast(char*)("C".ptr), MBSTRING_ASC, cast(ubyte*)("DE".ptr), -1, -1, 0); 587 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(dev_serial.toStringz), -1, -1, 0); 588 } 589 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(cnStr), -1, -1, 0); 590 /* Add other domainName as extension */ 591 if (domainNames.length > 1) 592 { 593 // We have multiple Subject Alternative Names 594 auto extensions = sk_X509_EXTENSION_new_null(); 595 if (extensions is null) { 596 throw new AcmeException("Unable to allocate Subject Alternative Name extensions"); 597 } 598 foreach (i, ref v ; domainNames[1..$]) 599 { 600 auto cstr = ("DNS:" ~ v).toStringz; 601 auto nid = X509V3_EXT_conf_nid(null, null, NID_subject_alt_name, cast(char*)cstr); 602 if (!sk_X509_EXTENSION_push(extensions, nid)) { 603 throw new AcmeException("Unable to add Subject Alternative Name to extensions"); 604 } 605 } 606 if (X509_REQ_add_extensions(x509_req, extensions) != 1) { 607 throw new AcmeException("Unable to add Subject Alternative Names to CSR"); 608 } 609 sk_X509_EXTENSION_pop_free(extensions, &X509_EXTENSION_free); 610 } 611 612 /* Code below BREAKS acception of CSR at LCG. Also see CON-289 - minimize cert size, leave it out. */ 613 version(hasExtentions) { 614 STACK_OF(X509_EXTENSION) *exts = sk_X509_EXTENSION_new_null(); 615 616 // # Extensions for client certificates (`man x509v3_config`). 617 // basicConstraints = CA:FALSE 618 // nsCertType = client, email 619 // nsComment = "OpenSSL Generated Client Certificate" 620 // subjectKeyIdentifier = hash 621 // authorityKeyIdentifier = keyid,issuer 622 // keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment 623 // extendedKeyUsage = clientAuth, emailProtection 624 625 /* Add various extensions: standard extensions */ 626 add_req_ext(exts, NID_basic_constraints, "CA:FALSE"); 627 add_req_ext(exts, NID_key_usage, "critical, nonRepudiation, digitalSignature, keyEncipherment"); 628 add_req_ext(exts, NID_ext_key_usage, "clientAuth, emailProtection"); 629 add_req_ext(exts, NID_subject_key_identifier, "hash"); 630 add_req_ext(exts, NID_authority_key_identifier, "keyid,issuer"); 631 632 /* Some Netscape specific extensions */ 633 add_req_ext(exts, NID_netscape_cert_type, "client, email"); 634 add_req_ext(exts, NID_netscape_comment, "OpenSSL Generated Client Certificate"); 635 636 X509_REQ_add_extensions(x509_req, exts); 637 638 sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); 639 } 640 641 /* Sign the CSR with our PKEY */ 642 X509_REQ_sign(x509_req, pkey, EVP_sha1()); 643 return x509_req; 644 } 645 646 647 } 648 else 649 { 650 /* Sick of broken D binding for OpenSSL, I used the stub approach - 651 * just compile it as C and call the functions */ 652 653 extern(C) bool C_SSL_OpenLibrary(); 654 bool SSL_OpenLibrary() 655 { 656 return C_SSL_OpenLibrary(); 657 } 658 extern(C) void C_SSL_CloseLibrary(); 659 void SSL_CloseLibrary() 660 { 661 C_SSL_CloseLibrary(); 662 } 663 extern(C) EVP_PKEY* C_SSL_x509_make_pkey(int bits); 664 EVP_PKEY* SSL_x509_make_pkey(int bits) 665 { 666 return C_SSL_x509_make_pkey(bits); 667 } 668 extern(C) bool C_add_ext(X509* cert, int nid, char* value); 669 private bool add_ext(X509* cert, int nid, char[] value) 670 { 671 return C_add_ext(cert, nid, cast(char*)(value.toStringz)); 672 } 673 extern(C) X509* C_SSL_x509_make_cert(EVP_PKEY* pkey, char* subject); 674 X509* SSL_x509_make_cert(EVP_PKEY* pkey, char[] subject) 675 { 676 return C_SSL_x509_make_cert(pkey, cast(char*)(subject.toStringz)); 677 } 678 extern(C) X509_REQ* C_SSL_x509_make_csr(EVP_PKEY* pkey, char** domainNames, int domainNamesLength); 679 X509_REQ* SSL_x509_make_csr(EVP_PKEY* pkey, string[] domainNames) 680 { 681 char*[] C_domainNames; 682 C_domainNames.length = domainNames.length; 683 foreach(i, ref v; domainNames) C_domainNames[i] = cast(char*)v.toStringz; 684 return C_SSL_x509_make_csr(pkey, cast(char**)(C_domainNames.ptr), domainNames.length.to!int); 685 } 686 } 687 688 689 /++ 690 /* Code below commented out, obsolete function */ 691 /** Add extension using V3 code: we can set the config file as null 692 * because we wont reference any other sections. 693 * @param sk pointer to STACK_OF(X509_EXTENSION 694 * @param nid Extention ID 695 * @param value value of nid 696 * Returns: bool_t: 0 == False, !=0 True 697 * @internal 698 */ 699 static 700 bool_t add_req_ext(STACK_OF(X509_EXTENSION) *sk, int nid, cstring_p value) 701 { 702 X509_EXTENSION *ex; 703 ex = X509V3_EXT_conf_nid(null, null, nid, value); 704 if (!ex) 705 return False; 706 sk_X509_EXTENSION_push(sk, ex); 707 return True; 708 } 709 ++/ 710 711 /** Get a CSR as PEM string */ 712 char[] SSL_x509_get_PEM(X509_REQ* csr) 713 { 714 BUF_MEM* mem; 715 BIO* bio = BIO_new(BIO_s_mem()); 716 PEM_write_bio_X509_REQ(bio, csr); 717 BIO_get_mem_ptr(bio, &mem); 718 if (null == mem) { 719 return null; 720 } 721 //cstring_p rs = strndup(mem->data, mem->length); 722 char[] rs = mem.data[0..mem.length].dup; 723 BIO_free(bio); 724 return rs; 725 } 726 727 /** Read a x509 pkey pem string from memory 728 */ 729 EVP_PKEY* SSL_x509_read_pkey_memory(const char[] pkeyString, RSA** rsaRef = null) 730 { 731 auto cstr = cast(void*)pkeyString.toStringz; 732 EVP_PKEY* privateKey = EVP_PKEY_new(); 733 734 BIO* bio = BIO_new_mem_buf(cstr, -1); 735 RSA* rsa = PEM_read_bio_RSAPrivateKey(bio, null, null, null); 736 if (!rsa) { 737 throw new AcmeException("Unable to read private key"); 738 } 739 // rsa will get freed when privateKey_ is freed 740 auto rc = !EVP_PKEY_assign_RSA(privateKey, rsa); 741 if (rc) { 742 throw new AcmeException("Unable to assign RSA to private key"); 743 } 744 if (rsaRef) *rsaRef = rsa; 745 return privateKey; 746 } 747 748 /** Save a x509 pkey to a file 749 * @param path pathname of file to write 750 * @param pkey pointer to pkey struct to store 751 * Returns: return value of PEM_write_PrivateKey() 752 * @internal 753 */ 754 int SSL_x509_write_pkey(char[] path, EVP_PKEY * pkey) 755 { 756 import core.stdc.stdio; 757 int rc = -1; 758 FILE * f; 759 if (path is null) path = "key.pem".dup; 760 f = fopen(cast(char*)(path.toStringz), cast(char*)("wb".toStringz)); 761 if (f !is null) { 762 alias cbt = extern(C) int function(char*, int, int, void*); 763 rc = PEM_write_PrivateKey( 764 f, /* write the key to the file we've opened */ 765 pkey, /* our key from earlier */ 766 EVP_des_ede3_cbc(), /* default cipher for encrypting the key on disk */ 767 cast(ubyte*)("replace_me".ptr), /* passphrase required for decrypting the key on disk */ 768 10, /* length of the passphrase string */ 769 cast(cbt)null, /* callback for requesting a password */ 770 cast(void*)null /* data to pass to the callback */ 771 ); 772 fclose(f); 773 } 774 return rc; 775 } 776 777 /** Read a x509 pkey from a file 778 * @param path pathname of file to read 779 * Returns: pointer to EVP_PKEY, return value of PEM_write_PrivateKey() 780 * @internal 781 */ 782 EVP_PKEY * SSL_x509_read_pkey(char[] path) 783 { 784 import core.stdc.stdio; 785 EVP_PKEY * pkey; 786 pkey = EVP_PKEY_new(); 787 FILE * f; 788 if (path is null) path = "key.pem".dup; 789 f = fopen(cast(char*)(path.toStringz), cast(char*)("rb".ptr)); 790 if (f !is null) { 791 pkey = PEM_read_PrivateKey( 792 f, /* read the key to the file we've opened */ 793 &pkey, /* our key from earlier */ 794 null, /* callback for requesting a password */ 795 null /* data to pass to the callback */ 796 ); 797 fclose(f); 798 } 799 return pkey; 800 } 801 802 /** Save a x509 cert to a file 803 * @param path pathname of file to write 804 * @param x509 pointer to x509 struct to store 805 * Returns: return value of PEM_write_X509() 806 * @internal 807 */ 808 int SSL_x509_write_cert(char[] path, X509* x509) 809 { 810 import core.stdc.stdio; 811 int rc = -1; 812 FILE * f; 813 if (path is null) path = "cert.pem".dup; 814 f = fopen(cast(char*)(path.toStringz), cast(char*)("wb".ptr)); 815 if (f !is null) { 816 rc = PEM_write_X509( 817 f, /* write the certificate to the file we've opened */ 818 x509 /* our certificate */ 819 ); 820 fclose(f); 821 } 822 return rc; 823 } 824 825 /* ------------------------------------------------------------------------ */ 826 827 /** Create a SSL private key 828 * 829 * This functions creates an EVP_PKEY with 2048 bits. It's returned as 830 * PEM encoded text. 831 * 832 * Returns: 833 * pointer to pem encoded string containing EVP_PKEY private key. 834 */ 835 char[] openSSL_CreatePrivateKey(int bits = 4096) 836 { 837 import std.stdio; 838 //writeln("Create a SSL pKey."); 839 char[] rs; 840 841 EVP_PKEY * pkey = SSL_x509_make_pkey(bits); 842 if (null == pkey) { 843 stderr.writeln("Can't create a pKey"); 844 } else { 845 BIO *bio = BIO_new(BIO_s_mem()); 846 if (null == bio) { 847 stderr.writeln("Can't create a BIO"); 848 } else { 849 alias cbt = extern(C) int function(char*, int, int, void*); 850 int rc = PEM_write_bio_PrivateKey(bio, pkey, 851 cast(const(evp_cipher_st)*)null, 852 cast(ubyte*)null, 0, 853 cast(cbt)null, cast(void*)null); 854 if (!rc) { 855 stderr.writeln("Can't write pKEY to BIO"); 856 } else { 857 BUF_MEM *mem; 858 BIO_get_mem_ptr(bio, &mem); 859 if (null == mem) { 860 stderr.writeln("Can't get pointer to BUF_MEM from BIO"); 861 } else { 862 if (mem.data !is null) 863 rs = mem.data[0..mem.length]; 864 if (rs is null || rs.empty) { 865 stderr.writeln("Can't get data from BIO"); 866 } else { 867 rs = (rs).dup; 868 } 869 } 870 } 871 BIO_free(bio); 872 } 873 EVP_PKEY_free(pkey); 874 } 875 return rs; 876 } 877 unittest { 878 import std.stdio : writeln, writefln, stdout, stderr; 879 import std.datetime.stopwatch : benchmark; 880 881 /* Test Key Generation */ 882 writeln("Testing the SSL routines ported from C"); 883 writeln("--- Create a private key ---"); 884 stdout.flush; 885 char[] myPKey = openSSL_CreatePrivateKey(); 886 writeln("Got the following from library:\n", myPKey); 887 stdout.flush; 888 889 /* Benchmark Key Generation */ 890 writeln("--- Benchmark creating a private key ---"); 891 stdout.flush; 892 void benchCreateKeyStub() { 893 char[] tmp = openSSL_CreatePrivateKey(); 894 assert(tmp !is null && !tmp.empty, "Empty private key."); 895 } 896 auto dur = benchmark!(benchCreateKeyStub)(100); 897 writeln("Benchmarking 100 calls, duration ", dur); 898 stdout.flush; 899 } 900 901 /** Create a SSL cert signing request from a pkey and a serial number 902 * 903 * This functions creates an CertificateSignRequest (CSR) with 2048 bits. 904 * It's returned as PEM encoded text. 905 * 906 * Params: 907 * prkey - private key as PEM string 908 * serial - same custom data, e.g. a serial number 909 * Returns: 910 * ERROR: pointer to pem encoded string of CSR. 911 * CORRECT: pointer to bas64url encoded DER data! See RFC. 912 */ 913 char[] openSSL_CreateCertificateSignRequest(const char[] prkey, string[] domainNames) 914 { 915 BIO *bio; 916 int rc; 917 918 //HACK 919 const char[] prkey2 = openSSL_CreatePrivateKey(); 920 921 /* Get EVP_PKEY from PEM encoded string */ 922 EVP_PKEY* pkey; 923 //pkey = SSL_x509_read_pkey_memory(prkey); 924 pkey = SSL_x509_read_pkey_memory(prkey2); 925 926 /* Create CSR from private key and serial number */ 927 928 X509_REQ* x509_req = SSL_x509_make_csr(pkey, domainNames); 929 assert (x509_req !is null, "Returned empty cert req."); 930 931 /* Convert to PEM string */ 932 // auto rs = SSL_x509_get_PEM(x509_req); 933 BIO* reqBio = BIO_new(BIO_s_mem()); 934 if (i2d_X509_REQ_bio(reqBio, x509_req) < 0) { 935 throw new AcmeException("Can't convert CSR to DER."); 936 } 937 char[] rs = cast(char[])base64EncodeUrlSafe(toVector(reqBio)).to!string; 938 BIO_free(reqBio); 939 EVP_PKEY_free(pkey); 940 return rs; 941 } 942 unittest { 943 import std.stdio : writeln, writefln, stdout, stderr; 944 import std.datetime.stopwatch : benchmark; 945 946 /* Test Key Generation */ 947 writeln("Testing the CSR-creation routines ported from C"); 948 writeln("--- Create a private key ---"); 949 stdout.flush; 950 char[] myPKey = openSSL_CreatePrivateKey(); 951 writeln("Got the following from library:\n", myPKey); 952 stdout.flush; 953 char[] myCSR = openSSL_CreateCertificateSignRequest(myPKey, [ "bodylove.myds.me" ]); 954 writeln("Got the following CSR from library:\n", myCSR); 955 956 /* Benchmark CSR Generation */ 957 writeln("--- Benchmark creating a CSR ---"); 958 stdout.flush; 959 void benchCreateCSRStub() { 960 char[] tmp = openSSL_CreateCertificateSignRequest(myPKey, [ "bodylove.myds.me" ]); 961 assert(tmp !is null && !tmp.empty, "Empty CSR."); 962 } 963 auto dur = benchmark!(benchCreateCSRStub)(100); 964 writeln("Benchmarking 100 calls, duration ", dur); 965 stdout.flush; 966 } 967 968 /* ------------------------------------------------------------------------ */ 969