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