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 	import acme.openssl_glues;
152 
153 	/** The full CA chain with cert */
154 	string fullchain;
155 	/** The private key to sign requests */
156 	string privkey;
157 
158 	// Note that neither of the 'Expiry' calls below require 'privkey'
159 	// to be set; they only rely on 'fullchain'.
160 
161 	/**
162 		Returns the number of seconds since 1970, i.e., epoch time.
163 
164 		Due to openssl quirkiness there might be a little drift
165 		from a strictly accurate result, but it should be close
166 		enough for the purpose of determining whether the certificate
167 		needs to be renewed.
168 	*/
169 	DateTime getExpiry() const
170 	{
171 		static const(DateTime) extractor(const ASN1_TIME * t)
172 		{
173 			// See this link for issues in converting from ASN1_TIME to epoch time.
174 			// https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion
175 			int days, seconds;
176 			if (!C_ASN1_TIME_diff(&days, &seconds, cast(ASN1_TIME*)null, cast(ASN1_TIME*)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 = C_ASN1_TIME_print(t);
205 			scope(exit) C_BIO_free(b);
206 			return b.toVector.to!string;
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 	import acme.openssl_glues;
223 	EVP_PKEY*   privateKey_;     /// Copy of private key as ASC PEM
224 
225 	JSONValue   jwkData_;        /// JWK object as JSONValue tree
226 	string      jwkString_;      /// JWK as plain JSON string
227 	ubyte[]     jwkSHAHash_;     /// The SHA256 hash value of jwkString_
228 	string      jwkThumbprint_;  /// Base64 url-safe string of jwkSHAHash_
229 
230 	bool		beVerbose_;      /// Be verbose
231 
232 	void myLog(alias fun = writeln, T...)(T args)
233 	{
234 		if (beVerbose_) {
235 			fun(args);
236 			stdout.flush;
237 		}
238 	}
239 
240 	/** Create and send a JWS request with payload to a ACME enabled CA server
241 	 *
242 	 * Still unfinished template to build the JWS object. This code must be
243 	 * refactored later.
244 	 *
245 	 * Params:
246 	 *  T = return type
247 	 *  useKID = useKID
248 	 *  url = Url to post to
249 	 *  payload = data to send
250 	 *  status = pointer to StatusLine
251 	 *  rheaders = Pointer to ResponseHeaders received from server, or null
252 	 *
253 	 * See: https://tools.ietf.org/html/rfc7515
254 	 */
255 	T sendRequest(T, bool useKID = true)
256 		(string url, string payload, HTTP.StatusLine* status = null, string[string]* rheaders = null)
257 			if ( is(T : string) || is(T : char[]) || is(T : ubyte[]))
258 	{
259 		string nonce = this.acmeRes.nonce;
260 		assert(nonce !is null && !nonce.empty, "Invalid Nonce value.");
261 		myLog("Using NOnce: ", nonce);
262 
263 		/* Create protection data */
264 		JSONValue jvReqHeader;
265 		jvReqHeader["alg"] = "RS256";
266 		static if (useKID)
267 			jvReqHeader["kid"] = acmeRes.accountUrl;
268 		else
269 			jvReqHeader["jwk"] = jwkData_;
270 		jvReqHeader["nonce"] = nonce;
271 		jvReqHeader["url"] = url;
272 		char[] protectd = jvReqHeader.toJSON.dup;
273 		myLog(protectd);
274 		protectd = base64EncodeUrlSafe(protectd);
275 
276 		const char[] payld = base64EncodeUrlSafe(payload);
277 
278 		auto signData = protectd ~ "." ~ payld;
279 		myLog!writefln("Data to sign: %s", signData);
280 		char[] signature = signDataWithSHA256(signData, privateKey_);
281 		myLog!writefln("Signature: %s", signature);
282 
283 		JSONValue jvBody;
284 		jvBody["protected"] = protectd;
285 		jvBody["payload"] = payld;
286 		jvBody["signature"] = signature;
287 		char[] body_ = jvBody.toJSON.dup;
288 		myLog!writefln("Body: %s", jvBody.toPrettyString);
289 
290 		auto response = doPost(url, body_, status, rheaders, &(acmeRes.nonce));
291 		if (rheaders) {
292 			myLog( "ResponseHeaders: ");
293 			foreach( v ; (*rheaders).byKey) {
294 				myLog("  ", v, " : ", (*rheaders)[v]);
295 				//~ if (v.toLower == "replay-nonce") {
296 					//~ acmeRes.nonce = (*rheaders)[v];
297 					//~ myLog("Setting new NOnce: ", acmeRes.nonce);
298 				//~ }
299 			}
300 		}
301 		myLog( "Response: ", response);
302 
303 		return to!T(response);
304 	}
305 
306 public:
307 	AcmeResources acmeRes;       /// The Urls to the ACME resources
308 
309 
310 	/** Instanciate a AcmeClient using a private key for signing
311 	 *
312 	 *  Param:
313 	 *     accountPrivateKey = The signingKey is the Acme account private
314 	 *     		key used to sign requests to the acme CA, in pem format.
315 	 *  Throws: an instance of AcmeException on fatal or unexpected errors.
316 	 */
317 	this(string accountPrivateKey, bool beVerbose = false)
318 	{
319 		import acme.openssl_glues;
320 		beVerbose_ = beVerbose;
321 
322 		acmeRes.initClient( useStagingServer ? directoryUrlStaging : directoryUrlProd);
323 		SSL_OpenLibrary();
324 
325 		/* Create the private key */
326 		RSA* rsa;
327 		privateKey_ = SSL_x509_read_pkey_memory( cast(const(char[]))accountPrivateKey, &rsa);
328 
329 		BIGNUM* n;
330 		BIGNUM* e;
331 		BIGNUM* d;
332 		C_RSA_Get0_key(rsa, &n, &e, &d);
333 
334 		// https://tools.ietf.org/html/rfc7638
335 		// JSON Web Key (JWK) Thumbprint
336 		JSONValue jvJWK;
337 		jvJWK["e"] = getBigNumberBytes(e).base64EncodeUrlSafe;
338 		jvJWK["kty"] = "RSA";
339 		jvJWK["n"] = getBigNumberBytes(n).base64EncodeUrlSafe;
340 		jwkData_ = jvJWK;
341 		jwkString_ = jvJWK.toJSON;
342 		jwkSHAHash_ = sha256Encode( jwkString_ );
343 		jwkThumbprint_ = jwkSHAHash_.base64EncodeUrlSafe.idup;
344 		//myLog("JWK:\n", jvJWK.toPrettyString);
345 		myLog("JWK:\n", jvJWK.toJSON);
346 		myLog("SHA of JWK:\n", jwkSHAHash_);
347 		myLog("Thumbprint of JWK:\n", jwkThumbprint_);
348 	}
349 	~this () {
350 		SSL_CloseLibrary();
351 	}
352 
353 	/** Call once after instantiating AcmeClient to setup parameters
354 	 *
355 	 * This function will fetch the directory from the ACME CA server and
356 	 * extracts the Urls.
357 	 *
358 	 * Also fetches the initial NOnce value for the next JWS transfer.
359 	 */
360 	void setupClient()
361 	{
362 		acmeRes.getResources();
363 		// Get initial Nonce
364 		this.getNonce();
365 
366 	}
367 
368 	/** Get a fresh and new Nonce from server
369 	 *
370 	 * To start the communication with JWS, an initial Nonce value must be
371 	 * fetched from the server.
372 	 *
373 	 * The Nonce returned is internally store in the AcmeResource structure.
374 	 *
375 	 * Note:
376 	 *   Use the Nonce of a JWS response header to update the Nonce for the
377 	 *   next transfer! So, only a single call to this function is needed to
378 	 *   setup the initial transfer.
379 	 *
380 	 * Returns:
381 	 *   a fresh and new Nonce value.
382 	 */
383 	string getNonce()
384 	{
385 		/* Get a NOnce number from server */
386 		auto nonce = getResponseHeader(acmeRes.newNOnceUrl, "Replay-Nonce");
387 		acmeRes.nonce = nonce;
388 		return nonce;
389 	}
390 
391 	/** Create a new account and bind a key pair to it.
392 	 *
393 	 * Before we can do anything, we need to register an account and
394 	 * bind a RSA/EC keypair to it, which is used for signatures in
395 	 * JWS and to create the CSR.
396 	 *
397 	 * Params:
398 	 *   contacts = list of contacts for the account
399 	 *   tosAgreed = set this to true, when user ack on commandline. otherwise
400 	 *               the default is false, and the CA server might refuse to
401 	 *               operate in this case.
402 	 *   onlyReturnExisting = do not create a new account, but only reuse an
403 	 *                 existing one. Defaults to false. When set to true, an
404 	 *                 account is never created, but only existing accounts are
405 	 *                 returned. Useful to lookup the accountUrl of an key.
406 	 *
407 	 * Note: tosAgreed must be queried from user, e.g. by setting a commandline
408 	 *       option. This is required by the RFC8555.
409 	 * Note: Usually there is no need to set useExisting to false. If set to
410 	 *       true, an existing account for a JWK is returned or new one
411 	 *       is created and returned.
412 	 */
413 	bool createNewAccount(string[] contacts, bool tosAgreed = false, bool onlyReturnExisting = false)
414 	{
415 		bool rc;
416 		/* Create newAccount payload */
417 		JSONValue jvPayload;
418 		jvPayload["termsOfServiceAgreed"] = tosAgreed;
419 		/* Do not create new account? Used for lookups of accountURL */
420 		if (onlyReturnExisting)
421 			jvPayload["onlyReturnExisting"] = true;
422 		const JSONValue jvContact = contacts;
423 		jvPayload["contact"] = jvContact;
424 
425 		string payload = jvPayload.toJSON;
426 
427 		string[string] rheaders;
428 		import std.net.curl : HTTP;
429 		HTTP.StatusLine statusLine;
430 		string response = sendRequest!(string,false)(acmeRes.newAccountUrl, payload, &statusLine, &rheaders);
431 		if (statusLine.code / 100 == 2)
432 		{
433 			acmeRes.accountUrl = rheaders["location"];
434 			writeln("Account Location : ", acmeRes.accountUrl);
435 
436 			auto json = parseJSON(response);
437 			if ("createdAt" in json)
438 				writeln("Account Creation : ", json["createdAt"]);
439 			// ...
440 			rc = true;
441 		}
442 		else {
443 			stderr.writeln("Got http error: ", statusLine);
444 			stderr.writeln("Got response:\n", response);
445 			// FIXME handle different error types...
446 		}
447 		return rc;
448 	}
449 
450 	/** Authorization setup callback
451 	*
452 	*   The implementation of this function allows Let's Encrypt to
453 	*   verify that the requestor has control of the domain name.
454 	*
455 	*   The callback may be called once for each domain name in the
456 	*   'issueCertificate' call. The callback should do whatever is
457 	*   needed so that a GET on the 'url' returns the 'keyAuthorization',
458 	*   (which is what the Acme protocol calls the expected response.)
459 	*
460 	*   Note that this function may not be called in cases where
461 	*   Let's Encrypt already believes the caller has control
462 	*   of the domain name.
463 	*/
464 	alias Callback =
465 		int function (
466 			string domainName,
467 			string url,
468 			string keyAuthorization);
469 
470 	/** Issue a certificate for domainNames
471 	 *
472 	 * The client begins the certificate issuance process by sending a POST
473 	 * request to the server's newOrder resource.  The body of the POST is a
474 	 * JWS object whose JSON payload is a subset of the order object defined
475 	 * in Section 7.1.3, containing the fields that describe the certificate
476 	 * to be issued.
477 	 *
478 	 * Params:
479 	 *   domainKeyData = the private PEM-encoded key
480 	 *   domainNames = list of domains
481 	 *   callback = pointer to function to setup expected response
482 	 *              on given URL
483 	 * Returns: A Certificate object or null.
484 	 * Throws: an instance of AcmeException on fatal or unexpected errors.
485 	 */
486 	Certificate issueCertificate(string domainKeyData, string[] domainNames, Callback callback)
487 	{
488 		if (domainNames.empty)
489 			throw new AcmeException("There must be at least one domain name in a certificate");
490 
491 		/* Pass any challenges we need to pass to make the CA believe we're entitled to a certificate. */
492 		JSONValue[] jvIdentifiers;
493 		jvIdentifiers.length = domainNames.length;
494 		foreach (i, domain ; domainNames)
495 		{
496 			jvIdentifiers[i]["type"] = "dns";
497 			jvIdentifiers[i]["value"] = domain;
498 		}
499 		JSONValue jvIdentifiersArray;
500 		jvIdentifiersArray.array = jvIdentifiers;
501 
502 		JSONValue jvPayload;
503 		// ISSUE: https://community.letsencrypt.org/t/notbefore-and-notafter-are-not-supported/54712
504 		version (boulderHasBeforeAfter) {
505 			jvPayload["notBefore"] = "2016-01-01T00:04:00+04:00";  // FIXME - use DateTime.to...()
506 			jvPayload["notAfter"]  = "2020-01-01T00:04:00+04:00";  // FIXME - use DateTime.to...()
507 		}
508 		jvPayload["identifiers"]  = jvIdentifiersArray;
509 
510 		string payload = jvPayload.toJSON;
511 		myLog("Payload : ", jvPayload.toPrettyString);
512 		HTTP.StatusLine statusLine;
513 		string response = sendRequest!string(acmeRes.newOrderUrl, payload, &statusLine);
514 
515 		if (statusLine.code / 100 != 2) {
516 			stderr.writeln("Got http error: ", statusLine);
517 			stderr.writeln("Got response:\n", response);
518 			throw new AcmeException("Issue Request failed.");
519 		}
520 		auto json = parseJSON(response);
521 		myLog(json.toPrettyString);
522 
523 		/* If you pass a challenge, that's good for 300 days. The cert is only good for 90.
524 		 * This means for a while you can re-issue without passing another challenge, so we
525 		 * check to see if we need to validate again.
526 		 *
527 		 * Note that this introduces a race since it possible for the status to not be valid
528 		 * by the time the certificate is requested. The assumption is that client retries
529 		 * will deal with this.
530 		 */
531 		if ( ("status" in json) &&
532 			 (json["status"].type == JSONType..string) &&
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 }