1 /** This is commandline tool of the ACME v2 client
2  *
3  * This tool implements a ACME V2 compatible client, which allows to setup
4  * an account on the LetsEncrypt ACME server, to open an order for a new
5  * certificate, to setup the challanges and finally downloads the certificate.
6  *
7  * See:
8  *   RFC8555 - Describes the ACME Protokol, version 2
9  */
10 module app;
11 
12 /* Imports */
13 
14 import std.file;
15 import std.getopt;
16 import std.json;
17 import std.stdio;
18 
19 import acme;
20 
21 /* Decoded Commandline Options */
22 
23 string argServerUrl;         /// Alternate server URL
24 string argPrivateKeyFile;    /// The path to the private key for the ACME account
25 string argDomainKeyFile;     /// The path to the private key for the certs and csr
26 string argOutputFile;        /// The output path for the downloaded cert.
27 string argChallangeScript;   /// Name of challange script to call
28 string[] argDomainNames;     /// The list of domain names
29 string[] argContacts;        /// The list of account names
30 /// Supported key sizes
31 enum ArgRSABitsEnum {
32 	rsa2048 = 2048,
33 	rsa4096 = 4096
34 }
35 ArgRSABitsEnum argRSABits;   /// Select the number of bit by the enum name
36 bool argVerbose;             /// Verbosity mode?
37 bool argUseStaging;          /// Use staging server
38 bool argTosAgree;            /// Agree to Terms of Service
39 
40 /** Long Help text and example */
41 enum helpLongText = q"(
42 Example:
43   $ ./acme-lw-d -k key.pem -p domain.key -o domain.pem \
44        -d your-domain.net -d www.your-domain.net \
45        -c "mailto:webmaster@domain.net" \
46        -w "./examples/setupChallange.sh" \
47        -y -v -b {rsa2048|rs4096}
48 
49   RS keys will be created on first run and stored on disk. They are reused
50   when existing.
51 
52   The setup-challange script is called with the challange type, the filename
53    and token. Right new, only http challange is supported (FIXME).
54 )";
55 
56 
57 /** Call-out to setup the ACME
58  *
59  * Params:
60  *   domain - domain identifier
61  *   url - the url to prepare
62  *   keyAuthorization - the token to return
63  * Returns:
64  *   A shell return value (0 means success)
65  */
66 private
67 int handleChallenge(string domain, string url, string keyAuthorization)
68 {
69 	import std.format : format;
70 	string cmd = format("%s %s %s %s", argChallangeScript, domain, url, keyAuthorization);
71 	writefln("Running command '%s'", cmd); stdout.flush;
72 
73 	import std.process : executeShell;
74 	auto rc = executeShell(cmd);
75 	writeln(rc.output);
76 	writefln("Command returned status %d (sucess=%s)", rc.status, rc.status == 0 ? true : false);
77 	return rc.status;
78 }
79 
80 /** Programm Main
81  *
82  * Param:
83  *   args - array of command line args
84  * Return:
85  *   shell error code
86  */
87 int main(string[] args)
88 {
89 	if (args.length <= 1) args ~= "-h";
90 	auto helpInformation = getopt(
91 		args,
92 		std.getopt.config.required,
93 		"key|k",     "The path to private key of ACME account. (PEM file)", &argPrivateKeyFile,
94 		std.getopt.config.required,
95 		"domainkey|p",     "The path to your private key for X509 certificates (PEM file)", &argDomainKeyFile,
96 		std.getopt.config.required,
97 		"domain|d",  "A domain name. Can be given multiple times. First entry will be subject name.", &argDomainNames,
98 		std.getopt.config.required,
99 		"contact|c", "A contact for the account. Can be given multiple times.", &argContacts,
100 		std.getopt.config.required,
101 		"output|o",  "The output file for the PEM encoded X509 cert", &argOutputFile,
102 		std.getopt.config.required,
103 		"setupchallange|w",  "Programm to call to setup a challange", &argChallangeScript,
104 		"bits|b",    "RSA bits to use for keys. Used on new key creation", &argRSABits,
105 		"agree|y",   "Agree to TermsOfService, when creating the account.", &argTosAgree,
106 		"staging|s", "Use the staging server for initial testing or developing", &argUseStaging,
107 		"server",    "Alternate ACME server directory url", &argServerUrl,
108 		"verbose|v", "Verbose output", &argVerbose);
109 	if (helpInformation.helpWanted)
110 	{
111 		defaultGetoptPrinter("Usage: acme_client <options>",
112 			helpInformation.options);
113 		writeln(helpLongText);
114 		return 1;
115 	}
116 	assert(argPrivateKeyFile !is null, "The path should be set?!");
117 	assert(argDomainKeyFile !is null, "The path should be set?!");
118 	assert(argDomainNames.length >= 1, "No domain names found?!");
119 	assert(argContacts.length >= 1, "No contacts found?!");
120 
121 	if (argUseStaging) {
122 		writeln("Note: Running against staging environment!");
123 		useStagingServer = true;
124 	} else {
125 		writeln("Note: Running against production environment!");
126 		useStagingServer = false;
127 	}
128 
129 	/* -- Read the keys from disk ---------------------------------------- */
130 	string privateKeyData;
131 	if (exists(argPrivateKeyFile)) {
132 		privateKeyData = std.file.readText(argPrivateKeyFile);
133 		if (argVerbose) writefln("Read private key for ACME account from %s.", argPrivateKeyFile);
134 	} else {
135 		import acme.openssl_helpers : openSSL_CreatePrivateKey;
136 		privateKeyData = openSSL_CreatePrivateKey(argRSABits).idup;
137 		std.file.write(argPrivateKeyFile, privateKeyData);
138 		if (argVerbose) writeln("Created private key for ACME account.");
139 	}
140 
141 	string domainKeyData;
142 	if (exists(argDomainKeyFile)) {
143 		domainKeyData = std.file.readText(argDomainKeyFile);
144 		if (argVerbose) writefln("Read private key for csr from %s.", argDomainKeyFile);
145 	}
146 	else {
147 		import acme.openssl_helpers : openSSL_CreatePrivateKey;
148 		domainKeyData = openSSL_CreatePrivateKey(argRSABits).idup;
149 		std.file.write(argDomainKeyFile, domainKeyData);
150 		if (argVerbose) writeln("Created private key for ACME account.");
151 	}
152 
153 	/* -- ACME V2 process starts below ----------------------------------- */
154 
155 	int exitStatus = -1;
156 	try
157 	{
158 		curlBeVerbose = argVerbose;
159 
160 		/* --- Create the ACME client object ----------------------------- */
161 		AcmeClient acmeClient = new AcmeClient(privateKeyData, argVerbose);
162 
163 		if (argServerUrl)
164 			acmeClient.acmeRes.directoryUrl = argServerUrl;
165 
166 		acmeClient.setupClient();
167 		if (argVerbose) {
168 			writeln( "URL for ACME directory : ", acmeClient.acmeRes.directoryUrl);
169 			writeln( acmeClient.acmeRes.directoryJson.toPrettyString() );
170 		}
171 		/* --- Create a new account/Use existing account  ----------------- */
172 		const bool nwaccrc = acmeClient.createNewAccount(argContacts, argTosAgree);
173 		if (!nwaccrc) {
174 			stdout.writeln("Failed to create new or obtain exiting account.");
175 			return exitStatus;
176 		}
177 
178 		/* --- Issue a new cert process ----------------------------------- */
179 		Certificate certificate;
180 		certificate = acmeClient.issueCertificate(domainKeyData, argDomainNames, &handleChallenge);
181 
182 		/* --- Write out file --------------------------------------------- */
183 		std.file.write(argOutputFile, certificate.fullchain);
184 		std.file.write(argDomainKeyFile, certificate.privkey);
185 		writefln( "Files '%s' and '%s' have been written.", argOutputFile, argDomainKeyFile);
186 
187 		/* Get the expiry date from cert */
188 		auto expdate = certificate.getExpiryDisplay();
189 		writeln( "Certificate expires on " ~ expdate);
190 
191 		exitStatus = 0;
192 	}
193 	catch (AcmeException e)
194 	{
195 		writeln( "Failed with error: " ~ e.msg );
196 	}
197 	return exitStatus;
198 }
199