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