Aktualizr
C++ SOTA Client
repo.cc
1 #include "repo.h"
2 
3 #include <array>
4 #include <ctime>
5 #include <regex>
6 
7 #include "crypto/crypto.h"
8 #include "director_repo.h"
9 #include "image_repo.h"
10 #include "libaktualizr/campaign.h"
11 #include "logging/logging.h"
12 
13 Repo::Repo(Uptane::RepositoryType repo_type, boost::filesystem::path path, const std::string &expires,
14  std::string correlation_id)
15  : repo_type_(repo_type), path_(std::move(path)), correlation_id_(std::move(correlation_id)) {
16  expiration_time_ = getExpirationTime(expires);
17  if (boost::filesystem::exists(path_)) {
18  if (boost::filesystem::directory_iterator(path_) != boost::filesystem::directory_iterator()) {
19  readKeys();
20  }
21  }
22 
23  if (repo_type_ == Uptane::RepositoryType::Director()) {
24  repo_dir_ = path_ / DirectorRepo::dir;
25  } else if (repo_type_ == Uptane::RepositoryType::Image()) {
26  repo_dir_ = path_ / ImageRepo::dir;
27  }
28 }
29 
30 void Repo::addDelegationToSnapshot(Json::Value *snapshot, const Uptane::Role &role) {
31  boost::filesystem::path repo_dir = repo_dir_;
32  if (role.IsDelegation()) {
33  repo_dir = repo_dir / "delegations";
34  }
35  std::string role_file_name = role.ToString() + ".json";
36 
37  Json::Value role_json = Utils::parseJSONFile(repo_dir / role_file_name)["signed"];
38  std::string signed_role = Utils::readFile(repo_dir / role_file_name);
39 
40  (*snapshot)["meta"][role_file_name]["version"] = role_json["version"].asUInt();
41 
42  if (role_json["delegations"].isObject()) {
43  auto delegations_list = role_json["delegations"]["roles"];
44 
45  for (auto it = delegations_list.begin(); it != delegations_list.end(); it++) {
46  addDelegationToSnapshot(snapshot, Uptane::Role((*it)["name"].asString(), true));
47  }
48  }
49 }
50 
51 void Repo::updateRepo() {
52  const Json::Value old_snapshot = Utils::parseJSONFile(repo_dir_ / "snapshot.json")["signed"];
53  Json::Value snapshot;
54  snapshot["_type"] = "Snapshot";
55  snapshot["expires"] = old_snapshot["expires"];
56  snapshot["version"] = (old_snapshot["version"].asUInt()) + 1;
57 
58  const Json::Value root = Utils::parseJSONFile(repo_dir_ / "root.json")["signed"];
59  snapshot["meta"]["root.json"]["version"] = root["version"].asUInt();
60 
61  addDelegationToSnapshot(&snapshot, Uptane::Role::Targets());
62 
63  const std::string signed_snapshot = Utils::jsonToCanonicalStr(signTuf(Uptane::Role::Snapshot(), snapshot));
64  Utils::writeFile(repo_dir_ / "snapshot.json", signed_snapshot);
65 
66  Json::Value timestamp = Utils::parseJSONFile(repo_dir_ / "timestamp.json")["signed"];
67  timestamp["version"] = (timestamp["version"].asUInt()) + 1;
68  timestamp["meta"]["snapshot.json"]["hashes"]["sha256"] =
69  boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha256digest(signed_snapshot)));
70  timestamp["meta"]["snapshot.json"]["hashes"]["sha512"] =
71  boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha512digest(signed_snapshot)));
72  timestamp["meta"]["snapshot.json"]["length"] = static_cast<Json::UInt>(signed_snapshot.length());
73  timestamp["meta"]["snapshot.json"]["version"] = snapshot["version"].asUInt();
74  Utils::writeFile(repo_dir_ / "timestamp.json",
75  Utils::jsonToCanonicalStr(signTuf(Uptane::Role::Timestamp(), timestamp)));
76 }
77 
78 Json::Value Repo::signTuf(const Uptane::Role &role, const Json::Value &json) {
79  auto key = keys_[role];
80  std::string b64sig =
81  Utils::toBase64(Crypto::Sign(key.public_key.Type(), nullptr, key.private_key, Utils::jsonToCanonicalStr(json)));
82  Json::Value signature;
83  switch (key.public_key.Type()) {
84  case KeyType::kRSA2048:
85  case KeyType::kRSA3072:
86  case KeyType::kRSA4096:
87  signature["method"] = "rsassa-pss";
88  break;
89  case KeyType::kED25519:
90  signature["method"] = "ed25519";
91  break;
92  default:
93  throw std::runtime_error("Unknown key type");
94  }
95  signature["sig"] = b64sig;
96 
97  Json::Value signed_data;
98  signature["keyid"] = key.public_key.KeyId();
99 
100  signed_data["signed"] = json;
101  signed_data["signatures"].append(signature);
102  return signed_data;
103 }
104 
105 std::string Repo::getExpirationTime(const std::string &expires) {
106  if (!expires.empty()) {
107  std::smatch match;
108  std::regex time_pattern("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); // NOLINT(modernize-raw-string-literal)
109  if (!std::regex_match(expires, time_pattern)) {
110  throw std::runtime_error("Expiration date has wrong format\n date should be in ISO 8601 UTC format");
111  }
112  return expires;
113  } else {
114  time_t raw_time;
115  struct tm time_struct {};
116  time(&raw_time);
117  gmtime_r(&raw_time, &time_struct);
118  time_struct.tm_year += 3;
119  std::array<char, 22> formatted{};
120  strftime(formatted.data(), formatted.size(), "%Y-%m-%dT%H:%M:%SZ", &time_struct);
121  return formatted.data();
122  }
123 }
124 void Repo::generateKeyPair(KeyType key_type, const Uptane::Role &key_name) {
125  boost::filesystem::path keys_dir = path_ / ("keys/" + repo_type_.toString() + "/" + key_name.ToString());
126  boost::filesystem::create_directories(keys_dir);
127 
128  std::string public_key_string;
129  std::string private_key;
130  if (!Crypto::generateKeyPair(key_type, &public_key_string, &private_key)) {
131  throw std::runtime_error("Key generation failure");
132  }
133  PublicKey public_key(public_key_string, key_type);
134 
135  std::stringstream key_str;
136  key_str << key_type;
137 
138  Utils::writeFile(keys_dir / "private.key", private_key);
139  Utils::writeFile(keys_dir / "public.key", public_key_string);
140  Utils::writeFile(keys_dir / "key_type", key_str.str());
141 
142  keys_[key_name] = KeyPair(public_key, private_key);
143 }
144 
145 void Repo::generateRepoKeys(KeyType key_type) {
146  generateKeyPair(key_type, Uptane::Role::Root());
147  generateKeyPair(key_type, Uptane::Role::Snapshot());
148  generateKeyPair(key_type, Uptane::Role::Targets());
149  generateKeyPair(key_type, Uptane::Role::Timestamp());
150 }
151 
152 void Repo::generateRepo(KeyType key_type) {
153  generateRepoKeys(key_type);
154 
155  boost::filesystem::create_directories(repo_dir_);
156  Json::Value root;
157  root["_type"] = "Root";
158  root["expires"] = expiration_time_;
159  root["version"] = 1;
160  for (auto const &keypair : keys_) {
161  root["keys"][keypair.second.public_key.KeyId()] = keypair.second.public_key.ToUptane();
162  }
163 
164  Json::Value role;
165  role["threshold"] = 1;
166 
167  role["keyids"].append(keys_[Uptane::Role::Root()].public_key.KeyId());
168  root["roles"]["root"] = role;
169 
170  role["keyids"].clear();
171  role["keyids"].append(keys_[Uptane::Role::Snapshot()].public_key.KeyId());
172  root["roles"]["snapshot"] = role;
173 
174  role["keyids"].clear();
175  role["keyids"].append(keys_[Uptane::Role::Targets()].public_key.KeyId());
176  root["roles"]["targets"] = role;
177 
178  role["keyids"].clear();
179  role["keyids"].append(keys_[Uptane::Role::Timestamp()].public_key.KeyId());
180  root["roles"]["timestamp"] = role;
181 
182  const std::string signed_root = Utils::jsonToCanonicalStr(signTuf(Uptane::Role::Root(), root));
183  Utils::writeFile(repo_dir_ / "root.json", signed_root);
184  Utils::writeFile(repo_dir_ / "1.root.json", signed_root);
185 
186  Json::Value targets;
187  targets["_type"] = "Targets";
188  targets["expires"] = expiration_time_;
189  targets["version"] = 1;
190  targets["targets"] = Json::objectValue;
191  if (repo_type_ == Uptane::RepositoryType::Director() && !correlation_id_.empty()) {
192  targets["custom"]["correlationId"] = correlation_id_;
193  }
194  const std::string signed_targets = Utils::jsonToCanonicalStr(signTuf(Uptane::Role::Targets(), targets));
195  Utils::writeFile(repo_dir_ / "targets.json", signed_targets);
196 
197  Json::Value snapshot;
198  snapshot["_type"] = "Snapshot";
199  snapshot["expires"] = expiration_time_;
200  snapshot["version"] = 1;
201  snapshot["meta"]["root.json"]["hashes"]["sha256"] =
202  boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha256digest(signed_root)));
203  snapshot["meta"]["root.json"]["hashes"]["sha512"] =
204  boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha512digest(signed_root)));
205  snapshot["meta"]["root.json"]["length"] = static_cast<Json::UInt>(signed_root.length());
206  snapshot["meta"]["root.json"]["version"] = 1;
207  snapshot["meta"]["targets.json"]["version"] = 1;
208  std::string signed_snapshot = Utils::jsonToCanonicalStr(signTuf(Uptane::Role::Snapshot(), snapshot));
209  Utils::writeFile(repo_dir_ / "snapshot.json", signed_snapshot);
210 
211  Json::Value timestamp;
212  timestamp["_type"] = "Timestamp";
213  timestamp["expires"] = expiration_time_;
214  timestamp["version"] = 1;
215  timestamp["meta"]["snapshot.json"]["hashes"]["sha256"] =
216  boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha256digest(signed_snapshot)));
217  timestamp["meta"]["snapshot.json"]["hashes"]["sha512"] =
218  boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha512digest(signed_snapshot)));
219  timestamp["meta"]["snapshot.json"]["length"] = static_cast<Json::UInt>(signed_snapshot.length());
220  timestamp["meta"]["snapshot.json"]["version"] = 1;
221  Utils::writeFile(repo_dir_ / "timestamp.json",
222  Utils::jsonToCanonicalStr(signTuf(Uptane::Role::Timestamp(), timestamp)));
223  if (repo_type_ == Uptane::RepositoryType::Director()) {
224  Utils::writeFile(path_ / DirectorRepo::dir / "manifest", std::string()); // just empty file to work with put method
225  }
226 }
227 
228 void Repo::generateCampaigns() const {
229  std::vector<campaign::Campaign> campaigns;
230  campaigns.resize(1);
231  auto &c = campaigns[0];
232 
233  c.name = "campaign1";
234  c.id = "c2eb7e8d-8aa0-429d-883f-5ed8fdb2a493";
235  c.size = 62470;
236  c.autoAccept = true;
237  c.description = "this is my message to show on the device";
238  c.estInstallationDuration = 10;
239  c.estPreparationDuration = 20;
240 
241  Json::Value json;
242  campaign::Campaign::JsonFromCampaigns(campaigns, json);
243 
244  Utils::writeFile(path_ / "campaigns.json", Utils::jsonToCanonicalStr(json));
245 }
246 
247 Json::Value Repo::getTarget(const std::string &target_name) {
248  const Json::Value image_targets = Utils::parseJSONFile(repo_dir_ / "targets.json")["signed"];
249  if (image_targets["targets"].isMember(target_name)) {
250  return image_targets["targets"][target_name];
251  } else if (repo_type_ == Uptane::RepositoryType::Image()) {
252  if (!boost::filesystem::is_directory(repo_dir_ / "delegations")) {
253  return {};
254  }
255  for (auto &p : boost::filesystem::directory_iterator(repo_dir_ / "delegations")) {
256  if (Uptane::Role::IsReserved(p.path().stem().string())) {
257  continue;
258  }
259  auto targets = Utils::parseJSONFile(p)["signed"];
260  if (targets["targets"].isMember(target_name)) {
261  return targets["targets"][target_name];
262  }
263  }
264  }
265  return {};
266 }
267 
268 void Repo::readKeys() {
269  auto keys_path = path_ / "keys" / repo_type_.toString();
270  for (auto &p : boost::filesystem::directory_iterator(keys_path)) {
271  std::string public_key_string = Utils::readFile(p / "public.key");
272  std::istringstream key_type_str(Utils::readFile(p / "key_type"));
273  KeyType key_type;
274  key_type_str >> key_type;
275  std::string private_key_string(Utils::readFile(p / "private.key"));
276  auto name = p.path().filename().string();
277  keys_[Uptane::Role(name, !Uptane::Role::IsReserved(name))] =
278  KeyPair(PublicKey(public_key_string, key_type), private_key_string);
279  }
280 }
281 
282 void Repo::refresh(const Uptane::Role &role) {
283  boost::filesystem::path meta_path = repo_dir_;
284 
285  if (repo_type_ == Uptane::RepositoryType::Director() &&
286  (role == Uptane::Role::Timestamp() || role == Uptane::Role::Snapshot())) {
287  throw std::runtime_error("The " + role.ToString() + " in the Director repo is not currently supported.");
288  }
289 
290  if (role == Uptane::Role::Root()) {
291  meta_path /= "root.json";
292  } else if (role == Uptane::Role::Timestamp()) {
293  meta_path /= "timestamp.json";
294  } else if (role == Uptane::Role::Snapshot()) {
295  meta_path /= "snapshot.json";
296  } else if (role == Uptane::Role::Targets()) {
297  meta_path /= "targets.json";
298  } else {
299  throw std::runtime_error("Refreshing custom role " + role.ToString() + " is not currently supported.");
300  }
301 
302  // The only interesting part here is to increment the version. It could be
303  // interesting to allow changing the expiry, too.
304  Json::Value meta_raw = Utils::parseJSONFile(meta_path)["signed"];
305  const unsigned version = meta_raw["version"].asUInt() + 1;
306 
307  auto current_expire_time = TimeStamp(meta_raw["expires"].asString());
308 
309  if (current_expire_time.IsExpiredAt(TimeStamp::Now())) {
310  time_t new_expiration_time;
311  std::time(&new_expiration_time);
312  new_expiration_time += 60 * 60; // make it valid for the next hour
313  struct tm new_expiration_time_str {};
314  gmtime_r(&new_expiration_time, &new_expiration_time_str);
315 
316  meta_raw["expires"] = TimeStamp(new_expiration_time_str).ToString();
317  }
318  meta_raw["version"] = version;
319  const std::string signed_meta = Utils::jsonToCanonicalStr(signTuf(role, meta_raw));
320  Utils::writeFile(meta_path, signed_meta);
321 
322  // Write a new numbered version of the Root if relevant.
323  if (role == Uptane::Role::Root()) {
324  std::stringstream root_name;
325  root_name << version << ".root.json";
326  Utils::writeFile(repo_dir_ / root_name.str(), signed_meta);
327  }
328 
329  updateRepo();
330 }
331 
332 Delegation::Delegation(const boost::filesystem::path &repo_path, std::string delegation_name)
333  : name(std::move(delegation_name)) {
334  if (Uptane::Role::IsReserved(name)) {
335  throw std::runtime_error("Delegation name " + name + " is reserved.");
336  }
337  boost::filesystem::path delegation_path(((repo_path / ImageRepo::dir / "delegations") / name).string() + ".json");
338  boost::filesystem::path targets_path(repo_path / ImageRepo::dir / "targets.json");
339  if (!boost::filesystem::exists(delegation_path) || !boost::filesystem::exists(targets_path)) {
340  throw std::runtime_error(std::string("delegation ") + delegation_path.string() + " does not exist");
341  }
342 
343  pattern = findPatternInTree(repo_path, name, Utils::parseJSONFile(targets_path)["signed"]);
344 
345  if (pattern.empty()) {
346  throw std::runtime_error("Could not find delegation role in the delegation tree");
347  }
348 }
349 
350 std::string Delegation::findPatternInTree(const boost::filesystem::path &repo_path, const std::string &name,
351  const Json::Value &targets_json) {
352  Json::Value delegations = targets_json["delegations"];
353  for (const auto &role : delegations["roles"]) {
354  auto role_name = role["name"].asString();
355  if (role_name == name) {
356  auto pattern = role["paths"][0].asString();
357  if (pattern.back() == '/') {
358  pattern.append("**");
359  }
360  return pattern;
361  } else {
362  auto pattern = findPatternInTree(
363  repo_path, name,
364  Utils::parseJSONFile((repo_path / ImageRepo::dir / "delegations") / (role_name + ".json"))["signed"]);
365  if (!pattern.empty()) {
366  return pattern;
367  }
368  }
369  }
370 
371  return "";
372 }
TimeStamp
Definition: types.h:188
Uptane::RepositoryType
Definition: tuf.h:21
PublicKey
Definition: types.h:119
Uptane::Role
TUF Roles.
Definition: tuf.h:61
KeyPair
Definition: repo.h:11