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