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