Aktualizr
C++ SOTA Client
cert_provider_test.cc
1 #include <gtest/gtest.h>
2 
3 #include <boost/format.hpp>
4 
5 #include "cert_provider_test.h"
6 #include "crypto/crypto.h"
7 #include "libaktualizr/config.h"
8 #include "utilities/utils.h"
9 
10 static boost::filesystem::path CERT_PROVIDER_PATH;
11 
12 class AktualizrCertProviderTest : public ::testing::Test {
13  protected:
14  struct TestArgs {
15  TestArgs(const TemporaryDirectory& tmp_dir) : test_dir{tmp_dir.PathString()} {}
16 
17  const std::string test_dir;
18  const std::string fleet_ca_cert = "tests/test_data/CAcert.pem";
19  const std::string fleet_ca_private_key = "tests/test_data/CApkey.pem";
20  };
21 
22  class Cert {
23  public:
24  Cert(const std::string& cert_file_path) {
25  fp_ = fopen(cert_file_path.c_str(), "r");
26  if (!fp_) {
27  throw std::invalid_argument("Cannot open the specified cert file: " + cert_file_path);
28  }
29 
30  cert_ = PEM_read_X509(fp_, NULL, NULL, NULL);
31  if (!cert_) {
32  fclose(fp_);
33  throw std::runtime_error("Failed to read the cert file: " + cert_file_path);
34  }
35  }
36 
37  ~Cert() {
38  X509_free(cert_);
39  fclose(fp_);
40  }
41 
42  std::string getSubjectItemValue(int itemID) {
43  const uint32_t SUBJECT_ITEM_MAX_LENGTH = 2048;
44  char buffer[SUBJECT_ITEM_MAX_LENGTH + 1] = {0};
45 
46  X509_NAME* subj = X509_get_subject_name(cert_);
47  if (subj == nullptr) {
48  throw std::runtime_error("Failed to retrieve a subject from the certificate");
49  }
50 
51  if (X509_NAME_get_text_by_NID(subj, itemID, buffer, SUBJECT_ITEM_MAX_LENGTH) == -1) {
52  throw std::runtime_error("Failed to retrieve an item from from the certificate subject");
53  }
54 
55  return buffer;
56  }
57 
58  private:
59  FILE* fp_;
60  X509* cert_;
61  };
62 
63  protected:
64  TemporaryDirectory tmp_dir_;
65  TestArgs test_args_{tmp_dir_};
66  DeviceCredGenerator device_cred_gen_{CERT_PROVIDER_PATH.string()};
67 };
68 
69 /**
70  * Verifies generation and serialization of a device private key and a certificate (including its signing)
71  * in case of the fleet credentials usage (i.e. a fleet private key) for the certificate signing.
72  *
73  * - [x] Use fleet credentials if provided
74  * - [x] Read fleet CA certificate
75  * - [x] Read fleet private key
76  * - [x] Create device certificate
77  * - [x] Create device keys
78  * - [x] Set public key for the device certificate
79  * - [x] Sign device certificate with fleet private key
80  * - [x] Serialize device private key to a string (we actually can verify only 'searilized' version of the key )
81  * - [x] Serialize device certificate to a string (we actually can verify only 'serialized' version of the certificate)
82  * - [x] Write credentials to a local directory if requested
83  * - [x] Provide device private key
84  * - [x] Provide device certificate
85  */
86 
87 TEST_F(AktualizrCertProviderTest, DeviceCredCreationWithFleetCred) {
89 
90  args.fleetCA = test_args_.fleet_ca_cert;
91  args.fleetCAKey = test_args_.fleet_ca_private_key;
92  args.localDir = test_args_.test_dir;
93 
94  device_cred_gen_.run(args);
95  ASSERT_EQ(device_cred_gen_.lastExitCode(), 0) << device_cred_gen_.lastStdErr();
96 
97  DeviceCredGenerator::OutputPath device_cred_path(test_args_.test_dir);
98 
99  ASSERT_TRUE(boost::filesystem::exists(device_cred_path.privateKeyFileFullPath))
100  << device_cred_path.privateKeyFileFullPath;
101  ASSERT_TRUE(boost::filesystem::exists(device_cred_path.certFileFullPath)) << device_cred_path.certFileFullPath;
102 
103  Process openssl("/usr/bin/openssl");
104 
105  openssl.run({"rsa", "-in", device_cred_path.privateKeyFileFullPath.string(), "-noout", "-check"});
106  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
107  ASSERT_EQ(openssl.lastStdOut(), "RSA key ok\n") << openssl.lastStdOut();
108 
109  openssl.run({"x509", "-in", device_cred_path.certFileFullPath.string(), "-noout", "-pubkey"});
110  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
111  ASSERT_NE(openssl.lastStdOut().find("-----BEGIN PUBLIC KEY-----\n"), std::string::npos) << openssl.lastStdOut();
112 
113  openssl.run({"rsa", "-in", device_cred_path.privateKeyFileFullPath.string(), "-noout", "-modulus"});
114  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
115  const std::string private_key_modulus = openssl.lastStdOut();
116 
117  openssl.run({"x509", "-in", device_cred_path.certFileFullPath.string(), "-noout", "-modulus"});
118  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
119  const std::string public_key_modulus = openssl.lastStdOut();
120 
121  ASSERT_EQ(private_key_modulus, public_key_modulus);
122 
123  openssl.run({"verify", "-verbose", "-CAfile", test_args_.fleet_ca_cert, device_cred_path.certFileFullPath.string()});
124  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
125  ASSERT_EQ(openssl.lastStdOut(), str(boost::format("%1%: OK\n") % device_cred_path.certFileFullPath.string()));
126 }
127 
128 /**
129  * Verifies cert_provider's output if an incomplete set of fleet credentials is specified.
130  * Just a fleet CA without a fleet private key.
131  * Just a fleet private key without a fleet CA
132  * Neither `--target` nor `--local` is specified
133  *
134  * Checks actions:
135  *
136  * - [x] Abort if fleet CA is provided without fleet private key
137  * - [x] Abort if fleet private key is provided without fleet CA
138  */
139 
140 TEST_F(AktualizrCertProviderTest, IncompleteFleetCredentials) {
141  const std::string expected_error_msg = "fleet-ca and fleet-ca-key options should be used together\n";
142 
143  {
145 
146  args.fleetCA = test_args_.fleet_ca_cert;
147  // args.fleetCAKey = test_args_.fleet_ca_private_key;
148  args.localDir = test_args_.test_dir;
149 
150  device_cred_gen_.run(args);
151 
152  EXPECT_EQ(device_cred_gen_.lastExitCode(), 1) << device_cred_gen_.lastStdOut();
153  ASSERT_EQ(device_cred_gen_.lastStdErr(), expected_error_msg) << device_cred_gen_.lastStdErr();
154  }
155 
156  {
158 
159  // args.fleetCA = test_args_.fleet_ca_cert;
160  args.fleetCAKey = test_args_.fleet_ca_private_key;
161  args.localDir = test_args_.test_dir;
162 
163  device_cred_gen_.run(args);
164 
165  EXPECT_EQ(device_cred_gen_.lastExitCode(), 1) << device_cred_gen_.lastStdOut();
166  ASSERT_EQ(device_cred_gen_.lastStdErr(), expected_error_msg) << device_cred_gen_.lastStdErr();
167  }
168 
169  {
171 
172  args.fleetCA = test_args_.fleet_ca_cert;
173  args.fleetCAKey = test_args_.fleet_ca_private_key;
174  // args.localDir = test_args_.test_dir;
175 
176  device_cred_gen_.run(args);
177 
178  ASSERT_EQ(device_cred_gen_.lastExitCode(), 1);
179  ASSERT_NE(device_cred_gen_.lastStdErr().find(
180  "Please provide a local directory and/or target to output the generated files to"),
181  std::string::npos)
182  << device_cred_gen_.lastStdErr();
183  }
184 }
185 
186 /**
187  * Verifies usage of the paths from a config file which is specified via `--config` param.
188  * The resultant files's path, private key and certificate files, must correspond to what is specified in the config.
189  *
190  * Verifies cert_provider's output if both `--directory` and `--config` params are specified
191  *
192  * Checks actions:
193  *
194  * - [x] Use file paths from config if provided
195  */
196 TEST_F(AktualizrCertProviderTest, ConfigFilePathUsage) {
197  const std::string base_path = "my_device_cred";
198  const std::string private_key_file = "my_device_private_key.pem";
199  const std::string cert_file = "my_device_cert.pem";
200 
201  Config config;
202  config.import.base_path = base_path;
203  config.import.tls_pkey_path = utils::BasedPath(private_key_file);
204  config.import.tls_clientcert_path = utils::BasedPath(cert_file);
205 
206  auto test_conf_file = tmp_dir_ / "conf.toml";
207  boost::filesystem::ofstream conf_file(test_conf_file);
208  config.writeToStream(conf_file);
209  conf_file.close();
210 
211  DeviceCredGenerator::OutputPath device_cred_path(test_args_.test_dir, base_path, private_key_file, cert_file);
213 
214  args.fleetCA = test_args_.fleet_ca_cert;
215  args.fleetCAKey = test_args_.fleet_ca_private_key;
216  args.localDir = test_args_.test_dir;
217  args.configFile = test_conf_file.string();
218 
219  device_cred_gen_.run(args);
220 
221  ASSERT_EQ(device_cred_gen_.lastExitCode(), 0) << device_cred_gen_.lastStdErr();
222 
223  ASSERT_TRUE(boost::filesystem::exists(device_cred_path.privateKeyFileFullPath))
224  << "Private key file is missing: " << device_cred_path.privateKeyFileFullPath;
225  ASSERT_TRUE(boost::filesystem::exists(device_cred_path.certFileFullPath))
226  << "Certificate file is missing: " << device_cred_path.certFileFullPath;
227 
228  {
229  // The case when both 'directory' and 'config' parameters are specified
230  args.directoryPrefix = "whatever-dir";
231 
232  device_cred_gen_.run(args);
233  EXPECT_EQ(device_cred_gen_.lastExitCode(), 1) << device_cred_gen_.lastStdErr();
234  EXPECT_EQ(device_cred_gen_.lastStdErr(),
235  "Directory (--directory) and config (--config) options cannot be used together\n")
236  << device_cred_gen_.lastStdErr();
237  }
238 }
239 
240 /**
241  * Verifies application of the certificate's and key's parameters specified via parameters
242  *
243  * Checks actions:
244  *
245  * - [x] Specify device certificate expiration date
246  * - [x] Specify device certificate country code
247  * - [x] Specify device certificate state abbreviation
248  * - [x] Specify device certificate organization name
249  * - [x] Specify device certificate common name
250  * - [x] Specify RSA bit length
251  */
252 
253 TEST_F(AktualizrCertProviderTest, DeviceCertParams) {
254  const std::string validity_days = "100";
255  auto expires_after_sec = (std::stoul(validity_days) * 24 * 3600) + 1;
256  std::unordered_map<int, std::string> subject_items = {
257  {NID_countryName, "UA"},
258  {NID_stateOrProvinceName, "Lviv"},
259  {NID_organizationName, "ATS"},
260  {NID_commonName, "ats.io"},
261  };
262  const std::string rsa_bits = "1024";
263 
265 
266  args.fleetCA = test_args_.fleet_ca_cert;
267  args.fleetCAKey = test_args_.fleet_ca_private_key;
268  args.localDir = test_args_.test_dir;
269 
270  args.validityDays = validity_days;
271  args.countryCode = subject_items[NID_countryName];
272  args.state = subject_items[NID_stateOrProvinceName];
273  args.organization = subject_items[NID_organizationName];
274  args.commonName = subject_items[NID_commonName];
275  args.rsaBits = rsa_bits;
276 
277  device_cred_gen_.run(args);
278  ASSERT_EQ(device_cred_gen_.lastExitCode(), 0) << device_cred_gen_.lastStdErr();
279 
280  DeviceCredGenerator::OutputPath device_cred_path(test_args_.test_dir);
281 
282  ASSERT_TRUE(boost::filesystem::exists(device_cred_path.privateKeyFileFullPath))
283  << device_cred_path.privateKeyFileFullPath;
284  ASSERT_TRUE(boost::filesystem::exists(device_cred_path.certFileFullPath)) << device_cred_path.certFileFullPath;
285 
286  // check subject's params
287  Cert cert(device_cred_path.certFileFullPath.string());
288  for (auto subject_item : subject_items) {
289  ASSERT_EQ(cert.getSubjectItemValue(subject_item.first), subject_item.second);
290  }
291 
292  Process openssl("/usr/bin/openssl");
293  // check RSA length
294  const std::string expected_key_str = str(boost::format("Private-Key: (%1% bit") % rsa_bits);
295  openssl.run({"rsa", "-in", device_cred_path.privateKeyFileFullPath.string(), "-text", "-noout"});
296  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
297  ASSERT_NE(openssl.lastStdOut().find(expected_key_str), std::string::npos);
298 
299  // check expiration date
300  openssl.run({"x509", "-in", device_cred_path.certFileFullPath.string(), "-checkend",
301  std::to_string(expires_after_sec - 1024)});
302 
303  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdOut();
304  ASSERT_NE(openssl.lastStdOut().find("Certificate will not expire"), std::string::npos);
305 
306  openssl.run(
307  {"x509", "-in", device_cred_path.certFileFullPath.string(), "-checkend", std::to_string(expires_after_sec)});
308  ASSERT_EQ(openssl.lastExitCode(), 1) << openssl.lastStdOut();
309  ASSERT_NE(openssl.lastStdOut().find("Certificate will expire"), std::string::npos);
310 
311  // check signature
312  openssl.run({"verify", "-verbose", "-CAfile", test_args_.fleet_ca_cert, device_cred_path.certFileFullPath.string()});
313  ASSERT_EQ(openssl.lastExitCode(), 0) << openssl.lastStdErr();
314  ASSERT_EQ(openssl.lastStdOut(), str(boost::format("%1%: OK\n") % device_cred_path.certFileFullPath.string()));
315 }
316 
317 #ifndef __NO_MAIN__
318 int main(int argc, char** argv) {
319  ::testing::InitGoogleTest(&argc, argv);
320 
321  if (argc < 2) {
322  std::cerr << "A path to the cert_provider is not specified." << std::endl;
323  return EXIT_FAILURE;
324  }
325 
326  CERT_PROVIDER_PATH = argv[1];
327  std::cout << "Path to the cert_provider executable: " << CERT_PROVIDER_PATH << std::endl;
328 
329  int test_run_res = RUN_ALL_TESTS();
330 
331  return test_run_res;
332 }
333 #endif
DeviceCredGenerator::OutputPath
Definition: cert_provider_test.h:79
utils::BasedPath
The BasedPath class Can represent an absolute or relative path, only readable through the BasePath::g...
Definition: types.h:31
AktualizrCertProviderTest::Cert
Definition: cert_provider_test.cc:22
Config
Configuration object for an aktualizr instance running on a Primary ECU.
Definition: config.h:208
DeviceCredGenerator
Definition: cert_provider_test.h:9
Process
Definition: test_utils.h:19
AktualizrCertProviderTest::TestArgs
Definition: cert_provider_shared_cred_test.cc:14
TemporaryDirectory
Definition: utils.h:82
DeviceCredGenerator::ArgSet
Definition: cert_provider_test.h:13
AktualizrCertProviderTest
Definition: cert_provider_shared_cred_test.cc:12