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 }