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 BETA 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 curlBeVerbose = argVerbose; 152 153 /* --- Create the ACME client object ----------------------------- */ 154 AcmeClient acmeClient = new AcmeClient(privateKeyData, argVerbose); 155 156 acmeClient.setupClient(); 157 if (argVerbose) { 158 writeln( "URL for ACME directory : ", acmeClient.acmeRes.directoryUrl); 159 writeln( acmeClient.acmeRes.directoryJson.toPrettyString() ); 160 } 161 /* --- Create a new account/Use existing account ----------------- */ 162 const bool nwaccrc = acmeClient.createNewAccount(argContacts, argTosAgree); 163 if (!nwaccrc) { 164 stdout.writeln("Failed to create new or obtain exiting account."); 165 return exitStatus; 166 } 167 168 /* --- Issue a new cert process ----------------------------------- */ 169 Certificate certificate; 170 certificate = acmeClient.issueCertificate(domainKeyData, argDomainNames, &handleChallenge); 171 172 /* --- Write out file --------------------------------------------- */ 173 std.file.write(argOutputFile, certificate.fullchain); 174 std.file.write(argDomainKeyFile, certificate.privkey); 175 writefln( "Files '%s' and '%s' have been written.", argOutputFile, argDomainKeyFile); 176 177 /* Get the expiry date from cert */ 178 auto expdate = certificate.getExpiryDisplay(); 179 writeln( "Certificate expires on " ~ expdate); 180 181 exitStatus = 0; 182 } 183 catch (AcmeException e) 184 { 185 writeln( "Failed with error: " ~ e.msg ); 186 } 187 return exitStatus; 188 } 189