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