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