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 = C_BIO_new_BIO_s_mem(); 35 scope(exit) C_BIO_free(bio); 36 C_BN_print(bio, bn); 37 char[2048] buffer; 38 auto rc = C_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 = C_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 = C_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; 113 auto s = Base64URLNoPadding.encode(t); 114 } else { 115 ubyte[] tt = (cast(ubyte*)t.ptr)[0..t.length]; 116 import std.base64; 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; 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 = C_convertDERtoPEM(der.ptr, der.length.to!int); 173 /* Output data as data string */ 174 return cast(string)(toVector(pemBio)); 175 } 176 177 extern(C) ASN1_TIME * C_X509_get_notAfter(const char* certPtr, int certLen); 178 179 /** Extract expiry date from a PEM encoded Zertificate 180 * 181 * Params: 182 * cert = PEM encoded certificate to query 183 * extractor = function or delegate process an ASN1_TIME* argument. 184 */ 185 T extractExpiryData(T, alias extractor)(const(char[]) cert) 186 { 187 ASN1_TIME * t = C_X509_get_notAfter(cert.ptr, cert.length.to!int); 188 T rc = extractor(t); 189 return rc; 190 } 191 192 /* ----------------------------------------------------------------------- */ 193 194 /** Sign a given string with an SHA256 hash 195 * 196 * Param: 197 * s = string to sign 198 * privateKey = signing key to use 199 * 200 * Returns: 201 * A SHA256 signature on provided data 202 * See: https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying 203 */ 204 char[] signDataWithSHA256(char[] s, EVP_PKEY* privateKey) 205 { 206 size_t signatureLength = 0; 207 char[1024] sig; 208 auto rc = C_signDataWithSHA256(s.ptr, s.length.to!int, privateKey, sig.ptr, sig.length.to!int); 209 if (rc == 0) 210 { 211 throw new AcmeException("Error creating SHA256 digest in final signature"); 212 } 213 return base64EncodeUrlSafe(sig[0..rc]); 214 } 215 216 version (HAS_WORKING_SSL) 217 { 218 /** Initialize SSL library 219 * 220 * Do any kind of initialization here. 221 * Returns: 222 * true or false 223 */ 224 bool SSL_OpenLibrary() 225 { 226 /* Load the human readable error strings for libcrypto */ 227 OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, null); 228 229 /* Load all digest and cipher algorithms */ 230 //OpenSSL_add_all_algorithms(); // Is a macro for 231 OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS 232 | OPENSSL_INIT_ADD_ALL_DIGESTS 233 | OPENSSL_INIT_LOAD_CONFIG, null); 234 return true; 235 } 236 237 /** Teardown SSL library 238 * 239 * Reverse anything done in SSL_OpenLibrary(). 240 */ 241 void SSL_CloseLibrary() 242 { 243 /* Clean up */ 244 OPENSSL_cleanup(); 245 } 246 247 /* http://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl 248 * http://www.codepool.biz/how-to-use-openssl-to-generate-x-509-certificate-request.html 249 */ 250 251 /** Make a x509 pkey 252 * 253 * Create a RSA private keys with 2048 bits 254 * Returns: pointer to EVP_PKEY structure 255 * @internal 256 */ 257 EVP_PKEY* SSL_x509_make_pkey(int bits = 4096) 258 { 259 EVP_PKEY * pkey; 260 pkey = EVP_PKEY_new(); 261 RSA * rsa; 262 rsa = RSA_generate_key( 263 bits, /* number of bits for the key - 2048 is a sensible value */ 264 RSA_F4, /* exponent - RSA_F4 is defined as 0x10001L */ 265 null, /* callback - can be null if we aren't displaying progress */ 266 null /* callback argument - not needed in this case */ 267 ); 268 EVP_PKEY_assign_RSA(pkey, rsa); 269 return pkey; 270 } 271 272 273 /** Add extension using V3 code: we can set the config file as null 274 * because we wont reference any other sections. 275 * 276 * Params: 277 * sk = pointer to STACK_OF(X509_EXTENSION 278 * nid = Extention ID 279 * value = value of nid 280 * Returns: 281 * bool_t: 0 == False, !=0 True 282 */ 283 private 284 bool add_req_ext(STACK_OF!X509_EXTENSION *sk, int nid, string value) 285 { 286 X509_EXTENSION *ex; 287 ex = X509V3_EXT_conf_nid(cast(LHASH_OF!(CONF_VALUE)*)null, cast(v3_ext_ctx*)null, nid, cast(char*)value.toStringz); 288 if (!ex) 289 return false; 290 sk_X509_EXTENSION_push(sk, ex); 291 return true; 292 } 293 294 /** Make a x509 CSR (cert signing request) 295 * @param pkey pointer to pkey struct to store 296 * @param dev_serial pointer to device serial string 297 * Returns: pointer to X509_REQ structure 298 */ 299 X509_REQ* SSL_x509_make_csr(EVP_PKEY* pkey, string[] domainNames) 300 { 301 assert(domainNames.length >= 1, "No domain names given."); 302 auto cnStr = domainNames[0].toStringz; 303 string[] extStrs; extStrs.length = domainNames.length - 1; 304 305 X509_REQ * x509_req; 306 x509_req = X509_REQ_new(); 307 assert(x509_req.req_info !is null, "The allocated X509_REQ* has req_info member set to NULL. This shouldn't be."); 308 309 X509_REQ_set_version(x509_req, 1); 310 X509_REQ_set_pubkey(x509_req, pkey); 311 312 X509_NAME * name; 313 name = X509_REQ_get_subject_name(x509_req); 314 assert (name !is null, "Can't read the req subject name struct."); 315 316 /* Setup some fields for the CSR */ 317 version(none) { 318 X509_NAME_add_entry_by_txt(name, cast(char*)("ST".ptr), MBSTRING_ASC, cast(ubyte*)("Niedersachsen".ptr), -1, -1, 0); 319 X509_NAME_add_entry_by_txt(name, cast(char*)("L".ptr), MBSTRING_ASC, cast(ubyte*)("Hannover".ptr), -1, -1, 0); 320 X509_NAME_add_entry_by_txt(name, cast(char*)("OU".ptr), MBSTRING_ASC, cast(ubyte*)("IT".ptr), -1, -1, 0); 321 X509_NAME_add_entry_by_txt(name, cast(char*)("O".ptr), MBSTRING_ASC, cast(ubyte*)("Vahanus ".ptr), -1, -1, 0); 322 X509_NAME_add_entry_by_txt(name, cast(char*)("C".ptr), MBSTRING_ASC, cast(ubyte*)("DE".ptr), -1, -1, 0); 323 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(dev_serial.toStringz), -1, -1, 0); 324 } 325 X509_NAME_add_entry_by_txt(name, cast(char*)("CN".ptr), MBSTRING_ASC, cast(ubyte*)(cnStr), -1, -1, 0); 326 /* Add other domainName as extension */ 327 if (domainNames.length > 1) 328 { 329 // We have multiple Subject Alternative Names 330 auto extensions = sk_X509_EXTENSION_new_null(); 331 if (extensions is null) { 332 throw new AcmeException("Unable to allocate Subject Alternative Name extensions"); 333 } 334 foreach (i, ref v ; domainNames[1..$]) 335 { 336 auto cstr = ("DNS:" ~ v).toStringz; 337 auto nid = X509V3_EXT_conf_nid(null, null, NID_subject_alt_name, cast(char*)cstr); 338 if (!sk_X509_EXTENSION_push(extensions, nid)) { 339 throw new AcmeException("Unable to add Subject Alternative Name to extensions"); 340 } 341 } 342 if (X509_REQ_add_extensions(x509_req, extensions) != 1) { 343 throw new AcmeException("Unable to add Subject Alternative Names to CSR"); 344 } 345 sk_X509_EXTENSION_pop_free(extensions, &X509_EXTENSION_free); 346 } 347 348 /* Code below might BREAK acception of CSR at ACME server. Leave it out for now. */ 349 version(none) { 350 STACK_OF(X509_EXTENSION) *exts = sk_X509_EXTENSION_new_null(); 351 352 // # Extensions for client certificates (`man x509v3_config`). 353 // basicConstraints = CA:FALSE 354 // nsCertType = client, email 355 // nsComment = "OpenSSL Generated Client Certificate" 356 // subjectKeyIdentifier = hash 357 // authorityKeyIdentifier = keyid,issuer 358 // keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment 359 // extendedKeyUsage = clientAuth, emailProtection 360 361 /* Add various extensions: standard extensions */ 362 add_req_ext(exts, NID_basic_constraints, "CA:FALSE"); 363 add_req_ext(exts, NID_key_usage, "critical, nonRepudiation, digitalSignature, keyEncipherment"); 364 add_req_ext(exts, NID_ext_key_usage, "clientAuth, emailProtection"); 365 add_req_ext(exts, NID_subject_key_identifier, "hash"); 366 add_req_ext(exts, NID_authority_key_identifier, "keyid,issuer"); 367 368 /* Some Netscape specific extensions */ 369 add_req_ext(exts, NID_netscape_cert_type, "client, email"); 370 add_req_ext(exts, NID_netscape_comment, "OpenSSL Generated Client Certificate"); 371 372 X509_REQ_add_extensions(x509_req, exts); 373 374 sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); 375 } 376 377 /* Sign the CSR with our PKEY */ 378 X509_REQ_sign(x509_req, pkey, EVP_sha1()); 379 return x509_req; 380 } 381 382 383 } 384 else 385 { 386 /* Here we import the autogenerated DI file for the C module */ 387 import std.stdint; 388 import acme.openssl_glues; 389 390 bool SSL_OpenLibrary() 391 { 392 return C_SSL_OpenLibrary(); 393 } 394 void SSL_CloseLibrary() 395 { 396 C_SSL_CloseLibrary(); 397 } 398 EVP_PKEY* SSL_x509_make_pkey(int bits) 399 { 400 return C_SSL_x509_make_pkey(bits); 401 } 402 X509_REQ* SSL_x509_make_csr(EVP_PKEY* pkey, string[] domainNames) 403 { 404 char*[] C_domainNames; 405 C_domainNames.length = domainNames.length; 406 foreach(i, ref v; domainNames) C_domainNames[i] = cast(char*)v.toStringz; 407 return C_SSL_x509_make_csr(pkey, cast(char**)(C_domainNames.ptr), domainNames.length.to!int); 408 } 409 } 410 411 412 413 /** Get a CSR as PEM string */ 414 char[] SSL_x509_get_PEM(X509_REQ* x509_req) 415 { 416 char* rs = C_SSL_x509_get_PEM(x509_req); 417 import std.string; 418 return rs.fromStringz; 419 } 420 421 /** Get a CSR as base64url-encoded DER string */ 422 char[] SSL_x509_get_DER_as_B64URL(X509_REQ* x509_req) 423 { 424 ubyte[1024] b; 425 auto rc = C_SSL_x509_get_DER(x509_req, b.ptr, b.length.to!int); 426 char[] rs = base64EncodeUrlSafe(b[0..rc]); 427 return rs; 428 } 429 430 /** Read a x509 pkey pem string from memory 431 */ 432 EVP_PKEY* SSL_x509_read_pkey_memory(const char[] pkeyString, RSA** rsaRef) 433 { 434 auto cstr = cast(char*)pkeyString.toStringz; 435 return C_SSL_x509_read_pkey_memory(cstr, rsaRef); 436 } 437 438 /** Save a x509 pkey to a file 439 * @param path pathname of file to write 440 * @param pkey pointer to pkey struct to store 441 * Returns: return value of PEM_write_PrivateKey() 442 * @internal 443 */ 444 int SSL_x509_write_pkey(char[] path, EVP_PKEY * pkey) 445 { 446 return C_SSL_x509_write_pkey( cast(char*)(path.toStringz), pkey); 447 } 448 449 /** Read a x509 pkey from a file 450 * @param path pathname of file to read 451 * Returns: pointer to EVP_PKEY, return value of PEM_write_PrivateKey() 452 * @internal 453 */ 454 EVP_PKEY * SSL_x509_read_pkey(char[] path) 455 { 456 return C_SSL_x509_read_pkey(cast(char*)(path.toStringz)); 457 } 458 459 460 /* ------------------------------------------------------------------------ */ 461 462 /** Create a SSL private key 463 * 464 * This functions creates an EVP_PKEY with 2048 bits. It's returned as 465 * PEM encoded text. 466 * 467 * Returns: 468 * pointer to pem encoded string containing EVP_PKEY private key. 469 */ 470 char[] openSSL_CreatePrivateKey(int bits = 4096) 471 { 472 import std.stdio; 473 //writeln("Create a SSL pKey."); 474 char[] rs; 475 char*cs = C_openSSL_CreatePrivateKey(bits); 476 rs = cs.fromStringz; 477 return rs; 478 } 479 unittest { 480 import std.stdio : writeln, writefln, stdout, stderr; 481 import std.datetime.stopwatch : benchmark; 482 483 /* Test Key Generation */ 484 writeln("Testing the SSL routines ported from C"); 485 writeln("--- Create a private key ---"); 486 stdout.flush; 487 char[] myPKey = openSSL_CreatePrivateKey(); 488 writeln("Got the following from library:\n", myPKey); 489 stdout.flush; 490 491 /* Benchmark Key Generation */ 492 writeln("--- Benchmark creating a private key ---"); 493 stdout.flush; 494 void benchCreateKeyStub() { 495 char[] tmp = openSSL_CreatePrivateKey(); 496 assert(tmp !is null && !tmp.empty, "Empty private key."); 497 } 498 auto dur = benchmark!(benchCreateKeyStub)(100); 499 writeln("Benchmarking 100 calls, duration ", dur); 500 stdout.flush; 501 } 502 503 /** Create a SSL cert signing request from a pkey and a serial number 504 * 505 * This functions creates an CertificateSignRequest (CSR) with 2048 bits. 506 * It's returned as PEM encoded text. 507 * 508 * Params: 509 * prkey = private key as PEM string 510 * domainNames = same custom data, e.g. a serial number 511 * Returns: 512 * pointer to bas64url encoded DER data! See RFC. 513 */ 514 char[] openSSL_CreateCertificateSignRequest(const char[] prkey, string[] domainNames) 515 { 516 BIO *bio; 517 int rc; 518 519 /* Get EVP_PKEY from PEM encoded string */ 520 EVP_PKEY* pkey; 521 RSA* rsa; 522 pkey = SSL_x509_read_pkey_memory(prkey, &rsa); 523 524 /* Create CSR from private key and serial number */ 525 526 X509_REQ* x509_req = SSL_x509_make_csr(pkey, domainNames); 527 assert (x509_req !is null, "Returned empty cert req."); 528 529 // /* Convert to PEM string */ 530 // auto rs = SSL_x509_get_PEM(x509_req); 531 532 /* Convert to DER with base64url-encoded data */ 533 auto rs = SSL_x509_get_DER_as_B64URL(x509_req); 534 C_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 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