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