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 * Note: 11 * The D binding seem to be outdated or otherwise broken. At least some 12 * code only works in C. That's why a C stub was added. However, the code 13 * is still available in D below in hope that things can be fixed later. 14 */ 15 module acme.openssl_helpers; 16 17 import std.conv; 18 import std.string; 19 import std.typecons; 20 21 import acme.exception; 22 23 /* ----------------------------------------------------------------------- */ 24 25 /** Get the contents of a big number as string 26 * 27 * Param: 28 * bn = pointer to a big number structure 29 * Returns: 30 * a string representing the BIGNUM 31 */ 32 string getBigNumber(BIGNUM* bn) 33 { 34 BIO * bio = stubSSL_BIO_new_BIO_s_mem(); 35 scope(exit) stubSSL_BIO_free(bio); 36 stubSSL_BN_print(bio, bn); 37 char[2048] buffer; 38 auto rc = stubSSL_BIO_gets(bio, buffer.ptr, buffer.length); 39 auto num = buffer[0..rc].to!string; 40 return num; 41 } 42 43 /** Get the content bytes of a big number as string 44 * 45 * Param: 46 * bn = pointer to a big number structure 47 * Returns: 48 * a string representing the BIGNUM 49 */ 50 ubyte[] getBigNumberBytes(const BIGNUM* bn) 51 { 52 /* Get number of bytes to store a BIGNUM */ 53 ubyte[2048] buffer; 54 auto numBytes = stubSSL_getBigNumberBytes(bn, cast(void*)buffer.ptr, buffer.length); 55 return buffer[0..numBytes].dup; 56 } 57 58 59 /* ----------------------------------------------------------------------- */ 60 61 /** Export BIO contents as an array of chars 62 * 63 * Param: 64 * bio = pointer to a BIO structure 65 * Returns: 66 * An array of chars representing the BIO structure 67 */ 68 char[] toVector(BIO * bio) 69 { 70 enum uint buffSize = 1024; 71 char[buffSize] buffer; 72 char[] rc; 73 74 int count = 0; 75 do 76 { 77 count = stubSSL_BIO_read(bio, buffer.ptr, buffer.length); 78 if (count > 0) 79 { 80 rc ~= buffer[0..count]; 81 } 82 } 83 while (count > 0); 84 85 return rc; 86 } 87 88 89 90 /* ----------------------------------------------------------------------- */ 91 92 /** Encode data as URl-safe Base64 93 * 94 * We need url safe base64 encoding and openssl only gives us regular 95 * base64, so we convert it here. Also trim trailing '=' from data 96 * (see RFC). 97 * 98 * The following replacements are done: 99 * * '+' is converted to '-' 100 * * '/' is converted to '_' 101 * * '=' terminates the output at this point, stripping all '=' chars 102 * 103 * Params: 104 * t = data to encode as base64 105 * Returns: 106 * An array of chars with the base64 encoded data. 107 */ 108 char[] base64EncodeUrlSafe(T)(T t) 109 if ( is(T : string) || is(T : char[]) || is(T : ubyte[])) 110 { 111 static if (is(T : ubyte[])) { 112 import std.base64 : Base64URLNoPadding; 113 auto s = Base64URLNoPadding.encode(t); 114 } else { 115 ubyte[] tt = (cast(ubyte*)t.ptr)[0..t.length]; 116 import std.base64 : Base64URLNoPadding; 117 auto s = Base64URLNoPadding.encode(tt); 118 } 119 return s; 120 } 121 122 /** Encode BIGNUM data as URl-safe Base64 123 * 124 * We need url safe base64 encoding and openssl only gives us regular 125 * base64, so we convert it here. Also trim trailing '=' from data 126 * (see RFC). 127 * 128 * The following replacements are done: 129 * * '+' is converted to '-' 130 * * '/' is converted to '_' 131 * * '=' terminates the output at this point, stripping all '=' chars 132 * 133 * Params: 134 * bn = pointer to BIGNUM to encode as base64 135 * Returns: 136 * An array of chars with the base64 encoded data. 137 */ 138 char[] base64EncodeUrlSafe(const BIGNUM* bn) 139 { 140 /* Get contents bytes of a BIGNUM */ 141 ubyte[] buffer = getBigNumberBytes(bn); 142 143 /* Encode the buffer as URL-safe base64 string */ 144 return base64EncodeUrlSafe(buffer); 145 } 146 147 /** Calculate the SHA256 of a string 148 * 149 * We use openssl to do this since we're already linking to it. We could 150 * also use functions from the phobos library. 151 * 152 * Param: 153 * s = string to calculate hash from 154 * Returns: 155 * ubyte[SHA256_DIGEST_LENGTH] for the hash 156 */ 157 auto sha256Encode(const char[] s) 158 { 159 import std.digest.sha : sha256Of; 160 return sha256Of(s); 161 } 162 163 /** Convert certificate from DER format to PEM format 164 * 165 * Params: 166 * der = DER encoded certificate 167 * Returns: 168 * a PEM-encoded certificate 169 */ 170 string convertDERtoPEM(const char[] der) 171 { 172 BIO* pemBio = stubSSL_convertDERtoPEM(der.ptr, der.length.to!int); 173 /* Output data as data string */ 174 return cast(string)(toVector(pemBio)); 175 } 176 177 /** Extract expiry date from a PEM encoded Zertificate 178 * 179 * Params: 180 * cert = PEM encoded certificate to query 181 * extractor = function or delegate process an ASN1_TIME* argument. 182 */ 183 T extractExpiryData(T, alias extractor)(const(char[]) cert) 184 { 185 ASN1_TIME * t = stubSSL_X509_getNotAfter(cert.ptr, cert.length.to!int); 186 T rc = extractor(t); 187 return rc; 188 } 189 190 /* ----------------------------------------------------------------------- */ 191 192 /** Sign a given string with an SHA256 hash 193 * 194 * Param: 195 * s = string to sign 196 * privateKey = signing key to use 197 * 198 * Returns: 199 * A SHA256 signature on provided data 200 * See: https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying 201 */ 202 char[] signDataWithSHA256(char[] s, EVP_PKEY* privateKey) 203 { 204 char[1024] sig; 205 auto rc = stubSSL_signDataWithSHA256(s.ptr, s.length.to!int, privateKey, sig.ptr, sig.length.to!int); 206 if (rc == 0) 207 { 208 throw new AcmeException("Error creating SHA256 digest in final signature"); 209 } 210 return base64EncodeUrlSafe(sig[0..rc]); 211 } 212 213 version (HAS_WORKING_SSL) 214 { 215 /** Initialize SSL library 216 * 217 * Do any kind of initialization here. 218 * Returns: 219 * true or false 220 */ 221 bool SSL_OpenLibrary() 222 { 223 /* Load the human readable error strings for libcrypto */ 224 OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, null); 225 226 /* Load all digest and cipher algorithms */ 227 //OpenSSL_add_all_algorithms(); // Is a macro for 228 OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS 229 | OPENSSL_INIT_ADD_ALL_DIGESTS 230 | OPENSSL_INIT_LOAD_CONFIG, null); 231 return true; 232 } 233 234 /** Teardown SSL library 235 * 236 * Reverse anything done in SSL_OpenLibrary(). 237 */ 238 void SSL_CloseLibrary() 239 { 240 /* Clean up */ 241 OPENSSL_cleanup(); 242 } 243 244 /* http://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl 245 * http://www.codepool.biz/how-to-use-openssl-to-generate-x-509-certificate-request.html 246 */ 247 248 /** Make a x509 pkey 249 * 250 * Create a RSA private keys with 2048 bits 251 * Returns: pointer to EVP_PKEY structure 252 * @internal 253 */ 254 EVP_PKEY* SSL_x509_make_pkey(int bits = 4096) 255 { 256 EVP_PKEY * pkey; 257 pkey = EVP_PKEY_new(); 258 RSA * rsa; 259 rsa = RSA_generate_key( 260 bits, /* number of bits for the key - 2048 is a sensible value */ 261 RSA_F4, /* exponent - RSA_F4 is defined as 0x10001L */ 262 null, /* callback - can be null if we aren't displaying progress */ 263 null /* callback argument - not needed in this case */ 264 ); 265 EVP_PKEY_assign_RSA(pkey, rsa); 266 return pkey; 267 } 268 269 270 /** Add extension using V3 code: we can set the config file as null 271 * because we wont reference any other sections. 272 * 273 * Params: 274 * sk = pointer to STACK_OF(X509_EXTENSION 275 * nid = Extention ID 276 * value = value of nid 277 * Returns: 278 * bool_t: 0 == False, !=0 True 279 */ 280 private 281 bool add_req_ext(STACK_OF!X509_EXTENSION *sk, int nid, string value) 282 { 283 X509_EXTENSION *ex; 284 ex = X509V3_EXT_conf_nid(cast(LHASH_OF!(CONF_VALUE)*)null, cast(v3_ext_ctx*)null, nid, cast(char*)value.toStringz); 285 if (!ex) 286 return false; 287 sk_X509_EXTENSION_push(sk, ex); 288 return true; 289 } 290 291 /** Make a x509 CSR (cert signing request) 292 * @param pkey pointer to pkey struct to store 293 * @param dev_serial pointer to device serial string 294 * Returns: pointer to X509_REQ structure 295 */ 296 X509_REQ* SSL_x509_make_csr(EVP_PKEY* pkey, string[] domainNames) 297 { 298 assert(domainNames.length >= 1, "No domain names given."); 299 auto cnStr = domainNames[0].toStringz; 300 string[] extStrs; extStrs.length = domainNames.length - 1; 301 302 X509_REQ * x509_req; 303 x509_req = X509_REQ_new(); 304 assert(x509_req.req_info !is null, "The allocated X509_REQ* has req_info member set to NULL. This shouldn't be."); 305 306 X509_REQ_set_version(x509_req, 1); 307 X509_REQ_set_pubkey(x509_req, pkey); 308 309 X509_NAME * name; 310 name = X509_REQ_get_subject_name(x509_req); 311 assert (name !is null, "Can't read the req subject name struct."); 312 313 /* Setup some fields for the CSR */ 314 version(none) { 315 X509_NAME_add_entry_by_txt(name, cast(char*)("ST".ptr), MBSTRING_ASC, cast(ubyte*)("Niedersachsen".ptr), -1, -1, 0); 316 X509_NAME_add_entry_by_txt(name, cast(char*)("L".ptr), MBSTRING_ASC, cast(ubyte*)("Hannover".ptr), -1, -1, 0); 317 X509_NAME_add_entry_by_txt(name, cast(char*)("OU".ptr), MBSTRING_ASC, cast(ubyte*)("IT".ptr), -1, -1, 0); 318 X509_NAME_add_entry_by_txt(name, cast(char*)("O".ptr), MBSTRING_ASC, cast(ubyte*)("Vahanus ".ptr), -1, -1, 0); 319 X509_NAME_add_entry_by_txt(name, cast(char*)("C".ptr), MBSTRING_ASC, cast(ubyte*)("DE".ptr), -1, -1, 0); 320 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(dev_serial.toStringz), -1, -1, 0); 321 } 322 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(cnStr), -1, -1, 0); 323 /* Add other domainName as extension */ 324 if (domainNames.length > 1) 325 { 326 // We have multiple Subject Alternative Names 327 auto extensions = sk_X509_EXTENSION_new_null(); 328 if (extensions is null) { 329 throw new AcmeException("Unable to allocate Subject Alternative Name extensions"); 330 } 331 foreach (i, ref v ; domainNames[1..$]) 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 throw new AcmeException("Unable to add Subject Alternative Name to extensions"); 337 } 338 } 339 if (X509_REQ_add_extensions(x509_req, extensions) != 1) { 340 throw new AcmeException("Unable to add Subject Alternative Names to CSR"); 341 } 342 sk_X509_EXTENSION_pop_free(extensions, &X509_EXTENSION_free); 343 } 344 345 /* Code below might BREAK acception of CSR at ACME server. Leave it out for now. */ 346 version(none) { 347 STACK_OF(X509_EXTENSION) *exts = sk_X509_EXTENSION_new_null(); 348 349 // # Extensions for client certificates (`man x509v3_config`). 350 // basicConstraints = CA:FALSE 351 // nsCertType = client, email 352 // nsComment = "OpenSSL Generated Client Certificate" 353 // subjectKeyIdentifier = hash 354 // authorityKeyIdentifier = keyid,issuer 355 // keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment 356 // extendedKeyUsage = clientAuth, emailProtection 357 358 /* Add various extensions: standard extensions */ 359 add_req_ext(exts, NID_basic_constraints, "CA:FALSE"); 360 add_req_ext(exts, NID_key_usage, "critical, nonRepudiation, digitalSignature, keyEncipherment"); 361 add_req_ext(exts, NID_ext_key_usage, "clientAuth, emailProtection"); 362 add_req_ext(exts, NID_subject_key_identifier, "hash"); 363 add_req_ext(exts, NID_authority_key_identifier, "keyid,issuer"); 364 365 /* Some Netscape specific extensions */ 366 add_req_ext(exts, NID_netscape_cert_type, "client, email"); 367 add_req_ext(exts, NID_netscape_comment, "OpenSSL Generated Client Certificate"); 368 369 X509_REQ_add_extensions(x509_req, exts); 370 371 sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); 372 } 373 374 /* Sign the CSR with our PKEY */ 375 X509_REQ_sign(x509_req, pkey, EVP_sha1()); 376 return x509_req; 377 } 378 379 380 } 381 else 382 { 383 /* Here we import the autogenerated DI file for the C module */ 384 import std.stdint; 385 import acme.openssl_glues; 386 387 /** Initialize library */ 388 void SSL_OpenLibrary() 389 { 390 stubSSL_OpenLibrary(); 391 } 392 /** Close library */ 393 void SSL_CloseLibrary() 394 { 395 stubSSL_CloseLibrary(); 396 } 397 /** Make a private key */ 398 EVP_PKEY* SSL_x509_make_pkey(int bits) 399 { 400 return stubSSL_EVP_PKEY_makePrivateKey(bits); 401 } 402 /** Make a CSR */ 403 X509_REQ* SSL_x509_make_csr(EVP_PKEY* pkey, string[] domainNames) 404 { 405 char*[] C_domainNames; 406 C_domainNames.length = domainNames.length; 407 foreach(i, ref v; domainNames) C_domainNames[i] = cast(char*)v.toStringz; 408 return stubSSL_X509_REQ_makeCSR(pkey, cast(char**)(C_domainNames.ptr), domainNames.length.to!int); 409 } 410 } 411 412 413 414 /** Get a CSR as PEM string */ 415 char[] SSL_x509_get_PEM(X509_REQ* x509_req) 416 { 417 char* rs = stubSSL_X509_REQ_getAsPEM(x509_req); 418 import std.string : fromStringz; 419 return rs.fromStringz; 420 } 421 422 /** Get a CSR as base64url-encoded DER string */ 423 char[] SSL_x509_get_DER_as_B64URL(X509_REQ* x509_req) 424 { 425 ubyte[2048] b; 426 auto rc = stubSSL_X509_REQ_getAsDER(x509_req, b.ptr, b.length.to!int); 427 char[] rs = base64EncodeUrlSafe(b[0..rc]); 428 return rs; 429 } 430 431 /** Read a x509 pkey pem string from memory 432 */ 433 EVP_PKEY* SSL_x509_read_pkey_memory(const char[] pkeyString, RSA** rsaRef) 434 { 435 auto cstr = cast(char*)pkeyString.toStringz; 436 return stubSSL_EVP_PKEY_readPkeyFromMemory(cstr, rsaRef); 437 } 438 439 /** Save a x509 pkey to a file 440 * @param path pathname of file to write 441 * @param pkey pointer to pkey struct to store 442 * Returns: return value of PEM_write_PrivateKey() 443 * @internal 444 */ 445 int SSL_x509_write_pkey(char[] path, EVP_PKEY * pkey) 446 { 447 return stubSSL_EVP_PKEY_writePrivateKey( cast(char*)(path.toStringz), pkey); 448 } 449 450 /** Read a x509 pkey from a file 451 * @param path pathname of file to read 452 * Returns: pointer to EVP_PKEY, return value of PEM_write_PrivateKey() 453 * @internal 454 */ 455 EVP_PKEY * SSL_x509_read_pkey(char[] path) 456 { 457 return stubSSL_EVP_PKEY_readPrivateKey(cast(char*)(path.toStringz)); 458 } 459 460 461 /* ------------------------------------------------------------------------ */ 462 463 /** Create a SSL private key 464 * 465 * This functions creates an EVP_PKEY with 2048 bits. It's returned as 466 * PEM encoded text. 467 * 468 * Returns: 469 * pointer to pem encoded string containing EVP_PKEY private key. 470 */ 471 char[] openSSL_CreatePrivateKey(int bits = 4096) 472 { 473 //import std.stdio; 474 //writeln("Create a SSL pKey."); 475 char[] rs; 476 char* cs = stubSSL_createPrivateKey(bits); 477 rs = cs.fromStringz; 478 return rs; 479 } 480 unittest { 481 import std.stdio : writeln, writefln, stdout, stderr; 482 import std.datetime.stopwatch : benchmark; 483 484 /* Test Key Generation */ 485 writeln("Testing the SSL routines ported from C"); 486 writeln("--- Create a private key ---"); 487 stdout.flush; 488 char[] myPKey = openSSL_CreatePrivateKey(); 489 writeln("Got the following from library:\n", myPKey); 490 stdout.flush; 491 492 /* Benchmark Key Generation */ 493 writeln("--- Benchmark creating a private key ---"); 494 stdout.flush; 495 void benchCreateKeyStub() { 496 const char[] tmp = openSSL_CreatePrivateKey(); 497 assert(tmp !is null && !tmp.empty, "Empty private key."); 498 } 499 auto dur = benchmark!(benchCreateKeyStub)(100); 500 writeln("Benchmarking 100 calls, duration ", dur); 501 stdout.flush; 502 } 503 504 /** Create a SSL cert signing request from a pkey and a serial number 505 * 506 * This functions creates an CertificateSignRequest (CSR) with 2048 bits. 507 * It's returned as PEM encoded text. 508 * 509 * Params: 510 * prkey = private key as PEM string 511 * domainNames = same custom data, e.g. a serial number 512 * Returns: 513 * pointer to bas64url encoded DER data! See RFC. 514 */ 515 char[] openSSL_CreateCertificateSignRequest(const char[] prkey, string[] domainNames) 516 { 517 /* Get EVP_PKEY from PEM encoded string */ 518 EVP_PKEY* pkey; 519 RSA* rsa; 520 pkey = SSL_x509_read_pkey_memory(prkey, &rsa); 521 522 /* Create CSR from private key and serial number */ 523 524 X509_REQ* x509_req = SSL_x509_make_csr(pkey, domainNames); 525 assert (x509_req !is null, "Returned empty cert req."); 526 527 /* Convert to PEM string */ 528 auto pemStr = SSL_x509_get_PEM(x509_req); 529 import std.stdio : writeln; 530 writeln("CSR(PEM):", pemStr); 531 532 /* Convert to DER with base64url-encoded data */ 533 auto rs = SSL_x509_get_DER_as_B64URL(x509_req); 534 stubSSL_EVP_PKEY_free(pkey); 535 return rs; 536 } 537 unittest { 538 import std.stdio : writeln, writefln, stdout, stderr; 539 import std.datetime.stopwatch : benchmark; 540 541 /* Test Key Generation */ 542 writeln("Testing the CSR-creation routines ported from C"); 543 writeln("--- Create a private key ---"); 544 stdout.flush; 545 char[] myPKey = openSSL_CreatePrivateKey(); 546 writeln("Got the following from library:\n", myPKey); 547 stdout.flush; 548 char[] myCSR = openSSL_CreateCertificateSignRequest(myPKey, [ "bodylove.myds.me" ]); 549 writeln("Got the following CSR from library:\n", myCSR); 550 551 /* Benchmark CSR Generation */ 552 writeln("--- Benchmark creating a CSR ---"); 553 stdout.flush; 554 void benchCreateCSRStub() { 555 const char[] tmp = openSSL_CreateCertificateSignRequest(myPKey, [ "bodylove.myds.me" ]); 556 assert(tmp !is null && !tmp.empty, "Empty CSR."); 557 } 558 auto dur = benchmark!(benchCreateCSRStub)(100); 559 writeln("Benchmarking 100 calls, duration ", dur); 560 stdout.flush; 561 } 562 563 /* ------------------------------------------------------------------------ */ 564