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 argForceUpdate; /// Force Cert update 38 bool argUseStaging; /// Use staging server 39 bool argTosAgree; /// Agree to Terms of Service 40 41 /** Long Help text and example */ 42 enum helpLongText = q"( 43 Example: 44 $ ./acme-lw-d -k key.pem -p domain.key -o domain.pem \ 45 -d your-domain.net -d www.your-domain.net \ 46 -c "mailto:webmaster@domain.net" \ 47 -w "./examples/setupChallange.sh" \ 48 -y -v -b {rsa2048|rs4096} 49 50 RS keys will be created on first run and stored on disk. They are reused 51 when existing. 52 53 The setup-challange script is called with the challange type, the filename 54 and token. Right new, only http challange is supported (FIXME). 55 )"; 56 57 58 /** Call-out to setup the ACME 59 * 60 * Params: 61 * domain - domain identifier 62 * url - the url to prepare 63 * keyAuthorization - the token to return 64 * Returns: 65 * A shell return value (0 means success) 66 */ 67 private 68 int handleChallenge(string domain, string url, string keyAuthorization) 69 { 70 import std.format : format; 71 string cmd = format("%s %s %s %s", argChallangeScript, domain, url, keyAuthorization); 72 writefln("Running command '%s'", cmd); stdout.flush; 73 74 import std.process : executeShell; 75 auto rc = executeShell(cmd); 76 writeln(rc.output); 77 writefln("Command returned status %d (sucess=%s)", rc.status, rc.status == 0 ? true : false); 78 return rc.status; 79 } 80 81 /** Programm Main 82 * 83 * Param: 84 * args - array of command line args 85 * Return: 86 * shell error code 87 */ 88 int main(string[] args) 89 { 90 if (args.length <= 1) args ~= "-h"; 91 auto helpInformation = getopt( 92 args, 93 std.getopt.config.required, 94 "key|k", "The path to private key of ACME account. (PEM file)", &argPrivateKeyFile, 95 std.getopt.config.required, 96 "domainkey|p", "The path to your private key for X509 certificates (PEM file)", &argDomainKeyFile, 97 std.getopt.config.required, 98 "domain|d", "A domain name. Can be given multiple times. First entry will be subject name.", &argDomainNames, 99 std.getopt.config.required, 100 "contact|c", "A contact for the account. Can be given multiple times.", &argContacts, 101 std.getopt.config.required, 102 "output|o", "The output file for the PEM encoded X509 cert", &argOutputFile, 103 std.getopt.config.required, 104 "setupchallange|w", "Programm to call to setup a challange", &argChallangeScript, 105 "bits|b", "RSA bits to use for keys. Used on new key creation", &argRSABits, 106 "agree|y", "Agree to TermsOfService, when creating the account.", &argTosAgree, 107 "staging|s", "Use the staging server for initial testing or developing", &argUseStaging, 108 "server", "Alternate ACME server directory url", &argServerUrl, 109 "force|f", "Force certificate update", &argForceUpdate, 110 "verbose|v", "Verbose output", &argVerbose); 111 if (helpInformation.helpWanted) 112 { 113 defaultGetoptPrinter("Usage: acme_client <options>", 114 helpInformation.options); 115 writeln(helpLongText); 116 return 1; 117 } 118 assert(argPrivateKeyFile !is null, "The path should be set?!"); 119 assert(argDomainKeyFile !is null, "The path should be set?!"); 120 assert(argDomainNames.length >= 1, "No domain names found?!"); 121 assert(argContacts.length >= 1, "No contacts found?!"); 122 123 if (argUseStaging) { 124 writeln("Note: Running against staging environment!"); 125 useStagingServer = true; 126 } else { 127 writeln("Note: Running against production environment!"); 128 useStagingServer = false; 129 } 130 131 /* -- Read the keys from disk ---------------------------------------- */ 132 string privateKeyData; 133 if (exists(argPrivateKeyFile)) { 134 privateKeyData = std.file.readText(argPrivateKeyFile); 135 if (argVerbose) writefln("Read private key for ACME account from %s.", argPrivateKeyFile); 136 } else { 137 import acme.openssl_helpers : openSSL_CreatePrivateKey; 138 privateKeyData = openSSL_CreatePrivateKey(argRSABits).idup; 139 std.file.write(argPrivateKeyFile, privateKeyData); 140 if (argVerbose) writeln("Created private key for ACME account."); 141 } 142 143 string domainKeyData; 144 if (exists(argDomainKeyFile)) { 145 domainKeyData = std.file.readText(argDomainKeyFile); 146 if (argVerbose) writefln("Read private key for csr from %s.", argDomainKeyFile); 147 } 148 else { 149 import acme.openssl_helpers : openSSL_CreatePrivateKey; 150 domainKeyData = openSSL_CreatePrivateKey(argRSABits).idup; 151 std.file.write(argDomainKeyFile, domainKeyData); 152 if (argVerbose) writeln("Created private key for ACME account."); 153 } 154 155 /* -- Test for existing certificat, test remaining lifetime ---------- */ 156 157 if (argOutputFile.exists && argOutputFile.isFile) 158 { 159 bool updateCert; 160 writefln("Files '%s' and '%s' have been read.", argOutputFile, argDomainKeyFile); 161 auto certificatePEM = std.file.readText(argOutputFile); 162 Certificate certificate = Certificate(certificatePEM, domainKeyData); 163 import std.datetime : DateTime, SysTime, Clock, dur; 164 auto expdate = certificate.getExpiry(); 165 writeln("Certificate expires on " ~ expdate.toSimpleString); 166 SysTime today = Clock.currTime(); 167 if (!argForceUpdate && cast(DateTime)today < (expdate - dur!"weeks"(4)) ) 168 { 169 writeln("No need to update the certificate. Use --force flag."); 170 } 171 else { 172 writefln("%s to update the certificate.", argForceUpdate ? "Forced" : "Need"); 173 updateCert = true; 174 } 175 if (!argForceUpdate) 176 return 0; 177 } 178 179 /* -- ACME V2 process starts below ----------------------------------- */ 180 181 int exitStatus = -1; 182 try 183 { 184 curlBeVerbose = argVerbose; 185 186 /* --- Create the ACME client object ----------------------------- */ 187 AcmeClient acmeClient = new AcmeClient(privateKeyData, argVerbose); 188 189 if (argServerUrl) 190 acmeClient.acmeRes.directoryUrl = argServerUrl; 191 192 acmeClient.setupClient(); 193 writeln( "URL for ACME directory : ", acmeClient.acmeRes.directoryUrl); 194 if (argVerbose) { 195 writeln( acmeClient.acmeRes.directoryJson.toPrettyString() ); 196 } 197 /* --- Create a new account/Use existing account ----------------- */ 198 const bool nwaccrc = acmeClient.createNewAccount(argContacts, argTosAgree); 199 if (!nwaccrc) { 200 stdout.writeln("Failed to create new or obtain exiting account."); 201 return exitStatus; 202 } 203 204 /* --- Issue a new cert process ----------------------------------- */ 205 Certificate certificate; 206 certificate = acmeClient.issueCertificate(domainKeyData, argDomainNames, &handleChallenge); 207 208 /* --- Write out file --------------------------------------------- */ 209 std.file.write(argOutputFile, certificate.fullchain); 210 std.file.write(argDomainKeyFile, certificate.privkey); 211 writefln( "Files '%s' and '%s' have been written.", argOutputFile, argDomainKeyFile); 212 213 /* Get the expiry date from cert */ 214 auto expdate = certificate.getExpiryDisplay(); 215 writeln( "Certificate expires on " ~ expdate); 216 217 exitStatus = 0; 218 } 219 catch (AcmeException e) 220 { 221 writeln( "Failed with error: " ~ e.msg ); 222 } 223 return exitStatus; 224 } 225