1 
2 /** Simple client for ACME protocol
3  *
4  * This software provides a simple and minimalistic ACME client. It
5  * provides no fancy options, but sticks with the most common settings.
6  */
7 module acme.acme_client;
8 
9 import std.conv;
10 import std.datetime;
11 import std.json;
12 import std.net.curl;
13 import std.stdio;
14 import std.string;
15 import std.typecons;
16 
17 import deimos.openssl.asn1;
18 import deimos.openssl.evp;
19 import deimos.openssl.pem;
20 import deimos.openssl.rsa;
21 import deimos.openssl.x509v3;
22 
23 import acme;
24 import acme.exception;
25 import acme.curl_helpers;
26 import acme.openssl_helpers;
27 
28 /* ------------------------------------------------------------------------ */
29 
30 enum directoryUrlProd = "https://acme-v02.api.letsencrypt.org/directory";
31 enum directoryUrlStaging = "https://acme-staging-v02.api.letsencrypt.org/directory";
32 
33 version (STAGING)
34 	enum directoryUrlInit = directoryUrlStaging;
35 else
36 	enum directoryUrlInit = directoryUrlProd;
37 
38 /* ------------------------------------------------------------------------ */
39 
40 /** This structure stores the resource url of the ACME server
41  */
42 struct AcmeResources
43 {
44 	string nonce;            /// The Nonce for the next JWS transfer
45 
46 	string directoryUrl;     /// Initial config url to directory resource
47 	JSONValue directoryJson; /// JSON string returned returned from directoryURL
48 
49 	string newNOnceUrl;      /// Url to newNonce resource
50 	string newAccountUrl;    /// Url to newAccount resource
51 	string newOrderUrl;      /// Url to newOrder resource
52 	string newAuthZUrl;      /// Url to newAuthz resource
53 	string revokeCrtUrl;     /// Url to revokeCert resource
54 	string keyChangeUrl;     /// Url to keyChange resource
55 
56 	string metaJson;         /// Metadata as JSON string (undecoded)
57 
58 	// FIXME
59 	string accountUrl;       /// Account Url for a JWK.
60 	string newCertUrl;
61 	string newRegUrl;
62 
63 	void init(string initstr = directoryUrlInit) {
64 		directoryUrl = initstr;
65 	}
66 	void decodeDirectoryJson(const(char[]) directory)
67 	{
68 		directoryJson = parseJSON(directory);
69 		alias json = directoryJson;
70 		if ("keyChange" in json) this.keyChangeUrl = json["keyChange"].str;
71 		if ("newAccount" in json) this.newAccountUrl = json["newAccount"].str;
72 		if ("newNonce" in json) this.newNOnceUrl = json["newNonce"].str;
73 		if ("newOrder" in json) this.newOrderUrl = json["newOrder"].str;
74 		if ("revokeCert" in json) this.revokeCrtUrl = json["revokeCert"].str;
75 
76 		if ("newAuthz" in json) this.newAuthZUrl = json["newAuthz"].str;
77 		if ("newCert" in json) this.newCertUrl = json["newCert"].str;
78 		if ("newReg" in json) this.newRegUrl = json["newReg"].str;
79 
80 		if ("meta" in json) this.metaJson = json["meta"].toJSON;
81 	}
82 	void getResources()
83 	{
84 		try
85 		{
86 			char[] directory = get(this.directoryUrl);
87 			decodeDirectoryJson(directory);
88 		}
89 		catch (Exception e)
90 		{
91 			string msg = "Unable to initialize resource url from " ~ this.directoryUrl ~ ": " ~ e.msg;
92 			throw new AcmeException(msg, __FILE__, __LINE__, e );
93 		}
94 	}
95 }
96 
97 unittest
98 {
99 	string dirTestData = q"({
100     "Ca1Xc_O0Nwk": "https:\/\/community.letsencrypt.org\/t\/adding-random-entries-to-the-directory\/33417",
101     "keyChange": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/key-change",
102     "meta": {
103         "caaIdentities": [
104             "letsencrypt.org"
105         ],
106         "termsOfService": "https:\/\/letsencrypt.org\/documents\/LE-SA-v1.2-November-15-2017.pdf",
107         "website": "https:\/\/letsencrypt.org\/docs\/staging-environment\/"
108     },
109     "newAccount": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-acct",
110     "newAuthz": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-authz",
111     "newNonce": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-nonce",
112     "newOrder": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/new-order",
113     "revokeCert": "https:\/\/acme-staging-v02.api.letsencrypt.org\/acme\/revoke-cert"
114 })";
115 	void testcode(string url, bool dofullasserts = false )
116 	{
117 		AcmeResources test;
118 		if (url is null) {
119 			test.init();
120 			test.decodeDirectoryJson(dirTestData);
121 		} else {
122 			test.init(url);
123 			test.directoryUrl = url;
124 			test.getResources();
125 		}
126 		writeln("Received directory data :\n", test.directoryJson.toPrettyString);
127 		assert( test.directoryUrl !is null, "Shouldn't be null");
128 
129 		assert( test.keyChangeUrl !is null, "Shouldn't be null");
130 		assert( test.newAccountUrl !is null, "Shouldn't be null");
131 		assert( test.newNOnceUrl !is null, "Shouldn't be null");
132 		assert( test.newOrderUrl !is null, "Shouldn't be null");
133 		assert( test.revokeCrtUrl !is null, "Shouldn't be null");
134 		assert( test.metaJson !is null, "Shouldn't be null");
135 		if (dofullasserts) {
136 			assert( test.newAuthZUrl !is null, "Shouldn't be null");
137 		}
138 	}
139 	writeln("**** Testing AcmeResources : Decode test vector");
140 	testcode(null, true);
141 	writeln("**** Testing AcmeResources : Use staging server : ", directoryUrlStaging);
142 	testcode(directoryUrlStaging);
143 	writeln("**** Testing AcmeResources : Use production server : ", directoryUrlProd);
144 	testcode(directoryUrlProd);
145 }
146 
147 /* ------------------------------------------------------------------------ */
148 
149 /** An openssl certificate */
150 struct Certificate
151 {
152 	/** The full CA chain with cert */
153 	string fullchain;
154 	/** The private key to sign requests */
155 	string privkey;
156 
157 	// Note that neither of the 'Expiry' calls below require 'privkey'
158 	// to be set; they only rely on 'fullchain'.
159 
160 	/**
161 		Returns the number of seconds since 1970, i.e., epoch time.
162 
163 		Due to openssl quirkiness there might be a little drift
164 		from a strictly accurate result, but it should be close
165 		enough for the purpose of determining whether the certificate
166 		needs to be renewed.
167 	*/
168 	DateTime getExpiry() const
169 	{
170 		static const(DateTime) extractor(const ASN1_TIME * t)
171 		{
172 			// See this link for issues in converting from ASN1_TIME to epoch time.
173 			// https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion
174 
175 			int days, seconds;
176 			if (!ASN1_TIME_diff(&days, &seconds, null, t))
177 			{
178 				throw new AcmeException("Can't get time diff.");
179 			}
180 			// Hackery here, since the call to time(0) will not necessarily match
181 			// the equivilent call openssl just made in the 'diff' call above.
182 			// Nonetheless, it'll be close at worst.
183 			auto dt = DateTime( Date(0) );
184 			dt += dur!"seconds"(seconds + days * 3600 * 24);
185 			return dt;
186 		}
187 		DateTime rc = extractExpiryData!(DateTime, extractor)(this.fullchain);
188 		return rc;
189 	}
190 
191 	/** Returns the 'Not After' result that openssl would display if
192 		running the following command.
193 
194 			openssl x509 -noout -in fullchain.pem -text
195 
196 		For example:
197 
198 			May  6 21:15:03 2018 GMT
199 	*/
200 	string getExpiryDisplay() const
201 	{
202 		string extractor(const ASN1_TIME * t)
203 		{
204 			BIO* b = BIO_new(BIO_s_mem());
205 			if (!ASN1_TIME_print(b, t))
206 			{
207 				throw new AcmeException("Can't print expiry time.");
208 			}
209 			return toString(b);
210 		}
211 		return extractExpiryData!(string, extractor)(this.fullchain);
212 	}
213 }
214 
215 /* ------------------------------------------------------------------------ */
216 
217 /** A simple ACME v2 client
218  *
219  * This class implements the ACME v2 protocol to obtain signed SSL
220  * certificates.
221  */
222 class AcmeClient
223 {
224 private:
225 	EVP_PKEY*   privateKey_;     /// Copy of private key as ASC PEM
226 
227 	JSONValue   jwkData_;        /// JWK object as JSONValue tree
228 	string      jwkString_;      /// JWK as plain JSON string
229 	ubyte[]     jwkSHAHash_;     /// The SHA256 hash value of jwkString_
230 	string      jwkThumbprint_;  /// Base64 url-safe string of jwkSHAHash_
231 
232 	/** Create and send a JWS request with payload to a ACME enabled CA server
233 	 *
234 	 * Still unfinished template to build the JWS object. This code must be
235 	 * refactored later.
236 	 *
237 	 * Params:
238 	 *  T - return type
239 	 *  useKID - useKID
240 	 *  url - Url to post to
241 	 *  payload - data to send
242 	 *  status - pointer to StatusLine
243 	 *  rheaders - Pointer to ResponseHeaders received from server, or null
244 	 *
245 	 * See: https://tools.ietf.org/html/rfc7515
246 	 */
247 	T sendRequest(T, bool useKID = true)(string url, string payload, HTTP.StatusLine* status = null, string[string]* rheaders = null)
248 			if ( is(T : string) || is(T : char[]) || is(T : ubyte[]))
249 	{
250 		string nonce = this.acmeRes.nonce;
251 		assert(nonce !is null && !nonce.empty, "Invalid Nonce value.");
252 		writeln("Using NOnce: ", nonce);
253 
254 		/* Create protection data */
255 		JSONValue jvReqHeader;
256 		jvReqHeader["alg"] = "RS256";
257 		static if (useKID)
258 			jvReqHeader["kid"] = acmeRes.accountUrl;
259 		else
260 			jvReqHeader["jwk"] = jwkData_;
261 		jvReqHeader["nonce"] = nonce;
262 		jvReqHeader["url"] = url;
263 		char[] protectd = jvReqHeader.toJSON.dup;
264 
265 		protectd = base64EncodeUrlSafe(protectd);
266 
267 		char[] payld = base64EncodeUrlSafe(payload);
268 
269 		auto signData = protectd ~ "." ~ payld;
270 		//writefln("Data to sign: %s", signData);
271 		char[] signature = signDataWithSHA256(signData, privateKey_);
272 		//writefln("Signature: %s", signature);
273 
274 		JSONValue jvBody;
275 		jvBody["protected"] = protectd;
276 		jvBody["payload"] = payld;
277 		jvBody["signature"] = signature;
278 		char[] body_ = jvBody.toJSON.dup;
279 		//writefln("Body: %s", jvBody.toPrettyString);
280 
281 		auto response = doPost(url, body_, status, rheaders, &(acmeRes.nonce));
282 		if (rheaders) {
283 			writeln( "ResponseHeaders: ");
284 			foreach( v ; (*rheaders).byKey) {
285 				writeln("  ", v, " : ", (*rheaders)[v]);
286 				//~ if (v.toLower == "replay-nonce") {
287 					//~ acmeRes.nonce = (*rheaders)[v];
288 					//~ writeln("Setting new NOnce: ", acmeRes.nonce);
289 				//~ }
290 			}
291 		}
292 		//writeln( "Response: ", response);
293 
294 		return to!T(response);
295 	}
296 
297 public:
298 	AcmeResources acmeRes;       // The Url to the ACME resources
299 
300 
301 	/** Instanciate a AcmeClient using a private key for signing
302 	 *
303 	 *  Param:
304 	 *     accountPrivateKey - The signingKey is the Acme account private
305 	 *     		key used to sign requests to the acme CA, in pem format.
306 	 *  Throws: an instance of AcmeException on fatal or unexpected errors.
307 	 */
308 	this(string accountPrivateKey)
309 	{
310 		acmeRes.init();
311 		SSL_OpenLibrary();
312 
313 		/* Create the private key */
314 		RSA* rsa;
315 		privateKey_ = SSL_x509_read_pkey_memory(accountPrivateKey, &rsa);
316 
317 		// https://tools.ietf.org/html/rfc7638
318 		// JSON Web Key (JWK) Thumbprint
319 		JSONValue jvJWK;
320 		jvJWK["e"] = getBigNumberBytes(rsa.e).base64EncodeUrlSafe;
321 		jvJWK["kty"] = "RSA";
322 		jvJWK["n"] = getBigNumberBytes(rsa.n).base64EncodeUrlSafe;
323 		jwkData_ = jvJWK;
324 		jwkString_ = jvJWK.toJSON;
325 		jwkSHAHash_ = sha256Encode( jwkString_ );
326 		jwkThumbprint_ = jwkSHAHash_.base64EncodeUrlSafe.idup;
327 	}
328 	~this () {
329 		SSL_CloseLibrary();
330 	}
331 
332 	/** Call once after instantiating AcmeClient to setup parameters
333 	 *
334 	 * This function will fetch the directory from the ACME CA server and
335 	 * extracts the Urls.
336 	 *
337 	 * Also fetches the initial NOnce value for the next JWS transfer.
338 	 */
339 	void setupClient()
340 	{
341 		acmeRes.getResources();
342 		// Get initial Nonce
343 		this.getNonce();
344 
345 	}
346 
347 	/** Get a fresh and new Nonce from server
348 	 *
349 	 * To start the communication with JWS, an initial Nonce value must be
350 	 * fetched from the server.
351 	 *
352 	 * The Nonce returned is internally store in the AcmeResource structure.
353 	 *
354 	 * Note:
355 	 *   Use the Nonce of a JWS response header to update the Nonce for the
356 	 *   next transfer! So, only a single call to this function is needed to
357 	 *   setup the initial transfer.
358 	 *
359 	 * Returns:
360 	 *   a fresh and new Nonce value.
361 	 */
362 	string getNonce()
363 	{
364 		/* Get a NOnce number from server */
365 		auto nonce = getResponseHeader(acmeRes.newNOnceUrl, "Replay-Nonce");
366 		acmeRes.nonce = nonce;
367 		return nonce;
368 	}
369 
370 	/** Create a new account and bind a key pair to it.
371 	 *
372 	 * Before we can do anything, we need to register an account and
373 	 * bind a RSA/EC keypair to it, which is used for signatures in
374 	 * JWS and to create the CSR.
375 	 *
376 	 * Params:
377 	 *   contacts - list of contacts for the account
378 	 *   tosAgreed - set this to true, when user ack on commandline. otherwise
379 	 *               the default is false, and the CA server might refuse to
380 	 *               operate in this case.
381 	 *   onlyReturnExisting - do not create a new account, but only reuse an
382 	 *                 existing one. Defaults to false. When set to true, an
383 	 *                 account is never created, but only existing accounts are
384 	 *                 returned.
385 	 *
386 	 * Note: tosAgreed must be queried from user, e.g. by setting a commandline
387 	 *       option. This is required by the RFC8555.
388 	 * Note: Usually there is no need to set useExisting to false. If set to
389 	 *       true, an existing account for a JWK is returned or new one
390 	 *       is created and returned.
391 	 */
392 	bool createNewAccount(string[] contacts, bool tosAgreed = false, bool onlyReturnExisting = false)
393 	{
394 		bool rc;
395 		/* Create newAccount payload */
396 		JSONValue jvPayload;
397 		jvPayload["termsOfServiceAgreed"] = tosAgreed;
398 		JSONValue jvContact = contacts;
399 		jvPayload["contact"] = jvContact;
400 
401 		string payload = jvPayload.toJSON;
402 
403 		string[string] rheaders;
404 		import std.net.curl : HTTP;
405 		HTTP.StatusLine statusLine;
406 		string response = sendRequest!(string,false)(acmeRes.newAccountUrl, payload, &statusLine, &rheaders);
407 		if (statusLine.code / 100 == 2)
408 		{
409 			acmeRes.accountUrl = rheaders["location"];
410 			writeln("Account Location : ", acmeRes.accountUrl);
411 
412 			auto json = parseJSON(response);
413 			writeln("Account Creation : ", json["createdAt"]);
414 			// ...
415 			rc = true;
416 		}
417 		else {
418 			writeln("Got http error: ", statusLine);
419 			writeln("Got response:\n", response);
420 			// FIXME handle different error types...
421 		}
422 		return rc;
423 	}
424 
425 	/** Authorization setup callback
426 	*
427 	*   The implementation of this function allows Let's Encrypt to
428 	*   verify that the requestor has control of the domain name.
429 	*
430 	*   The callback may be called once for each domain name in the
431 	*   'issueCertificate' call. The callback should do whatever is
432 	*   needed so that a GET on the 'url' returns the 'keyAuthorization',
433 	*   (which is what the Acme protocol calls the expected response.)
434 	*
435 	*   Note that this function may not be called in cases where
436 	*   Let's Encrypt already believes the caller has control
437 	*   of the domain name.
438 	*/
439 	alias Callback =
440 		void function (
441 			string domainName,
442 			string url,
443 			string keyAuthorization);
444 
445 	/** Issue a certificate for domainNames
446 	 *
447 	 * The client begins the certificate issuance process by sending a POST
448 	 * request to the server's newOrder resource.  The body of the POST is a
449 	 * JWS object whose JSON payload is a subset of the order object defined
450 	 * in Section 7.1.3, containing the fields that describe the certificate
451 	 * to be issued.
452 	 *
453 	 * Params:
454 	 *   domainNames - list of domains
455 	 *   callback - pointer to function to setup expected response
456 	 *              on given URL
457 	 * Returns: A Certificate object or null.
458 	 * Throws: an instance of AcmeException on fatal or unexpected errors.
459 	 */
460 	Certificate issueCertificate(string domainKeyData, string[] domainNames, Callback callback)
461 	{
462 		if (domainNames.empty)
463 			throw new AcmeException("There must be at least one domain name in a certificate");
464 
465 		/* Pass any challenges we need to pass to make the CA believe we're entitled to a certificate. */
466 		JSONValue[] jvIdentifiers;
467 		jvIdentifiers.length = domainNames.length;
468 		foreach (i, domain ; domainNames)
469 		{
470 			jvIdentifiers[i]["type"] = "dns";
471 			jvIdentifiers[i]["value"] = domain;
472 		}
473 		JSONValue jvIdentifiersArray;
474 		jvIdentifiersArray.array = jvIdentifiers;
475 
476 		JSONValue jvPayload;
477 		// ISSUE: https://community.letsencrypt.org/t/notbefore-and-notafter-are-not-supported/54712
478 		version (boulderHasBeforeAfter) {
479 			jvPayload["notBefore"] = "2016-01-01T00:04:00+04:00";  // FIXME - use DateTime.to...()
480 			jvPayload["notAfter"]  = "2020-01-01T00:04:00+04:00";  // FIXME - use DateTime.to...()
481 		}
482 		jvPayload["identifiers"]  = jvIdentifiersArray;
483 
484 		string payload = jvPayload.toJSON;
485 writeln("Payload : ", jvPayload.toPrettyString);
486 		HTTP.StatusLine statusLine;
487 		string response = sendRequest!string(acmeRes.newOrderUrl, payload, &statusLine);
488 
489 		if (statusLine.code / 100 != 2) {
490 			writeln("Got http error: ", statusLine);
491 			writeln("Got response:\n", response);
492 			throw new AcmeException("Issue Request failed.");
493 			//return cast(Certificate)null;
494 		}
495 		auto json = parseJSON(response);
496 		writeln(json.toPrettyString);
497 
498 		/* If you pass a challenge, that's good for 300 days. The cert is only good for 90.
499 		 * This means for a while you can re-issue without passing another challenge, so we
500 		 * check to see if we need to validate again.
501 		 *
502 		 * Note that this introduces a race since it possible for the status to not be valid
503 		 * by the time the certificate is requested. The assumption is that client retries
504 		 * will deal with this.
505 		 */
506 		if ( ("status" in json) &&
507 			 (json["status"].type == JSONType..string) &&
508 			 (json["status"].str != "valid") )
509 		{
510 			if ("authorizations" in json) {
511 				auto authorizations = json["authorizations"];
512 				foreach ( i, authorizationUrl ; authorizations.array)
513 				{
514 					string authurl = authorizationUrl.str;
515 					string response2 = sendRequest!string(authurl, "", &statusLine);
516 					if (statusLine.code / 100 != 2) {
517 						writeln("Got http error: ", statusLine);
518 						writeln("Got response:\n", response2);
519 						stdout.flush;
520 						throw new AcmeException("Auth Request failed.");
521 						//return cast(Certificate)null;
522 					}
523 					auto json2 = parseJSON(response2);
524 					writeln(json2.toPrettyString);
525 
526 					if ("challenges" in json2)
527 					{
528 						auto domain = json2["identifier"]["value"].str;
529 						auto challenges = json2["challenges"];
530 						foreach (j, challenge; challenges.array)
531 						{
532 							if ( ("type" in challenge) &&
533 							     (challenge["type"].str == "http-01") )
534 							{
535 								string token = challenge["token"].str;
536 								string url = "http://" ~ domain ~ "/.well-known/acme-challenge/" ~ token;
537 								string keyAuthorization = token ~ "." ~ jwkThumbprint_.to!string;
538 								callback(domain, url, keyAuthorization);
539 								verifyChallengePassed(authorizationUrl.str, challenge);
540 								break;
541 							}
542 						}
543 					}
544 				}
545 			}
546 		} else {
547 			writefln("Send payload: \n%s", jvPayload.toPrettyString);
548 			writefln("Got failure response:\n%s", json.toPrettyString);
549 			throw new AcmeException(json.toPrettyString);
550 		}
551 
552 		// Issue the certificate
553 		// auto r = makeCertificateSigningRequest(domainNames);
554 		// string csr = r.csr;
555 		// string privateKey = r.pkey;
556 		const char[] privateKey = domainKeyData /* openSSL_CreatePrivateKey() */;
557 		const char[] csr = openSSL_CreateCertificateSignRequest(privateKey, domainNames);
558 
559 		writeln("CSR:\n", csr);
560 
561 		/* Send CSRs and get the intermediate certs */
562 		string[string] rheaders;
563 
564 		JSONValue ncrs;
565 		ncrs["csr"] = csr;
566 
567 		auto finalizeUrl = json["finalize"].str;
568 		auto finalizePayLoad = ncrs.toJSON;
569 		auto finalizeResponseStr = sendRequest!(char[])(finalizeUrl, finalizePayLoad, &statusLine, &rheaders);
570 		if (statusLine.code / 100 != 2) {
571 				writeln("Got http error: ", statusLine);
572 				writeln("Got response:\n", finalizeResponseStr);
573 				stdout.flush;
574 				throw new AcmeException("Verification for passed challange failed.");
575 		}
576 		auto finalizeResponseJV = parseJSON(finalizeResponseStr);
577 		writeln(finalizeResponseJV.toPrettyString);
578 
579 		/* Download the certificate (via POST-as-GET) */
580 		auto certificateUrl = finalizeResponseJV["certificate"].str;
581 		auto crtpem = sendRequest!(char[])(certificateUrl, "", &statusLine, &rheaders);
582 		writeln(crtpem);
583 
584 		/* Create a container object */
585 		Certificate cert;
586 		//~ cert.fullchain = convertDERtoPEM(der) ~ cast(string)getIntermediateCertificate(rheaders["Link"]);
587 		cert.fullchain = crtpem.to!string;
588 		cert.privkey = privateKey.idup;
589 		return cert;
590 	}
591 
592 	/** Acknowledge to CA server that a Auth is setup for check.
593 	 *
594 	 * Params:
595 	 *  authorizationUrl - url to a auth job
596 	 *  challenge - the current challange for reference
597 	 *
598 	 * Throws if the challenge isn't accepted (or on timeout)
599 	 */
600 	void verifyChallengePassed(string authorizationUrl, JSONValue challenge)
601 	{
602 		string verificationUri = challenge["url"].str;
603 
604 		import std.net.curl : HTTP;
605 		HTTP.StatusLine statusLine;
606 		string response = sendRequest!string(verificationUri, q"({})", &statusLine );
607 		if (statusLine.code / 100 != 2) {
608 			writeln("Got http error: ", statusLine);
609 			writeln("Got response:\n", response);
610 			stdout.flush;
611 			throw new AcmeException("Verification for passed challange failed.");
612 			//return cast(Certificate)null;
613 		}
614 		// Poll waiting for the CA to verify the challenge
615 		int counter = 0;
616 		enum count = 10;
617 		do
618 		{
619 			// sleep for a second
620 			import core.thread : Thread;
621 			Thread.sleep(dur!"seconds"(2));
622 
623 			// get response from verification URL
624 			response = sendRequest!string(authorizationUrl, "", &statusLine);
625 			if (statusLine.code / 100 != 2) {
626 				writeln("Got http error: ", statusLine);
627 				writeln("Got response:\n", response);
628 				stdout.flush;
629 				throw new AcmeException("Verification for passed challange failed.");
630 			}
631 			else {
632 				writeln(response);
633 				auto json = parseJSON(response);
634 				//writeln(json.toPrettyString);
635 				if (json["status"].str == "valid")
636 				{
637 					writeln("challange valid. Continue.");
638 					return;
639 				}
640 			}
641 		} while (counter++ < count);
642 
643 		throw new AcmeException("Failure / timeout verifying challenge passed");
644 	}
645 }
646 
647 /* ------------------------------------------------------------------------ */
648 /* --- Helper Functions --------------------------------------------------- */
649 /* ------------------------------------------------------------------------ */
650 
651 /** Get the issuer certificate from a 'Link' response header
652  *
653  * Param:
654  *  linkHeader - ResponseHeader Line of the form
655  *               Link: <https://acme-v01.api.letsencrypt.org/acme/issuer-cert>;rel="up"
656  * Returns:
657  *   Pem-encoded issuer certificate string
658  */
659 string getIntermediateCertificate(string linkHeader)
660 {
661 	/* Extract the URL from the Header */
662 	import std.regex;
663 	// Link: <https://acme-v01.api.letsencrypt.org/acme/issuer-cert>;rel="up"
664 	auto r = regex("^<(.*)>;rel=\"up\"$");
665 	auto match = matchFirst(linkHeader, r);
666 	if (match.empty)
667 	{
668 		throw new AcmeException("Unable to parse 'Link' header with value " ~ linkHeader);
669 	}
670 	char[] url = cast(char[])match[1];
671 
672 	/* Download the issuer certificate */
673 	auto reps = get(url);
674 	auto rstr = convertDERtoPEM( reps );
675 	return rstr;
676 }