1 
2 /** Small helpers for libCURL
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.evp;
13 import deimos.openssl.pem;
14 import deimos.openssl.rsa;
15 import deimos.openssl.x509v3;
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 = BIO_new(BIO_s_mem());
35 	scope(exit) BIO_free(bio);
36 	BN_print(bio, bn);
37 	char[2048] buffer;
38 	auto rc = 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 	int numBytes = BN_num_bytes(bn);
54 	ubyte[] buffer;
55 	buffer.length = numBytes;
56 
57 	/* Copy bytes of BIGNUM to our buffer */
58 	BN_bn2bin(bn, buffer.ptr);
59 
60 	return buffer;
61 }
62 
63 
64 /* ----------------------------------------------------------------------- */
65 
66 /** Export BIO contents as an array of chars
67  *
68  * Param:
69  *   bio - pointer to a BIO structure
70  * Returns:
71  *   An array of chars representing the BIO structure
72  */
73 char[] toVector(BIO * bio)
74 {
75 	enum buffSize = 1024;
76 	char[buffSize] buffer;
77 	char[] rc;
78 
79 	int count = 0;
80 	do
81 	{
82 		count = BIO_read(bio, buffer.ptr, buffer.length);
83 		if (count > 0)
84 		{
85 			rc ~= buffer[0..count];
86 		}
87 	}
88 	while (count > 0);
89 
90 	return rc;
91 }
92 
93 /** Export BIO contents as an array of immutable chars (string)
94  *
95  * Param:
96  *   bio - pointer to a BIO structure
97  * Returns:
98  *   An array of immutable chars representing the BIO structure
99  */
100 string toString(BIO *bio)
101 {
102 	char[] v = toVector(bio);
103 	return to!string(v);
104 }
105 
106 /* ----------------------------------------------------------------------- */
107 
108 /** Encode data as Base64
109  *
110  * We use openssl to do this since we're already linking to it. As an
111  * alternative we could also use the phobos routines.
112  *
113  * Params:
114  *  t - data to encode as base64
115  * Returns:
116  *  An array of chars with the base64 encoded data.
117  */
118 char[] base64Encode(T)(T t)
119 	if ( is(T : string) || is(T : char[]) || is(T : ubyte[]))
120 {
121 	BIO * bio = BIO_new(BIO_s_mem());
122 	BIO * b64 = BIO_new(BIO_f_base64());
123 
124 	// OpenSSL inserts new lines by default to make it look like PEM format.
125 	// Turn that off.
126 	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
127 
128 	BIO_push(b64, bio);
129 	if (BIO_write(b64, cast(void*)(t.ptr), t.length.to!int) <= 0 ||
130 		BIO_flush(b64) < 0)
131 	{
132 		throw new AcmeException("Can't encode data as base64.");
133 	}
134 	return toVector(bio);
135 }
136 
137 /** Encode data as URl-safe Base64
138  *
139  * We need url safe base64 encoding and openssl only gives us regular
140  * base64, so we convert it here. Also trim trailing '=' from data
141  * (see RFC).
142  *
143  * The following replacements are done:
144  *  * '+' is converted to '-'
145  *  * '/' is converted to '_'
146  *  * '=' terminates the output at this point, stripping all '=' chars
147  *
148  * Params:
149  *  t - data to encode as base64
150  * Returns:
151  *  An array of chars with the base64 encoded data.
152  */
153 char[] base64EncodeUrlSafe(T)(T t)
154 	if ( is(T : string) || is(T : char[]) || is(T : ubyte[]))
155 {
156 	/* Do a Standard Base64 Encode */
157 	char[] s = base64Encode(t);
158 
159 	/* Do the replacements */
160 	foreach (i, ref v; s)
161 	{
162 		     if (s[i] == '+') { s[i] = '-'; }
163 		else if (s[i] == '/') {	s[i] = '_';	}
164 		else if (s[i] == '=') {	s.length = i; break; }
165 	}
166 	return s;
167 }
168 
169 /** Encode BIGNUM data as URl-safe Base64
170  *
171  * We need url safe base64 encoding and openssl only gives us regular
172  * base64, so we convert it here. Also trim trailing '=' from data
173  * (see RFC).
174  *
175  * The following replacements are done:
176  *  * '+' is converted to '-'
177  *  * '/' is converted to '_'
178  *  * '=' terminates the output at this point, stripping all '=' chars
179  *
180  * Params:
181  *  bn - pointer to BIGNUM to encode as base64
182  * Returns:
183  *  An array of chars with the base64 encoded data.
184  */
185 char[] base64EncodeUrlSafe(const BIGNUM* bn)
186 {
187 	/* Get contents bytes of a BIGNUM */
188 	ubyte[] buffer = getBigNumberBytes(bn);
189 
190 	/* Encode the buffer as URL-safe base64 string */
191 	return base64EncodeUrlSafe(buffer);
192 }
193 
194 /** Calculate the SHA256 of a string
195  *
196  * We use openssl to do this since we're already linking to it. We could
197  * also use functions from the phobos library.
198  *
199  * Param:
200  *  s - string to calculate hash from
201  * Returns:
202  *  ubyte[SHA256_DIGEST_LENGTH] for the hash
203  */
204 ubyte[SHA256_DIGEST_LENGTH] sha256Encode(const char[] s)
205 {
206 	ubyte[SHA256_DIGEST_LENGTH] hash;
207 	SHA256_CTX sha256;
208 	if (!SHA256_Init(&sha256) ||
209 		!SHA256_Update(&sha256, s.ptr, s.length) ||
210 		!SHA256_Final(hash.ptr, &sha256))
211 	{
212 		throw new AcmeException("Error hashing string data");
213 	}
214 	return hash;
215 }
216 
217 /** Convert certificate from DER format to PEM format
218  *
219  * Params:
220  *   der - DER encoded certificate
221  * Returns:
222  *   a PEM-encoded certificate
223  */
224 string convertDERtoPEM(const char[] der)
225 {
226 	/* Write DER to BIO buffer */
227 	BIO* derBio = BIO_new(BIO_s_mem());
228 	BIO_write(derBio, cast(const(void)*)der.ptr, der.length.to!int);
229 
230 	/* Add conversion filter */
231 	X509* x509 = d2i_X509_bio(derBio, null);
232 
233 	/* Write DER through filter to as PEM to other BIO buffer */
234 	BIO* pemBio = BIO_new(BIO_s_mem());
235 	PEM_write_bio_X509(pemBio, x509);
236 
237 	/* Output data as data string */
238 	return toString(pemBio);
239 }
240 
241 /** Extract expiry date from a PEM encoded Zertificate
242  *
243  * Params:
244  *  cert - PEM encoded certificate to query
245  *  extractor - function or delegate process an ASN1_TIME* argument.
246  */
247 T extractExpiryData(T, alias extractor)(const(char[]) cert)
248 {
249 	BIO* bio = BIO_new(BIO_s_mem());
250 	if (BIO_write(bio, cast(const(void)*) cert.ptr, cert.length.to!int) <= 0)
251 	{
252 		throw new AcmeException("Can't write PEM data to BIO struct.");
253 	}
254 	X509* x509 = PEM_read_bio_X509(bio, null, null, null);
255 
256 	ASN1_TIME * t = X509_get_notAfter(x509);
257 
258 	T rc = extractor(t);
259 	return rc;
260 }
261 
262 /* ----------------------------------------------------------------------- */
263 
264 /// Return tuple of makeCertificateSigningRequest
265 alias tupleCsrPkey = Tuple!(string, "csr", string, "pkey");
266 
267 /** Create a CSR with our domains
268  *
269  * Params:
270  *   domainNames - Names of domains, first element is subject of cert
271  * Returns:
272  *   tupleCsrPkey containing CSr and PKey
273  */
274 tupleCsrPkey makeCertificateSigningRequest(string[] domainNames)
275 {
276 	if (domainNames.length < 1)
277 	{
278 		throw new AcmeException("We need at least one domain name.");
279 	}
280 
281 	BIGNUM* bn = BN_new();
282 	if (!BN_set_word(bn, RSA_F4))
283 	{
284 		throw new AcmeException("Can't set word.");
285 	}
286 
287 	RSA* rsa = RSA_new();
288 	enum bits = 2048;
289 	if (!RSA_generate_key_ex(rsa, bits, bn, null))
290 	{
291 		throw new AcmeException("Can't generate key.");
292 	}
293 
294 	/* Set first element of domainNames as cert CN subject */
295 	X509_REQ* req = X509_REQ_new();
296 	auto name = domainNames[0];
297 
298 	X509_NAME* cn = X509_REQ_get_subject_name(req);
299 	if (!X509_NAME_add_entry_by_txt(cn,
300 									"CN",
301 									MBSTRING_ASC,
302 									cast(const ubyte*)(name.toStringz),
303 									-1, -1, 0))
304 	{
305 		throw new AcmeException("Can't add CN entry.");
306 	}
307 
308 	/* Add other domainName as extension */
309 	if (domainNames.length > 1)
310 	{
311 		// We have multiple Subject Alternative Names
312 		auto extensions = sk_X509_EXTENSION_new_null();
313 		if (!extensions)
314 		{
315 			throw new AcmeException("Unable to allocate Subject Alternative Name extensions");
316 		}
317 
318 		foreach (i, ref v ; domainNames)
319 		{
320 			auto cstr = ("DNS:" ~ v).toStringz;
321 			auto nid = X509V3_EXT_conf_nid(null, null, NID_subject_alt_name, cast(char*)cstr);
322 			if (!sk_X509_EXTENSION_push(extensions, nid))
323 			{
324 				throw new AcmeException("Unable to add Subject Alternative Name to extensions");
325 			}
326 		}
327 
328 		if (X509_REQ_add_extensions(req, extensions) != 1)
329 		{
330 			throw new AcmeException("Unable to add Subject Alternative Names to CSR");
331 		}
332 
333 		sk_X509_EXTENSION_pop_free(extensions, &X509_EXTENSION_free);
334 	}
335 
336 	EVP_PKEY* key = EVP_PKEY_new();
337 	if (!EVP_PKEY_assign_RSA(key, rsa))
338 	{
339 		throw new AcmeException("Can't set RSA key.");
340 	}
341 	rsa = null;     // rsa will be freed when key is freed.
342 
343 	BIO* keyBio = BIO_new(BIO_s_mem());
344 	if (PEM_write_bio_PrivateKey(keyBio, key, null, null, 0, null, null) != 1)
345 	{
346 		throw new AcmeException("Can't set private key.");
347 	}
348 
349 	string privateKey = toString(keyBio);
350 
351 	if (!X509_REQ_set_pubkey(req, key))
352 	{
353 		throw new AcmeException("Can't set subkey.");
354 	}
355 
356 	if (!X509_REQ_sign(req, key, EVP_sha256()))
357 	{
358 		throw new AcmeException("Can't sign.");
359 	}
360 
361 	BIO* reqBio = BIO_new(BIO_s_mem());
362 	if (i2d_X509_REQ_bio(reqBio, req) < 0)
363 	{
364 		throw new AcmeException("Can't setup sign request");
365 	}
366 
367 	tupleCsrPkey rc = tuple(base64EncodeUrlSafe(toVector(reqBio)).to!string, privateKey);
368 	return rc;
369 }
370 
371 /* ----------------------------------------------------------------------- */
372