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