1 2 /** 3 * This is commandline tool of the ACME v2 client 4 */ 5 module app; 6 7 import std.file; 8 import std.getopt; 9 import std.json; 10 import std.stdio; 11 12 import acme; 13 import acme.acme_client; 14 import acme.exception; 15 16 /* Decoded Commandline Options */ 17 string argPrivateKeyFile; /// The path to the private key for the ACME account 18 string argDomainKeyFile; /// The path to the private key for the certs and csr 19 string argOutputFile; /// The output path for the downloaded cert. 20 string[] argDomainNames; /// The list of domain names 21 string[] argContacts; /// The list of account names 22 /// Supported key sizes 23 enum argRSABitsEnum { 24 rsa2048 = 2048, 25 rsa4096 = 4096 26 }; 27 argRSABitsEnum argRSABits; 28 bool argVerbose; /// Verbosity mode? 29 bool argTosAgree; /// Agree to Terms of Service 30 31 /** Programm Main */ 32 int main(string[] args) 33 { 34 version (STAGING) 35 writeln("Running against staging environment."); 36 37 auto helpInformation = getopt( 38 args, 39 std.getopt.config.required, 40 "key|k", "The path to private key of ACME account. (PEM file)", &argPrivateKeyFile, 41 std.getopt.config.required, 42 "domainkey|p", "The path to your private key for X509 certificates (PEM file)", &argDomainKeyFile, 43 std.getopt.config.required, 44 "domain|d", "A domain name. Can be given multiple times. First entry will be subject name.", &argDomainNames, 45 std.getopt.config.required, 46 "contact|c", "A contact for the account. Can be given multiple times.", &argContacts, 47 std.getopt.config.required, 48 "output|o", "The output file for the PEM encoded X509 cert", &argOutputFile, 49 "bits|b", "RSA bits to use for keys. Used on new key creation", &argRSABits, 50 "agree|y", "Agree to TermsOfService", &argTosAgree, 51 "verbose|v", "Verbose output", &argVerbose); 52 if (helpInformation.helpWanted) 53 { 54 defaultGetoptPrinter("Usage: acme_client <options>", 55 helpInformation.options); 56 return 1; 57 } 58 assert(argPrivateKeyFile !is null, "The path should be set?!"); 59 //assert(argDomainKeyFile !is null, "The path should be set?!"); 60 assert(argDomainNames.length >= 1, "No domain names found?!"); 61 assert(argContacts.length >= 1, "No contacts found?!"); 62 63 /* -- Read the keys from disk ---------------------------------------- */ 64 string privateKeyData; 65 if (exists(argPrivateKeyFile)) { 66 privateKeyData = std.file.readText(argPrivateKeyFile); 67 if (argVerbose) writefln("Read private key for ACME account from %s.", argPrivateKeyFile); 68 } else { 69 import acme.openssl_helpers : openSSL_CreatePrivateKey; 70 privateKeyData = openSSL_CreatePrivateKey().idup; 71 std.file.write(argPrivateKeyFile, privateKeyData); 72 if (argVerbose) writeln("Created private key for ACME account."); 73 } 74 75 string domainKeyData; 76 if (exists(argDomainKeyFile)) { 77 domainKeyData = std.file.readText(argDomainKeyFile); 78 if (argVerbose) writefln("Read private key for csr from %s.", argDomainKeyFile); 79 } 80 else { 81 import acme.openssl_helpers : openSSL_CreatePrivateKey; 82 domainKeyData = openSSL_CreatePrivateKey().idup; 83 std.file.write(argDomainKeyFile, domainKeyData); 84 if (argVerbose) writeln("Created private key for ACME account."); 85 } 86 87 /* -- ACME V2 process starts below ----------------------------------- */ 88 89 int exitStatus = -1; 90 try 91 { 92 /* --- Create the ACME client object ----------------------------- */ 93 AcmeClient acmeClient = new AcmeClient(privateKeyData); 94 95 acmeClient.setupClient(); 96 if (argVerbose) { 97 writeln( "URL for ACME directory : ", acmeClient.acmeRes.directoryUrl); 98 writeln( acmeClient.acmeRes.directoryJson.toPrettyString() ); 99 } 100 /* --- Create a new account/Use existing account ----------------- */ 101 const bool nwaccrc = acmeClient.createNewAccount(argContacts, argTosAgree); 102 if (!nwaccrc) { 103 stdout.writeln("Failed to create new or obtain exiting account."); 104 return exitStatus; 105 } 106 107 /* --- Issue a new cert process ----------------------------------- */ 108 Certificate certificate; 109 certificate = acmeClient.issueCertificate(domainKeyData, argDomainNames, &handleChallenge); 110 111 /* --- Write out file --------------------------------------------- */ 112 std.file.write(argOutputFile, certificate.fullchain); 113 std.file.write(argDomainKeyFile, certificate.privkey); 114 writefln( "Files '%s' and '%s' have been written.", argOutputFile, argDomainKeyFile); 115 116 /* Get the expiry date from cert */ 117 auto expdate = certificate.getExpiryDisplay(); 118 writeln( "Certificate expires on " ~ expdate); 119 120 exitStatus = 0; 121 } 122 catch (AcmeException e) 123 { 124 writeln( "Failed with error: " ~ e.msg ); 125 } 126 return exitStatus; 127 } 128 129 /* Expected response callback */ 130 private 131 void handleChallenge(string domain, string url, string keyAuthorization) 132 { 133 writeln("To verify ownership of " ~ domain ~ " make\n\n" 134 ~ "\t" ~ url ~ "\n\nrespond with this\n\n" 135 ~ "\t" ~ keyAuthorization ~ "\n\n" 136 ~ "Hit any key when done"); 137 import std.string; 138 string filename = (url.split("/"))[$-1]; 139 string cmd = "rsh raspi3 \"echo " ~ keyAuthorization ~ " > /var/www/html/.well-known/acme-challenge/" ~ filename ~ "\""; 140 writeln(cmd); 141 stdout.flush; 142 143 import std.process : executeShell; 144 executeShell(cmd); 145 146 //getchar(); 147 writeln( "\n***\n" ); 148 }