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