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