Aktualizr
C++ SOTA Client
All Classes Namespaces Files Functions Variables Enumerations Enumerator Pages
tuf.cc
1 #include "uptane/tuf.h"
2 
3 #include <ctime>
4 #include <ostream>
5 #include <sstream>
6 
7 #include <boost/algorithm/hex.hpp>
8 #include <boost/algorithm/string/case_conv.hpp>
9 #include <utility>
10 
11 #include "crypto/crypto.h"
12 #include "logging/logging.h"
13 #include "utilities/exceptions.h"
14 
15 using Uptane::Hash;
16 using Uptane::MetaPack;
17 using Uptane::Root;
18 using Uptane::Target;
19 using Uptane::Version;
20 
21 std::ostream &Uptane::operator<<(std::ostream &os, const Version &v) {
22  if (v.version_ == Version::ANY_VERSION) {
23  os << "vANY";
24  } else {
25  os << "v" << v.version_;
26  }
27  return os;
28 }
29 
30 std::ostream &Uptane::operator<<(std::ostream &os, const HardwareIdentifier &hwid) {
31  os << hwid.hwid_;
32  return os;
33 }
34 
35 std::ostream &Uptane::operator<<(std::ostream &os, const EcuSerial &ecu_serial) {
36  os << ecu_serial.ecu_serial_;
37  return os;
38 }
39 
40 Hash Hash::generate(Type type, const std::string &data) {
41  std::string hash;
42 
43  switch (type) {
44  case Type::kSha256: {
45  hash = boost::algorithm::hex(Crypto::sha256digest(data));
46  break;
47  }
48  case Type::kSha512: {
49  hash = boost::algorithm::hex(Crypto::sha512digest(data));
50  break;
51  }
52  default: { throw std::invalid_argument("Unsupported hash type"); }
53  }
54 
55  return Hash(type, hash);
56 }
57 
58 Hash::Hash(const std::string &type, const std::string &hash) : hash_(boost::algorithm::to_upper_copy(hash)) {
59  if (type == "sha512") {
60  type_ = Hash::Type::kSha512;
61  } else if (type == "sha256") {
62  type_ = Hash::Type::kSha256;
63  } else {
64  type_ = Hash::Type::kUnknownAlgorithm;
65  }
66 }
67 
68 Hash::Hash(Type type, const std::string &hash) : type_(type), hash_(boost::algorithm::to_upper_copy(hash)) {}
69 
70 bool Hash::operator==(const Hash &other) const { return type_ == other.type_ && hash_ == other.hash_; }
71 
72 std::string Hash::TypeString() const {
73  switch (type_) {
74  case Type::kSha256:
75  return "sha256";
76  case Type::kSha512:
77  return "sha512";
78  default:
79  return "unknown";
80  }
81 }
82 
83 Hash::Type Hash::type() const { return type_; }
84 
85 std::ostream &Uptane::operator<<(std::ostream &os, const Hash &h) {
86  os << "Hash: " << h.hash_;
87  return os;
88 }
89 
90 std::string Hash::encodeVector(const std::vector<Uptane::Hash> &hashes) {
91  std::stringstream hs;
92 
93  for (auto it = hashes.cbegin(); it != hashes.cend(); it++) {
94  hs << it->TypeString() << ":" << it->HashString();
95  if (std::next(it) != hashes.cend()) {
96  hs << ";";
97  }
98  }
99 
100  return hs.str();
101 }
102 
103 std::vector<Uptane::Hash> Hash::decodeVector(std::string hashes_str) {
104  std::vector<Uptane::Hash> hash_v;
105 
106  std::string cs = std::move(hashes_str);
107  while (!cs.empty()) {
108  size_t scp = cs.find(';');
109  std::string hash_token = cs.substr(0, scp);
110  if (scp == std::string::npos) {
111  cs = "";
112  } else {
113  cs = cs.substr(scp + 1);
114  }
115  if (hash_token.empty()) {
116  break;
117  }
118 
119  size_t cp = hash_token.find(':');
120  std::string hash_type_str = hash_token.substr(0, cp);
121  if (cp == std::string::npos) {
122  break;
123  }
124  std::string hash_value_str = hash_token.substr(cp + 1);
125 
126  if (!hash_value_str.empty()) {
127  Uptane::Hash h{hash_type_str, hash_value_str};
128  if (h.type() != Uptane::Hash::Type::kUnknownAlgorithm) {
129  hash_v.push_back(std::move(h));
130  }
131  }
132  }
133 
134  return hash_v;
135 }
136 
137 Target::Target(std::string filename, const Json::Value &content) : filename_(std::move(filename)) {
138  if (content.isMember("custom")) {
139  custom_ = content["custom"];
140 
141  // Image repo provides an array of hardware IDs.
142  if (custom_.isMember("hardwareIds")) {
143  Json::Value hwids = custom_["hardwareIds"];
144  for (auto i = hwids.begin(); i != hwids.end(); ++i) {
145  hwids_.emplace_back(HardwareIdentifier((*i).asString()));
146  }
147  }
148 
149  // Director provides a map of ECU serials to hardware IDs.
150  Json::Value ecus = custom_["ecuIdentifiers"];
151  for (auto i = ecus.begin(); i != ecus.end(); ++i) {
152  ecus_.insert({EcuSerial(i.key().asString()), HardwareIdentifier((*i)["hardwareId"].asString())});
153  }
154 
155  if (custom_.isMember("targetFormat")) {
156  type_ = custom_["targetFormat"].asString();
157  }
158 
159  if (custom_.isMember("uri")) {
160  std::string custom_uri = custom_["uri"].asString();
161  // Ignore this exact URL for backwards compatibility with old defaults that inserted it.
162  if (custom_uri != "https://example.com/") {
163  uri_ = std::move(custom_uri);
164  }
165  }
166  }
167 
168  length_ = content["length"].asUInt64();
169 
170  Json::Value hashes = content["hashes"];
171  for (auto i = hashes.begin(); i != hashes.end(); ++i) {
172  Hash h(i.key().asString(), (*i).asString());
173  if (h.HaveAlgorithm()) {
174  hashes_.push_back(h);
175  }
176  }
177  // sort hashes so that higher priority hash algorithm goes first
178  std::sort(hashes_.begin(), hashes_.end(), [](const Hash &l, const Hash &r) { return l.type() < r.type(); });
179 }
180 
181 Target::Target(std::string filename, EcuMap ecus, std::vector<Hash> hashes, uint64_t length, std::string correlation_id)
182  : filename_(std::move(filename)),
183  ecus_(std::move(ecus)),
184  hashes_(std::move(hashes)),
185  length_(length),
186  correlation_id_(std::move(correlation_id)) {
187  // sort hashes so that higher priority hash algorithm goes first
188  std::sort(hashes_.begin(), hashes_.end(), [](const Hash &l, const Hash &r) { return l.type() < r.type(); });
189 }
190 
191 Target Target::Unknown() {
192  Json::Value t_json;
193  t_json["hashes"]["sha256"] = boost::algorithm::to_lower_copy(boost::algorithm::hex(Crypto::sha256digest("")));
194  t_json["length"] = 0;
195  Uptane::Target target{"unknown", t_json};
196 
197  target.valid = false;
198 
199  return target;
200 }
201 
202 bool Target::MatchHash(const Hash &hash) const {
203  return (std::find(hashes_.begin(), hashes_.end(), hash) != hashes_.end());
204 }
205 
206 std::string Target::hashString(Hash::Type type) const {
207  std::vector<Uptane::Hash>::const_iterator it;
208  for (it = hashes_.begin(); it != hashes_.end(); it++) {
209  if (it->type() == type) {
210  return boost::algorithm::to_lower_copy(it->HashString());
211  }
212  }
213  return std::string("");
214 }
215 
216 std::string Target::sha256Hash() const { return hashString(Hash::Type::kSha256); }
217 
218 std::string Target::sha512Hash() const { return hashString(Hash::Type::kSha512); }
219 
220 bool Target::IsOstree() const {
221  if (type_ == "OSTREE") {
222  // Modern servers explicitly specify the type of the target
223  return true;
224  } else if (type_.empty() && length() == 0) {
225  // Older servers don't specify the type of the target. Assume that it is
226  // an OSTree target if the length is zero.
227  return true;
228  } else {
229  // If type is explicitly not OSTREE or the length is non-zero, then this
230  // is a firmware blob.
231  return false;
232  }
233 }
234 
235 bool Target::MatchTarget(const Target &t2) const {
236  // type_ (targetFormat) is only provided by the Image repo.
237  // ecus_ is only provided by the Image repo.
238  // correlation_id_ is only provided by the Director.
239  // uri_ is not matched. If the Director provides it, we use that. If not, but
240  // the Image repository does, use that. Otherwise, leave it empty and use the
241  // default.
242  if (filename_ != t2.filename_) {
243  return false;
244  }
245  if (length_ != t2.length_) {
246  return false;
247  }
248 
249  // If the HWID vector and ECU->HWID map match, we're good. Otherwise, assume
250  // we have a Target from the Director (ECU->HWID map populated, HWID vector
251  // empty) and a Target from the Image repo (HWID vector populated,
252  // ECU->HWID map empty). Figure out which Target has the map, and then for
253  // every item in the map, make sure it's in the other Target's HWID vector.
254  if (hwids_ != t2.hwids_ || ecus_ != t2.ecus_) {
255  std::shared_ptr<EcuMap> ecu_map; // Director
256  std::shared_ptr<std::vector<HardwareIdentifier>> hwid_vector; // Image repo
257  if (!hwids_.empty() && ecus_.empty() && t2.hwids_.empty() && !t2.ecus_.empty()) {
258  ecu_map = std::make_shared<EcuMap>(t2.ecus_);
259  hwid_vector = std::make_shared<std::vector<HardwareIdentifier>>(hwids_);
260  } else if (!t2.hwids_.empty() && t2.ecus_.empty() && hwids_.empty() && !ecus_.empty()) {
261  ecu_map = std::make_shared<EcuMap>(ecus_);
262  hwid_vector = std::make_shared<std::vector<HardwareIdentifier>>(t2.hwids_);
263  } else {
264  return false;
265  }
266  for (auto map_it = ecu_map->cbegin(); map_it != ecu_map->cend(); ++map_it) {
267  auto vec_it = find(hwid_vector->cbegin(), hwid_vector->cend(), map_it->second);
268  if (vec_it == hwid_vector->end()) {
269  return false;
270  }
271  }
272  }
273 
274  // requirements:
275  // - all hashes of the same type should match
276  // - at least one pair of hashes should match
277  bool oneMatchingHash = false;
278  for (const Hash &hash : hashes_) {
279  for (const Hash &hash2 : t2.hashes_) {
280  if (hash.type() == hash2.type() && !(hash == hash2)) {
281  return false;
282  }
283  if (hash == hash2) {
284  oneMatchingHash = true;
285  }
286  }
287  }
288  return oneMatchingHash;
289 }
290 
291 Json::Value Target::toDebugJson() const {
292  Json::Value res;
293  for (const auto &ecu : ecus_) {
294  res["custom"]["ecuIdentifiers"][ecu.first.ToString()]["hardwareId"] = ecu.second.ToString();
295  }
296  if (!hwids_.empty()) {
297  Json::Value hwids;
298  for (Json::Value::ArrayIndex i = 0; i < hwids_.size(); ++i) {
299  hwids[i] = hwids_[i].ToString();
300  }
301  res["custom"]["hardwareIds"] = hwids;
302  }
303  res["custom"]["targetFormat"] = type_;
304 
305  for (const auto &hash : hashes_) {
306  res["hashes"][hash.TypeString()] = hash.HashString();
307  }
308  res["length"] = Json::Value(static_cast<Json::Value::Int64>(length_));
309  return res;
310 }
311 
312 std::ostream &Uptane::operator<<(std::ostream &os, const Target &t) {
313  os << "Target(" << t.filename_;
314  os << " ecu_identifiers: (";
315  for (const auto &ecu : t.ecus_) {
316  os << ecu.first << " (hw_id: " << ecu.second << "), ";
317  }
318  os << ")"
319  << " hw_ids: (";
320  for (const auto &hwid : t.hwids_) {
321  os << hwid << ", ";
322  }
323  os << ")"
324  << " length:" << t.length();
325  os << " hashes: (";
326  for (const auto &hash : t.hashes_) {
327  os << hash << ", ";
328  }
329  os << "))";
330 
331  return os;
332 }
333 
334 void Uptane::BaseMeta::init(const Json::Value &json) {
335  if (!json.isObject() || !json.isMember("signed")) {
336  LOG_ERROR << "Failure during base metadata initialization from json";
337  throw Uptane::InvalidMetadata("", "", "invalid metadata json");
338  }
339 
340  version_ = json["signed"]["version"].asInt();
341  try {
342  expiry_ = TimeStamp(json["signed"]["expires"].asString());
343  } catch (const TimeStamp::InvalidTimeStamp &exc) {
344  throw Uptane::InvalidMetadata("", "", "invalid timestamp");
345  }
346  original_object_ = json;
347 }
348 Uptane::BaseMeta::BaseMeta(const Json::Value &json) { init(json); }
349 
350 Uptane::BaseMeta::BaseMeta(RepositoryType repo, const Role &role, const Json::Value &json,
351  const std::shared_ptr<MetaWithKeys> &signer) {
352  if (!json.isObject() || !json.isMember("signed")) {
353  throw Uptane::InvalidMetadata("", "", "invalid metadata json");
354  }
355 
356  signer->UnpackSignedObject(repo, role, json);
357 
358  init(json);
359 }
360 
361 void Uptane::Targets::init(const Json::Value &json) {
362  if (!json.isObject() || json["signed"]["_type"] != "Targets") {
363  throw Uptane::InvalidMetadata("", "targets", "invalid targets.json");
364  }
365 
366  const Json::Value target_list = json["signed"]["targets"];
367  for (auto t_it = target_list.begin(); t_it != target_list.end(); t_it++) {
368  Target t(t_it.key().asString(), *t_it);
369  targets.push_back(t);
370  }
371 
372  if (json["signed"]["delegations"].isObject()) {
373  const Json::Value key_list = json["signed"]["delegations"]["keys"];
374  ParseKeys(Uptane::RepositoryType::Image(), key_list);
375 
376  const Json::Value role_list = json["signed"]["delegations"]["roles"];
377  for (auto it = role_list.begin(); it != role_list.end(); it++) {
378  const std::string role_name = (*it)["name"].asString();
379  const Role role = Role::Delegation(role_name);
380  delegated_role_names_.push_back(role_name);
381  ParseRole(Uptane::RepositoryType::Image(), it, role, name_);
382 
383  const Json::Value paths_list = (*it)["paths"];
384  std::vector<std::string> paths;
385  for (auto p_it = paths_list.begin(); p_it != paths_list.end(); p_it++) {
386  paths.emplace_back((*p_it).asString());
387  }
388  paths_for_role_[role] = paths;
389 
390  terminating_role_[role] = (*it)["terminating"].asBool();
391  }
392  }
393 
394  if (json["signed"]["custom"].isObject()) {
395  correlation_id_ = json["signed"]["custom"]["correlationId"].asString();
396  } else {
397  correlation_id_ = "";
398  }
399 }
400 
401 Uptane::Targets::Targets(const Json::Value &json) : MetaWithKeys(json) { init(json); }
402 
403 Uptane::Targets::Targets(RepositoryType repo, const Role &role, const Json::Value &json,
404  const std::shared_ptr<MetaWithKeys> &signer)
405  : MetaWithKeys(repo, role, json, signer), name_(role.ToString()) {
406  init(json);
407 }
408 
409 void Uptane::TimestampMeta::init(const Json::Value &json) {
410  Json::Value hashes_list = json["signed"]["meta"]["snapshot.json"]["hashes"];
411  Json::Value meta_size = json["signed"]["meta"]["snapshot.json"]["length"];
412  Json::Value meta_version = json["signed"]["meta"]["snapshot.json"]["version"];
413  if (!json.isObject() || json["signed"]["_type"] != "Timestamp" || !hashes_list.isObject() ||
414  !meta_size.isIntegral() || !meta_version.isIntegral()) {
415  throw Uptane::InvalidMetadata("", "timestamp", "invalid timestamp.json");
416  }
417 
418  for (auto it = hashes_list.begin(); it != hashes_list.end(); ++it) {
419  Hash h(it.key().asString(), (*it).asString());
420  snapshot_hashes_.push_back(h);
421  }
422  snapshot_size_ = meta_size.asInt();
423  snapshot_version_ = meta_version.asInt();
424 }
425 
426 Uptane::TimestampMeta::TimestampMeta(const Json::Value &json) : BaseMeta(json) { init(json); }
427 
428 Uptane::TimestampMeta::TimestampMeta(RepositoryType repo, const Json::Value &json,
429  const std::shared_ptr<MetaWithKeys> &signer)
430  : BaseMeta(repo, Role::Timestamp(), json, signer) {
431  init(json);
432 }
433 
434 void Uptane::Snapshot::init(const Json::Value &json) {
435  Json::Value meta_list = json["signed"]["meta"];
436  if (!json.isObject() || json["signed"]["_type"] != "Snapshot" || !meta_list.isObject()) {
437  throw Uptane::InvalidMetadata("", "snapshot", "invalid snapshot.json");
438  }
439 
440  for (auto it = meta_list.begin(); it != meta_list.end(); ++it) {
441  Json::Value hashes_list = (*it)["hashes"];
442  Json::Value meta_size = (*it)["length"];
443  Json::Value meta_version = (*it)["version"];
444 
445  if (!meta_version.isIntegral()) {
446  throw Uptane::InvalidMetadata("", "snapshot", "invalid snapshot.json");
447  }
448 
449  auto role_name =
450  it.key().asString().substr(0, it.key().asString().rfind('.')); // strip extension from the role name
451  auto role_object = Role(role_name, !Role::IsReserved(role_name));
452 
453  if (meta_version.isIntegral()) {
454  role_version_[role_object] = meta_version.asInt();
455  } else {
456  role_version_[role_object] = -1;
457  }
458 
459  // Size and hashes are not required, but we may as well record them if
460  // present.
461  if (meta_size.isObject()) {
462  role_size_[role_object] = meta_size.asInt64();
463  } else {
464  role_size_[role_object] = -1;
465  }
466  if (hashes_list.isObject()) {
467  for (auto h_it = hashes_list.begin(); h_it != hashes_list.end(); ++h_it) {
468  Hash h(h_it.key().asString(), (*h_it).asString());
469  role_hashes_[role_object].push_back(h);
470  }
471  }
472  }
473 }
474 
475 Uptane::Snapshot::Snapshot(const Json::Value &json) : BaseMeta(json) { init(json); }
476 
477 Uptane::Snapshot::Snapshot(RepositoryType repo, const Json::Value &json, const std::shared_ptr<MetaWithKeys> &signer)
478  : BaseMeta(repo, Role::Snapshot(), json, signer) {
479  init(json);
480 }
481 
482 std::vector<Hash> Uptane::Snapshot::role_hashes(const Uptane::Role &role) const {
483  auto hashes = role_hashes_.find(role);
484  if (hashes == role_hashes_.end()) {
485  return std::vector<Hash>();
486  } else {
487  return hashes->second;
488  }
489 }
490 
491 int64_t Uptane::Snapshot::role_size(const Uptane::Role &role) const {
492  auto size = role_size_.find(role);
493  if (size == role_size_.end()) {
494  return 0;
495  } else {
496  return size->second;
497  }
498 }
499 
500 int Uptane::Snapshot::role_version(const Uptane::Role &role) const {
501  auto version = role_version_.find(role);
502  if (version == role_version_.end()) {
503  return -1;
504  } else {
505  return version->second;
506  }
507 };
508 
509 bool MetaPack::isConsistent() const {
510  TimeStamp now(TimeStamp::Now());
511  try {
512  if (director_root.original() != Json::nullValue) {
513  Uptane::Root original_root(director_root);
514  Uptane::Root new_root(RepositoryType::Director(), director_root.original(), new_root);
515  if (director_targets.original() != Json::nullValue) {
516  Uptane::Targets(RepositoryType::Director(), Role::Targets(), director_targets.original(),
517  std::make_shared<MetaWithKeys>(original_root));
518  }
519  }
520  } catch (const std::logic_error &exc) {
521  LOG_WARNING << "Inconsistent metadata: " << exc.what();
522  return false;
523  }
524  return true;
525 }
526 
527 int Uptane::extractVersionUntrusted(const std::string &meta) {
528  auto version_json = Utils::parseJSON(meta)["signed"]["version"];
529  if (!version_json.isIntegral()) {
530  return -1;
531  } else {
532  return version_json.asInt();
533  }
534 }
Uptane::Version
Metadata version numbers.
Definition: tuf.h:116
Version
Definition: helpers.h:10
Uptane::InvalidMetadata
Definition: exceptions.h:67
TimeStamp::InvalidTimeStamp
Definition: types.h:102
data
General data structures.
Definition: types.cc:55
Uptane::Hash
The hash of a file or TUF metadata.
Definition: tuf.h:209
Uptane::MetaPack
Definition: tuf.h:525
TimeStamp
Definition: types.h:86
Uptane::Targets
Definition: tuf.h:440
Uptane::Target::IsOstree
bool IsOstree() const
Is this an OSTree target? OSTree targets need special treatment because the hash doesn't represent th...
Definition: tuf.cc:220
Uptane::Role
TUF Roles.
Definition: tuf.h:57
Uptane::Target
Definition: tuf.h:238
Uptane::Root
Definition: tuf.h:385
Uptane::MetaWithKeys::UnpackSignedObject
virtual void UnpackSignedObject(RepositoryType repo, const Role &role, const Json::Value &signed_object)
Take a JSON blob that contains a signatures/signed component that is supposedly for a given role,...
Definition: metawithkeys.cc:58