Aktualizr
C++ SOTA Client
main.cc
1 #include <cstdlib>
2 #include <iostream>
3 #include <sstream>
4 #include <string>
5 
6 #include <boost/filesystem.hpp>
7 #include <boost/program_options.hpp>
8 
9 #include "json/json.h"
10 
11 #include "bootstrap/bootstrap.h"
12 #include "crypto/crypto.h"
13 #include "http/httpclient.h"
14 #include "libaktualizr/config.h"
15 #include "logging/logging.h"
16 #include "utilities/aktualizr_version.h"
17 #include "utilities/utils.h"
18 
19 namespace bpo = boost::program_options;
20 
21 void checkInfoOptions(const bpo::options_description& description, const bpo::variables_map& vm) {
22  if (vm.count("help") != 0) {
23  std::cout << description << '\n';
24  exit(EXIT_SUCCESS);
25  }
26  if (vm.count("version") != 0) {
27  std::cout << "Current aktualizr-cert-provider version is: " << aktualizr_version() << "\n";
28  exit(EXIT_SUCCESS);
29  }
30 }
31 
32 bpo::variables_map parseOptions(int argc, char** argv) {
33  bpo::options_description description("aktualizr-cert-provider command line options");
34  // clang-format off
35  description.add_options()
36  ("help,h", "print usage")
37  ("version,v", "Current aktualizr-cert-provider version")
38  ("credentials,c", bpo::value<boost::filesystem::path>(), "zipped credentials file")
39  ("fleet-ca", bpo::value<boost::filesystem::path>(), "path to fleet certificate authority certificate (for signing device certificates)")
40  ("fleet-ca-key", bpo::value<boost::filesystem::path>(), "path to the private key of fleet certificate authority")
41  ("bits", bpo::value<int>(), "size of RSA keys in bits")
42  ("days", bpo::value<int>(), "validity term for the certificate in days")
43  ("certificate-c", bpo::value<std::string>(), "value for C field in certificate subject name")
44  ("certificate-st", bpo::value<std::string>(), "value for ST field in certificate subject name")
45  ("certificate-o", bpo::value<std::string>(), "value for O field in certificate subject name")
46  ("certificate-cn", bpo::value<std::string>(), "value for CN field in certificate subject name (used for device ID)")
47  ("target,t", bpo::value<std::string>(), "target device to scp credentials to (or [user@]host)")
48  ("port,p", bpo::value<int>(), "target port")
49  ("directory,d", bpo::value<boost::filesystem::path>(), "directory on target to write credentials to (conflicts with --config)")
50  ("root-ca,r", "provide root CA certificate")
51  ("server-url,u", "provide server URL file")
52  ("local,l", bpo::value<boost::filesystem::path>(), "local directory to write credentials to")
53  ("config,g", bpo::value<std::vector<boost::filesystem::path> >()->composing(), "configuration file or directory from which to get file names")
54  ("skip-checks,s", "skip strict host key checking for ssh/scp commands");
55  // clang-format on
56 
57  bpo::variables_map vm;
58  std::vector<std::string> unregistered_options;
59  try {
60  bpo::basic_parsed_options<char> parsed_options =
61  bpo::command_line_parser(argc, argv).options(description).allow_unregistered().run();
62  bpo::store(parsed_options, vm);
63  checkInfoOptions(description, vm);
64  bpo::notify(vm);
65  unregistered_options = bpo::collect_unrecognized(parsed_options.options, bpo::include_positional);
66  if (vm.count("help") == 0 && !unregistered_options.empty()) {
67  std::cout << description << "\n";
68  exit(EXIT_FAILURE);
69  }
70  } catch (const bpo::required_option& ex) {
71  // print the error and append the default commandline option description
72  std::cout << ex.what() << std::endl << description;
73  exit(EXIT_FAILURE);
74  } catch (const bpo::error& ex) {
75  checkInfoOptions(description, vm);
76 
77  // print the error message to the standard output too, as the user provided
78  // a non-supported commandline option
79  std::cout << ex.what() << '\n';
80 
81  // set the returnValue, thereby ctest will recognize
82  // that something went wrong
83  exit(EXIT_FAILURE);
84  }
85 
86  return vm;
87 }
88 
89 class SSHRunner {
90  public:
91  SSHRunner(std::string target, const bool skip_checks, const int port = 22)
92  : target_(std::move(target)), skip_checks_(skip_checks), port_(port) {}
93 
94  void runCmd(const std::string& cmd) const {
95  std::ostringstream prefix;
96 
97  prefix << "ssh ";
98  if (port_ != 22) {
99  prefix << "-p " << port_ << " ";
100  }
101  if (skip_checks_) {
102  prefix << "-o StrictHostKeyChecking=no ";
103  }
104  prefix << target_ << " ";
105 
106  std::string fullCmd = prefix.str() + cmd;
107  std::cout << "Running " << fullCmd << std::endl;
108 
109  int ret = system(fullCmd.c_str());
110  if (ret != 0) {
111  throw std::runtime_error("Error running command on " + target_ + ": " + std::to_string(ret));
112  }
113  }
114 
115  void transferFile(const boost::filesystem::path& inFile, const boost::filesystem::path& targetPath) {
116  // create parent directory
117  runCmd("mkdir -p " + targetPath.parent_path().string());
118 
119  std::ostringstream prefix;
120 
121  prefix << "scp ";
122  if (port_ != 22) {
123  prefix << "-P " << port_ << " ";
124  }
125  if (skip_checks_) {
126  prefix << "-o StrictHostKeyChecking=no ";
127  }
128 
129  std::string fullCmd = prefix.str() + inFile.string() + " " + target_ + ":" + targetPath.string();
130  std::cout << "Running " << fullCmd << std::endl;
131 
132  int ret = system(fullCmd.c_str());
133  if (ret != 0) {
134  throw std::runtime_error("Error copying file on " + target_ + ": " + std::to_string(ret));
135  }
136  }
137 
138  private:
139  std::string target_;
140  bool skip_checks_;
141  int port_;
142 };
143 
144 void copyLocal(const boost::filesystem::path& src, const boost::filesystem::path& dest) {
145  boost::filesystem::path dest_dir = dest.parent_path();
146  if (boost::filesystem::exists(dest_dir)) {
147  boost::filesystem::remove(dest);
148  } else {
149  boost::filesystem::create_directories(dest_dir);
150  }
151  boost::filesystem::copy_file(src, dest);
152 }
153 
154 int main(int argc, char* argv[]) {
155  int exit_code = EXIT_FAILURE;
156 
157  logger_init();
158  logger_set_threshold(static_cast<boost::log::trivial::severity_level>(2));
159 
160  try {
161  bpo::variables_map commandline_map = parseOptions(argc, argv);
162 
163  std::string target;
164  if (commandline_map.count("target") != 0) {
165  target = commandline_map["target"].as<std::string>();
166  }
167  int port = 22;
168  if (commandline_map.count("port") != 0) {
169  port = (commandline_map["port"].as<int>());
170  }
171  const bool provide_ca = commandline_map.count("root-ca") != 0;
172  const bool provide_url = commandline_map.count("server-url") != 0;
173  boost::filesystem::path local_dir;
174  if (commandline_map.count("local") != 0) {
175  local_dir = commandline_map["local"].as<boost::filesystem::path>();
176  }
177  std::vector<boost::filesystem::path> config_path;
178  if (commandline_map.count("config") != 0) {
179  config_path = commandline_map["config"].as<std::vector<boost::filesystem::path>>();
180  }
181  const bool skip_checks = commandline_map.count("skip-checks") != 0;
182 
183  boost::filesystem::path fleet_ca_path = "";
184  if (commandline_map.count("fleet-ca") != 0) {
185  fleet_ca_path = commandline_map["fleet-ca"].as<boost::filesystem::path>();
186  }
187 
188  boost::filesystem::path fleet_ca_key_path = "";
189  if (commandline_map.count("fleet-ca-key") != 0) {
190  fleet_ca_key_path = commandline_map["fleet-ca-key"].as<boost::filesystem::path>();
191  }
192 
193  if (fleet_ca_path.empty() != fleet_ca_key_path.empty()) {
194  std::cerr << "fleet-ca and fleet-ca-key options should be used together" << std::endl;
195  return EXIT_FAILURE;
196  }
197 
198  if (!commandline_map["directory"].empty() && !commandline_map["config"].empty()) {
199  std::cerr << "Directory (--directory) and config (--config) options cannot be used together" << std::endl;
200  return EXIT_FAILURE;
201  }
202 
203  boost::filesystem::path credentials_path = "";
204  if (commandline_map.count("credentials") != 0) {
205  credentials_path = commandline_map["credentials"].as<boost::filesystem::path>();
206  }
207 
208  if (local_dir.empty() && target.empty()) {
209  std::cerr << "Please provide a local directory and/or target to output the generated files to" << std::endl;
210  return EXIT_FAILURE;
211  }
212 
213  std::string serverUrl;
214  if ((fleet_ca_path.empty() || provide_ca || provide_url) && credentials_path.empty()) {
215  std::cerr
216  << "Error: missing -c/--credentials parameters which is mandatory if the fleet CA is not specified or an "
217  "output of the root CA or a gateway URL is requested";
218  return EXIT_FAILURE;
219  } else {
220  serverUrl = Bootstrap::readServerUrl(credentials_path);
221  }
222 
223  std::string device_id;
224  if (commandline_map.count("certificate-cn") != 0) {
225  device_id = (commandline_map["certificate-cn"].as<std::string>());
226  if (device_id.empty()) {
227  std::cerr << "Common name (device ID, --certificate-cn) can't be empty" << std::endl;
228  return EXIT_FAILURE;
229  }
230  } else {
231  device_id = Utils::genPrettyName();
232  std::cout << "Random device ID is " << device_id << "\n";
233  }
234 
235  boost::filesystem::path directory = "/var/sota/import";
236  utils::BasedPath pkey_file = utils::BasedPath("pkey.pem");
237  utils::BasedPath cert_file = utils::BasedPath("client.pem");
238  utils::BasedPath ca_file = utils::BasedPath("root.crt");
239  utils::BasedPath url_file = utils::BasedPath("gateway.url");
240  if (!config_path.empty()) {
241  Config config(config_path);
242 
243  // try first import base path and then storage path
244  if (!config.import.base_path.empty()) {
245  directory = config.import.base_path;
246  } else if (!config.storage.path.empty()) {
247  directory = config.storage.path;
248  }
249 
250  if (!config.import.tls_pkey_path.empty()) {
251  pkey_file = config.import.tls_pkey_path;
252  } else {
253  pkey_file = config.storage.tls_pkey_path;
254  }
255 
256  if (!config.import.tls_clientcert_path.empty()) {
257  cert_file = config.import.tls_clientcert_path;
258  } else {
259  cert_file = config.storage.tls_clientcert_path;
260  }
261  if (provide_ca) {
262  if (!config.import.tls_cacert_path.empty()) {
263  ca_file = config.import.tls_cacert_path;
264  } else {
265  ca_file = config.storage.tls_cacert_path;
266  }
267  }
268  if (provide_url && !config.tls.server_url_path.empty()) {
269  url_file = config.tls.server_url_path;
270  }
271  }
272 
273  if (!commandline_map["directory"].empty()) {
274  directory = commandline_map["directory"].as<boost::filesystem::path>();
275  }
276 
277  TemporaryFile tmp_pkey_file(pkey_file.get("").filename().string());
278  TemporaryFile tmp_cert_file(cert_file.get("").filename().string());
279  TemporaryFile tmp_ca_file(ca_file.get("").filename().string());
280  TemporaryFile tmp_url_file(url_file.get("").filename().string());
281 
282  std::string pkey;
283  std::string cert;
284  std::string ca;
285 
286  if (fleet_ca_path.empty()) { // no fleet CA => provision with shared credentials
287  Bootstrap boot(credentials_path, "");
288  HttpClient http;
289  Json::Value data;
290  data["deviceId"] = device_id;
291  data["ttl"] = 36000;
292 
293  std::cout << "Provisioning against server...\n";
294  http.setCerts(boot.getCa(), CryptoSource::kFile, boot.getCert(), CryptoSource::kFile, boot.getPkey(),
295  CryptoSource::kFile);
296  HttpResponse response = http.post(serverUrl + "/devices", data);
297  if (!response.isOk()) {
298  Json::Value resp_code = response.getJson()["code"];
299  if (resp_code.isString() && resp_code.asString() == "device_already_registered") {
300  std::cout << "Device ID" << device_id << "is occupied.\n";
301  return EXIT_FAILURE;
302  }
303  std::cout << "Provisioning failed, response: " << response.body << "\n";
304  return EXIT_FAILURE;
305  }
306  std::cout << "...success\n";
307 
308  StructGuard<BIO> device_p12(BIO_new_mem_buf(response.body.c_str(), static_cast<int>(response.body.size())),
309  BIO_vfree);
310  if (!Crypto::parseP12(device_p12.get(), "", &pkey, &cert, &ca)) {
311  std::cout << "Unable to parse p12 file received from server.\n";
312  return EXIT_FAILURE;
313  }
314  } else { // fleet CA set => generate and sign a new certificate
315  int rsa_bits = 2048;
316  if (commandline_map.count("bits") != 0) {
317  rsa_bits = (commandline_map["bits"].as<int>());
318  }
319 
320  int cert_days = 365;
321  if (commandline_map.count("days") != 0) {
322  cert_days = (commandline_map["days"].as<int>());
323  }
324 
325  std::string newcert_c;
326  if (commandline_map.count("certificate-c") != 0) {
327  newcert_c = (commandline_map["certificate-c"].as<std::string>());
328  if (newcert_c.length() != 2) {
329  std::cerr << "Country code (--certificate-c) should be 2 characters long" << std::endl;
330  return EXIT_FAILURE;
331  }
332  }
333 
334  std::string newcert_st;
335  if (commandline_map.count("certificate-st") != 0) {
336  newcert_st = (commandline_map["certificate-st"].as<std::string>());
337  if (newcert_st.empty()) {
338  std::cerr << "State name (--certificate-st) can't be empty" << std::endl;
339  return EXIT_FAILURE;
340  }
341  }
342 
343  std::string newcert_o;
344  if (commandline_map.count("certificate-o") != 0) {
345  newcert_o = (commandline_map["certificate-o"].as<std::string>());
346  if (newcert_o.empty()) {
347  std::cerr << "Organization name (--certificate-o) can't be empty" << std::endl;
348  return EXIT_FAILURE;
349  }
350  }
351 
352  StructGuard<X509> certificate =
353  Crypto::generateCert(rsa_bits, cert_days, newcert_c, newcert_st, newcert_o, device_id);
354  Crypto::signCert(fleet_ca_path.native(), fleet_ca_key_path.native(), certificate.get());
355  Crypto::serializeCert(&pkey, &cert, certificate.get());
356 
357  if (provide_ca) {
358  // Read server root CA from server_ca.pem in archive if found (to support
359  // community edition use case). Otherwise, default to the old version of
360  // expecting it to be in the p12.
361  ca = Bootstrap::readServerCa(credentials_path);
362  if (ca.empty()) {
363  Bootstrap boot(credentials_path, "");
364  ca = boot.getCa();
365  std::cout << "Server root CA read from autoprov_credentials.p12 in zipped archive.\n";
366  } else {
367  std::cout << "Server root CA read from server_ca.pem in zipped archive.\n";
368  }
369  }
370  }
371 
372  tmp_pkey_file.PutContents(pkey);
373  tmp_cert_file.PutContents(cert);
374  if (provide_ca) {
375  tmp_ca_file.PutContents(ca);
376  }
377  if (provide_url) {
378  tmp_url_file.PutContents(serverUrl);
379  }
380 
381  if (!local_dir.empty()) {
382  auto pkey_file_path = local_dir / pkey_file.get(directory);
383  std::cout << "Writing the generated client private key to " << pkey_file_path << " ...\n";
384  copyLocal(tmp_pkey_file.PathString(), pkey_file_path);
385 
386  auto cert_file_path = local_dir / cert_file.get(directory);
387  std::cout << "Writing the generated and signed client certificate to " << cert_file_path << " ...\n";
388  copyLocal(tmp_cert_file.PathString(), cert_file_path);
389 
390  if (provide_ca) {
391  auto root_ca_file = local_dir / ca_file.get(directory);
392  std::cout << "Writing the server root CA to " << root_ca_file << " ...\n";
393  copyLocal(tmp_ca_file.PathString(), root_ca_file);
394  }
395  if (provide_url) {
396  auto gtw_url_file = local_dir / url_file.get(directory);
397  std::cout << "Writing the gateway URL to " << gtw_url_file << " ...\n";
398  copyLocal(tmp_url_file.PathString(), gtw_url_file);
399  }
400  std::cout << "...success\n";
401  }
402 
403  if (!target.empty()) {
404  std::cout << "Copying client certificate and keys to " << target << ":" << directory;
405  if (port != 0) {
406  std::cout << " on port " << port;
407  }
408  std::cout << " ...\n";
409 
410  SSHRunner ssh{target, skip_checks, port};
411 
412  try {
413  ssh.transferFile(tmp_pkey_file.Path(), pkey_file.get(directory));
414 
415  ssh.transferFile(tmp_cert_file.Path(), cert_file.get(directory));
416 
417  if (provide_ca) {
418  ssh.transferFile(tmp_ca_file.Path(), ca_file.get(directory));
419  }
420  if (provide_url) {
421  ssh.transferFile(tmp_url_file.Path(), url_file.get(directory));
422  }
423 
424  std::cout << "...success\n";
425  } catch (const std::runtime_error& exc) {
426  std::cout << exc.what() << std::endl;
427  }
428  }
429 
430  exit_code = EXIT_SUCCESS;
431  } catch (const std::exception& exc) {
432  LOG_ERROR << "Error: " << exc.what();
433 
434  exit_code = EXIT_FAILURE;
435  }
436 
437  return exit_code;
438 }
SSHRunner
Definition: main.cc:89
data
General data structures.
Definition: types.h:217
HttpResponse
Definition: httpinterface.h:17
utils::BasedPath
The BasedPath class Can represent an absolute or relative path, only readable through the BasePath::g...
Definition: types.h:31
Config
Configuration object for an aktualizr instance running on a Primary ECU.
Definition: config.h:208
HttpClient
Definition: httpclient.h:28
Bootstrap
Definition: bootstrap.h:7
TemporaryFile
RAII Temporary file creation.
Definition: utils.h:68