1 /*
2  * Copyright (C) 1996-2021 The Squid Software Foundation and contributors
3  *
4  * Squid software is distributed under GPLv2+ license and includes
5  * contributions from numerous individuals and organizations.
6  * Please see the COPYING and CONTRIBUTORS files for details.
7  */
8 
9 #include "squid.h"
10 #include "helper/protocol_defines.h"
11 #include "security/cert_generators/file/certificate_db.h"
12 #include "ssl/crtd_message.h"
13 
14 #include <cstring>
15 #include <iostream>
16 #include <sstream>
17 #include <stdexcept>
18 #include <string>
19 #if HAVE_GETOPT_H
20 #include <getopt.h>
21 #endif
22 
23 /**
24  \defgroup ssl_crtd security_file_certgen
25  \ingroup ExternalPrograms
26  \par
27     Because the standard generation of SSL certificates for
28     sslBump feature, Squid must use external process to
29     actually make these calls. This process generate new ssl
30     certificates and worked with ssl certificates disk cache.
31     Typically there will be five certificate generator processes
32     spawned from Squid. Communication occurs via TCP sockets
33     bound to the loopback interface. The class in helper.h are
34     primally concerned with starting and stopping the helpers.
35     Reading and writing to and from the helpers occurs in the
36     \link IPCacheAPI IP\endlink and the dnsservers occurs in
37     the \link IPCacheAPI IP\endlink and \link FQDNCacheAPI
38     FQDN\endlink cache modules.
39 
40  \section ssl_crtdInterface Command Line Interface
41  \verbatim
42 usage: security_file_certgen -hv -s directory -M size -b fs_block_size
43     -h                   Help
44     -v                   Version
45     -s directory         Directory path of SSL storage database.
46     -M size              Maximum size of SSL certificate disk storage.
47     -b fs_block_size     File system block size in bytes. Need for processing
48                          natural size of certificate on disk. Default value is
49                          2048 bytes.
50 
51     After running write requests in the next format:
52     <request code><whitespace><body_len><whitespace><body>
53     There are two kind of request now:
54     new_certificate 14 host=host.dom
55         Create new private key and selfsigned certificate for "host.dom".
56 
57     new_certificate xxx host=host.dom
58     -----BEGIN CERTIFICATE-----
59     ...
60     -----END CERTIFICATE-----
61     -----BEGIN RSA PRIVATE KEY-----
62     ...
63     -----END RSA PRIVATE KEY-----
64         Create new private key and certificate request for "host.dom".
65         Sign new request by received certificate and private key.
66 
67 usage: security_file_certgen -c -s ssl_store_path\n
68     -c                   Init ssl db directories and exit.
69 
70  \endverbatim
71  */
72 
73 static const char *const B_KBYTES_STR = "KB";
74 static const char *const B_MBYTES_STR = "MB";
75 static const char *const B_GBYTES_STR = "GB";
76 static const char *const B_BYTES_STR = "B";
77 
78 /// Get current time.
getCurrentTime(void)79 time_t getCurrentTime(void)
80 {
81     struct timeval current_time;
82 #if GETTIMEOFDAY_NO_TZP
83     gettimeofday(&current_time);
84 #else
85     gettimeofday(&current_time, NULL);
86 #endif
87     return current_time.tv_sec;
88 }
89 
90 /**
91  * Parse bytes unit. It would be one of the next value: MB, GB, KB or B.
92  * This function is caseinsensitive.
93  */
parseBytesUnits(const char * unit)94 static size_t parseBytesUnits(const char * unit)
95 {
96     if (!strncasecmp(unit, B_BYTES_STR, strlen(B_BYTES_STR)) ||
97             !strncasecmp(unit, "", strlen(unit)))
98         return 1;
99 
100     if (!strncasecmp(unit, B_KBYTES_STR, strlen(B_KBYTES_STR)))
101         return 1 << 10;
102 
103     if (!strncasecmp(unit, B_MBYTES_STR, strlen(B_MBYTES_STR)))
104         return 1 << 20;
105 
106     if (!strncasecmp(unit, B_GBYTES_STR, strlen(B_GBYTES_STR)))
107         return 1 << 30;
108 
109     std::cerr << "WARNING: Unknown bytes unit '" << unit << "'" << std::endl;
110 
111     return 0;
112 }
113 
114 /// Parse uninterrapted string of bytes value. It looks like "4MB".
parseBytesOptionValue(size_t * bptr,char const * value)115 static bool parseBytesOptionValue(size_t * bptr, char const * value)
116 {
117     // Find number from string beginning.
118     char const * number_begin = value;
119     char const * number_end = value;
120 
121     while ((*number_end >= '0' && *number_end <= '9')) {
122         ++number_end;
123     }
124 
125     std::string number(number_begin, number_end - number_begin);
126     std::istringstream in(number);
127     int d = 0;
128     if (!(in >> d))
129         return false;
130 
131     int m;
132     if ((m = parseBytesUnits(number_end)) == 0) {
133         return false;
134     }
135 
136     *bptr = static_cast<size_t>(m * d);
137     if (static_cast<long>(*bptr * 2) != m * d * 2)
138         return false;
139 
140     return true;
141 }
142 
143 /// Print help using response code.
usage()144 static void usage()
145 {
146     std::string example_host_name = "host.dom";
147     std::string request_string = Ssl::CrtdMessage::param_host + "=" + example_host_name;
148     std::stringstream request_string_size_stream;
149     request_string_size_stream << request_string.length();
150     std::string help_string =
151         "usage: security_file_certgen -hv -s directory -M size -b fs_block_size\n"
152         "\t-h                   Help\n"
153         "\t-v                   Version\n"
154         "\t-s directory         Directory path of SSL storage database.\n"
155         "\t-M size              Maximum size of SSL certificate disk storage.\n"
156         "\t-b fs_block_size     File system block size in bytes. Need for processing\n"
157         "\t                     natural size of certificate on disk. Default value is\n"
158         "\t                     2048 bytes.\n"
159         "\n"
160         "After running write requests in the next format:\n"
161         "<request code><whitespace><body_len><whitespace><body>\n"
162         "There are two kind of request now:\n"
163         + Ssl::CrtdMessage::code_new_certificate + " " + request_string_size_stream.str() + " " + request_string + "\n" +
164         "\tCreate new private key and selfsigned certificate for \"host.dom\".\n"
165         + Ssl::CrtdMessage::code_new_certificate + " xxx " + request_string + "\n" +
166         "-----BEGIN CERTIFICATE-----\n"
167         "...\n"
168         "-----END CERTIFICATE-----\n"
169         "-----BEGIN RSA PRIVATE KEY-----\n"
170         "...\n"
171         "-----END RSA PRIVATE KEY-----\n"
172         "\tCreate new private key and certificate request for \"host.dom\"\n"
173         "\tSign new request by received certificate and private key.\n"
174         "usage: security_file_certgen -c -s ssl_store_path\n"
175         "\t-c                   Init ssl db directories and exit.\n";
176     std::cerr << help_string << std::endl;
177 }
178 
179 /// Process new request message.
processNewRequest(Ssl::CrtdMessage & request_message,std::string const & db_path,size_t max_db_size,size_t fs_block_size)180 static bool processNewRequest(Ssl::CrtdMessage & request_message, std::string const & db_path, size_t max_db_size, size_t fs_block_size)
181 {
182     Ssl::CertificateProperties certProperties;
183     std::string error;
184     if (!request_message.parseRequest(certProperties, error))
185         throw std::runtime_error("Error while parsing the crtd request: " + error);
186 
187     // TODO: create a DB object only once, instead re-allocating here on every call.
188     std::unique_ptr<Ssl::CertificateDb> db;
189     if (!db_path.empty())
190         db.reset(new Ssl::CertificateDb(db_path, max_db_size, fs_block_size));
191 
192     Security::CertPointer cert;
193     Security::PrivateKeyPointer pkey;
194     Security::CertPointer orig;
195     std::string &certKey = Ssl::OnDiskCertificateDbKey(certProperties);
196 
197     bool dbFailed = false;
198     try {
199         if (db)
200             db->find(certKey, certProperties.mimicCert, cert, pkey);
201 
202     } catch (std::runtime_error &err) {
203         dbFailed = true;
204         error = err.what();
205     }
206 
207     if (!cert || !pkey) {
208         if (!Ssl::generateSslCertificate(cert, pkey, certProperties))
209             throw std::runtime_error("Cannot create ssl certificate or private key.");
210 
211         try {
212             /* XXX: this !dbFailed condition prevents the helper fixing DB issues
213                by adding cleanly generated certs. Which is not consistent with other
214                data caches used by Squid - they purge broken entries and allow clean
215                entries to later try and fix the issue.
216                We leave it in place now only to avoid breaking existing installations
217                behaviour with version 1.x of the helper.
218 
219                TODO: remove the !dbFailed condition when fixing the CertificateDb
220                     object lifecycle and formally altering the helper behaviour.
221             */
222             if (!dbFailed && db && !db->addCertAndPrivateKey(certKey, cert, pkey, certProperties.mimicCert))
223                 throw std::runtime_error("Cannot add certificate to db.");
224 
225         } catch (const std::runtime_error &err) {
226             dbFailed = true;
227             error = err.what();
228         }
229     }
230 
231     if (dbFailed)
232         std::cerr << "security_file_certgen helper database '" << db_path  << "' failed: " << error << std::endl;
233 
234     std::string bufferToWrite;
235     if (!Ssl::writeCertAndPrivateKeyToMemory(cert, pkey, bufferToWrite))
236         throw std::runtime_error("Cannot write ssl certificate or/and private key to memory.");
237 
238     Ssl::CrtdMessage response_message(Ssl::CrtdMessage::REPLY);
239     response_message.setCode("OK");
240     response_message.setBody(bufferToWrite);
241 
242     // Use the '\1' char as end-of-message character
243     std::cout << response_message.compose() << '\1' << std::flush;
244 
245     return true;
246 }
247 
248 /// This is the external security_file_certgen process.
main(int argc,char * argv[])249 int main(int argc, char *argv[])
250 {
251     try {
252         size_t max_db_size = 0;
253         size_t fs_block_size = 0;
254         int8_t c;
255         bool create_new_db = false;
256         std::string db_path;
257         // process options.
258         while ((c = getopt(argc, argv, "dchvs:M:b:")) != -1) {
259             switch (c) {
260             case 'd':
261                 debug_enabled = 1;
262                 break;
263             case 'b':
264                 if (!parseBytesOptionValue(&fs_block_size, optarg)) {
265                     throw std::runtime_error("Error when parsing -b options value");
266                 }
267                 break;
268             case 's':
269                 db_path = optarg;
270                 break;
271             case 'M':
272                 // use of -M without -s is probably an admin mistake, so make it an error
273                 if (db_path.empty()) {
274                     throw std::runtime_error("Error -M option requires an -s parameter be set first.");
275                 }
276                 if (!parseBytesOptionValue(&max_db_size, optarg)) {
277                     throw std::runtime_error("Error when parsing -M options value");
278                 }
279                 break;
280             case 'v':
281                 std::cout << "security_file_certgen version " << VERSION << std::endl;
282                 exit(0);
283                 break;
284             case 'c':
285                 create_new_db = true;
286                 break;
287             case 'h':
288                 usage();
289                 exit(0);
290             default:
291                 exit(0);
292             }
293         }
294 
295         // when -s is used, -M is required
296         if (!db_path.empty() && max_db_size == 0)
297             throw std::runtime_error("security_file_certgen -s requires an -M parameter");
298 
299         if (create_new_db) {
300             // when -c is used, -s is required (implying also -M, which is checked above)
301             if (db_path.empty())
302                 throw std::runtime_error("security_file_certgen is missing the required parameter. There should be -s and -M parameters when -c is used.");
303 
304             std::cout << "Initialization SSL db..." << std::endl;
305             Ssl::CertificateDb::Create(db_path);
306             std::cout << "Done" << std::endl;
307             exit(0);
308         }
309 
310         // only do filesystem checks when a path (-s) is given
311         if (!db_path.empty()) {
312             if (fs_block_size == 0) {
313                 struct statvfs sfs;
314 
315                 if (xstatvfs(db_path.c_str(), &sfs)) {
316                     fs_block_size = 2048;
317                 } else {
318                     fs_block_size = sfs.f_frsize;
319                     // Sanity check; make sure we have a meaningful value.
320                     if (fs_block_size < 512)
321                         fs_block_size = 2048;
322                 }
323             }
324             Ssl::CertificateDb::Check(db_path, max_db_size, fs_block_size);
325         }
326 
327         // Initialize SSL subsystem
328         SQUID_OPENSSL_init_ssl();
329         // process request.
330         for (;;) {
331             char request[HELPER_INPUT_BUFFER];
332             Ssl::CrtdMessage request_message(Ssl::CrtdMessage::REQUEST);
333             Ssl::CrtdMessage::ParseResult parse_result = Ssl::CrtdMessage::INCOMPLETE;
334 
335             while (parse_result == Ssl::CrtdMessage::INCOMPLETE) {
336                 if (fgets(request, HELPER_INPUT_BUFFER, stdin) == NULL)
337                     return 1;
338                 size_t gcount = strlen(request);
339                 parse_result = request_message.parse(request, gcount);
340             }
341 
342             if (parse_result == Ssl::CrtdMessage::ERROR) {
343                 throw std::runtime_error("Cannot parse request message.");
344             } else if (request_message.getCode() == Ssl::CrtdMessage::code_new_certificate) {
345                 processNewRequest(request_message, db_path, max_db_size, fs_block_size);
346             } else {
347                 throw std::runtime_error("Unknown request code: \"" + request_message.getCode() + "\".");
348             }
349             std::cout.flush();
350         }
351     } catch (std::runtime_error & error) {
352         std::cerr << argv[0] << ": " << error.what() << std::endl;
353         return -1;
354     }
355     return 0;
356 }
357 
358