| // Copyright 2021 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_software_update/update_bundle_accessor.h" |
| |
| #include <cstddef> |
| #include <cstring> |
| #include <string_view> |
| |
| #include "pw_crypto/ecdsa.h" |
| #include "pw_crypto/sha256.h" |
| #include "pw_log/log.h" |
| #include "pw_protobuf/message.h" |
| #include "pw_result/result.h" |
| #include "pw_software_update/config.h" |
| #include "pw_software_update/update_bundle.pwpb.h" |
| #include "pw_stream/interval_reader.h" |
| #include "pw_stream/memory_stream.h" |
| |
| #define PW_LOG_LEVEL PW_SOFTWARE_UPDATE_CONFIG_LOG_LEVEL |
| |
| namespace pw::software_update { |
| namespace { |
| |
| constexpr std::string_view kTopLevelTargetsName = "targets"; |
| |
| Result<bool> VerifyEcdsaSignature(protobuf::Bytes public_key, |
| ConstByteSpan digest, |
| protobuf::Bytes signature) { |
| // TODO(pwbug/456): Move this logic into an variant of the API in |
| // pw_crypto:ecdsa that takes readers as inputs. |
| std::byte public_key_bytes[65]; |
| std::byte signature_bytes[64]; |
| stream::IntervalReader key_reader = public_key.GetBytesReader(); |
| stream::IntervalReader sig_reader = signature.GetBytesReader(); |
| PW_TRY(key_reader.Read(public_key_bytes)); |
| PW_TRY(sig_reader.Read(signature_bytes)); |
| Status status = crypto::ecdsa::VerifyP256Signature( |
| public_key_bytes, digest, signature_bytes); |
| if (!status.ok()) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Convert an integer from [0, 16) to a hex char |
| char IntToHex(uint8_t val) { |
| PW_ASSERT(val < 16); |
| return val >= 10 ? (val - 10) + 'a' : val + '0'; |
| } |
| |
| void LogKeyId(ConstByteSpan key_id) { |
| char key_id_str[pw::crypto::sha256::kDigestSizeBytes * 2 + 1] = {0}; |
| for (size_t i = 0; i < pw::crypto::sha256::kDigestSizeBytes; i++) { |
| uint8_t value = std::to_integer<uint8_t>(key_id[i]); |
| key_id_str[i * 2] = IntToHex((value >> 4) & 0xf); |
| key_id_str[i * 2 + 1] = IntToHex(value & 0xf); |
| } |
| |
| PW_LOG_DEBUG("key_id: %s", key_id_str); |
| } |
| |
| // Verifies signatures of a TUF metadata. |
| Result<bool> VerifyTufMetadataSignatures( |
| protobuf::Bytes message, |
| protobuf::RepeatedMessages signatures, |
| protobuf::Message signature_requirement, |
| protobuf::StringToMessageMap key_mapping) { |
| // Gets the threshold -- at least `threshold` number of signatures must |
| // pass verification in order to trust this metadata. |
| protobuf::Uint32 threshold = signature_requirement.AsUint32( |
| static_cast<uint32_t>(SignatureRequirement::Fields::THRESHOLD)); |
| PW_TRY(threshold.status()); |
| |
| // Gets the ids of keys that are allowed for verifying the signatures. |
| protobuf::RepeatedBytes allowed_key_ids = |
| signature_requirement.AsRepeatedBytes( |
| static_cast<uint32_t>(SignatureRequirement::Fields::KEY_IDS)); |
| PW_TRY(allowed_key_ids.status()); |
| |
| // Verifies the signatures. Check that at least `threshold` number of |
| // signatures can be verified using the allowed keys. |
| size_t verified_count = 0; |
| for (protobuf::Message signature : signatures) { |
| protobuf::Bytes key_id = |
| signature.AsBytes(static_cast<uint32_t>(Signature::Fields::KEY_ID)); |
| PW_TRY(key_id.status()); |
| |
| // Reads the key id into a buffer, so that we can check whether it is |
| // listed as allowed and look up the key value later. |
| std::byte key_id_buf[pw::crypto::sha256::kDigestSizeBytes]; |
| stream::IntervalReader key_id_reader = key_id.GetBytesReader(); |
| Result<ByteSpan> key_id_read_res = key_id_reader.Read(key_id_buf); |
| PW_TRY(key_id_read_res.status()); |
| if (key_id_read_res.value().size() != sizeof(key_id_buf)) { |
| return Status::Internal(); |
| } |
| |
| // Verify that the `key_id` is listed in `allowed_key_ids`. |
| // Note that the function assumes that the key id is properly derived |
| // from the key (via sha256). |
| bool key_id_is_allowed = false; |
| for (protobuf::Bytes trusted : allowed_key_ids) { |
| Result<bool> key_id_equal = trusted.Equal(key_id_buf); |
| PW_TRY(key_id_equal.status()); |
| if (key_id_equal.value()) { |
| key_id_is_allowed = true; |
| break; |
| } |
| } |
| |
| if (!key_id_is_allowed) { |
| PW_LOG_DEBUG("Skipping a key id not listed in allowed key ids."); |
| LogKeyId(key_id_buf); |
| continue; |
| } |
| |
| // Retrieves the signature bytes. |
| protobuf::Bytes sig = |
| signature.AsBytes(static_cast<uint32_t>(Signature::Fields::SIG)); |
| PW_TRY(sig.status()); |
| |
| // Extracts the key type, scheme and value information. |
| std::string_view key_id_str(reinterpret_cast<const char*>(key_id_buf), |
| sizeof(key_id_buf)); |
| protobuf::Message key_info = key_mapping[key_id_str]; |
| PW_TRY(key_info.status()); |
| |
| protobuf::Bytes key_val = |
| key_info.AsBytes(static_cast<uint32_t>(Key::Fields::KEYVAL)); |
| PW_TRY(key_val.status()); |
| |
| // The function assume that all keys are ECDSA keys. This is guaranteed |
| // by the fact that all trusted roots have undergone content check. |
| |
| // computes the sha256 hash |
| std::byte sha256_digest[32]; |
| stream::IntervalReader bytes_reader = message.GetBytesReader(); |
| PW_TRY(crypto::sha256::Hash(bytes_reader, sha256_digest)); |
| Result<bool> res = VerifyEcdsaSignature(key_val, sha256_digest, sig); |
| PW_TRY(res.status()); |
| if (res.value()) { |
| verified_count++; |
| if (verified_count == threshold.value()) { |
| return true; |
| } |
| } |
| } |
| |
| PW_LOG_DEBUG( |
| "Not enough number of signatures verified. Requires at least %u, " |
| "verified %u", |
| threshold.value(), |
| verified_count); |
| return false; |
| } |
| |
| // Verifies the signatures of a signed new root metadata against a given |
| // trusted root. The helper function extracts the corresponding key maping |
| // signature requirement, signatures from the trusted root and passes them |
| // to VerifyTufMetadataSignatures(). |
| // |
| // Precondition: The trusted root metadata has undergone content validity check. |
| Result<bool> VerifyRootMetadataSignatures(protobuf::Message trusted_root, |
| protobuf::Message new_root) { |
| // Retrieves the trusted root metadata content message. |
| protobuf::Message trusted = trusted_root.AsMessage(static_cast<uint32_t>( |
| SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA)); |
| PW_TRY(trusted.status()); |
| |
| // Retrieves the serialized new root metadata bytes. |
| protobuf::Bytes serialized = new_root.AsBytes(static_cast<uint32_t>( |
| SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA)); |
| PW_TRY(serialized.status()); |
| |
| // Gets the key mapping from the trusted root metadata. |
| protobuf::StringToMessageMap key_mapping = trusted.AsStringToMessageMap( |
| static_cast<uint32_t>(RootMetadata::Fields::KEYS)); |
| PW_TRY(key_mapping.status()); |
| |
| // Gets the signatures of the new root. |
| protobuf::RepeatedMessages signatures = new_root.AsRepeatedMessages( |
| static_cast<uint32_t>(SignedRootMetadata::Fields::SIGNATURES)); |
| PW_TRY(signatures.status()); |
| |
| // Gets the signature requirement from the trusted root metadata. |
| protobuf::Message signature_requirement = trusted.AsMessage( |
| static_cast<uint32_t>(RootMetadata::Fields::ROOT_SIGNATURE_REQUIREMENT)); |
| PW_TRY(signature_requirement.status()); |
| |
| // Verifies the signatures. |
| return VerifyTufMetadataSignatures( |
| serialized, signatures, signature_requirement, key_mapping); |
| } |
| |
| } // namespace |
| |
| Status UpdateBundleAccessor::OpenAndVerify(const ManifestAccessor&) { |
| PW_TRY(DoOpen()); |
| #if !defined(PW_SOFTWARE_UPDATE_LANDING_BUNDLE_VERIFICATION) |
| PW_TRY(DoVerify()); |
| #endif |
| return OkStatus(); |
| } |
| |
| // Get the target element corresponding to `target_file` |
| stream::IntervalReader UpdateBundleAccessor::GetTargetPayload( |
| std::string_view target_file_name) { |
| protobuf::StringToBytesMap target_payloads = |
| decoder_.AsStringToBytesMap(static_cast<uint32_t>( |
| pw::software_update::UpdateBundle::Fields::TARGET_PAYLOADS)); |
| PW_TRY(target_payloads.status()); |
| protobuf::Bytes payload = target_payloads[target_file_name]; |
| PW_TRY(payload.status()); |
| return payload.GetBytesReader(); |
| } |
| |
| Result<bool> UpdateBundleAccessor::IsTargetPayloadIncluded( |
| std::string_view target_file_name) { |
| // TODO(pwbug/456): Perform personalization check first. If the target |
| // is personalized out. Don't need to proceed. |
| |
| protobuf::StringToMessageMap signed_targets_metadata_map = |
| decoder_.AsStringToMessageMap(static_cast<uint32_t>( |
| pw::software_update::UpdateBundle::Fields::TARGETS_METADATA)); |
| PW_TRY(signed_targets_metadata_map.status()); |
| |
| // There should only be one element in the map, which is the top-level |
| // targets metadata. |
| protobuf::Message signed_targets_metadata = |
| signed_targets_metadata_map[kTopLevelTargetsName]; |
| PW_TRY(signed_targets_metadata.status()); |
| |
| protobuf::Message metadata = signed_targets_metadata.AsMessage( |
| static_cast<uint32_t>(pw::software_update::SignedTargetsMetadata::Fields:: |
| SERIALIZED_TARGETS_METADATA)); |
| PW_TRY(metadata.status()); |
| |
| protobuf::RepeatedMessages target_files = |
| metadata.AsRepeatedMessages(static_cast<uint32_t>( |
| pw::software_update::TargetsMetadata::Fields::TARGET_FILES)); |
| PW_TRY(target_files.status()); |
| |
| for (protobuf::Message target_file : target_files) { |
| protobuf::String name = target_file.AsString(static_cast<uint32_t>( |
| pw::software_update::TargetFile::Fields::FILE_NAME)); |
| PW_TRY(name.status()); |
| Result<bool> file_name_matches = name.Equal(target_file_name); |
| PW_TRY(file_name_matches.status()); |
| if (file_name_matches.value()) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| Status UpdateBundleAccessor::WriteManifest( |
| [[maybe_unused]] stream::Writer& staged_manifest_writer) { |
| return Status::Unimplemented(); |
| } |
| |
| Status UpdateBundleAccessor::Close() { |
| // TODO(pwbug/456): To be implemented. |
| return bundle_reader_.Close(); |
| } |
| |
| Status UpdateBundleAccessor::DoOpen() { |
| PW_TRY(bundle_.Init()); |
| PW_TRY(bundle_reader_.Open()); |
| decoder_ = |
| protobuf::Message(bundle_reader_, bundle_reader_.ConservativeReadLimit()); |
| (void)backend_; |
| return OkStatus(); |
| } |
| |
| Status UpdateBundleAccessor::DoVerify() { |
| if (disable_verification_) { |
| PW_LOG_WARN("Update bundle verification is disabled."); |
| return OkStatus(); |
| } |
| |
| // Verify and upgrade the on-device trust to the incoming root metadata if |
| // one is included. |
| PW_TRY(DoUpgradeRoot()); |
| |
| // TODO(pwbug/456): Verify the targets metadata against the current trusted |
| // root. |
| |
| // TODO(pwbug/456): Investigate whether targets payload verification should |
| // be performed here or deferred until a specific target is requested. |
| |
| // TODO(pwbug/456): Invoke the backend to do downstream verification of the |
| // bundle (e.g. compatibility and manifest completeness checks). |
| |
| return OkStatus(); |
| } |
| |
| Status UpdateBundleAccessor::DoUpgradeRoot() { |
| protobuf::Message new_root = decoder_.AsMessage( |
| static_cast<uint32_t>(UpdateBundle::Fields::ROOT_METADATA)); |
| if (new_root.status().IsNotFound()) { |
| return OkStatus(); |
| } |
| |
| PW_TRY(new_root.status()); |
| |
| // Get the trusted root and prepare for verification. |
| Result<stream::SeekableReader*> trusted_root_reader = |
| backend_.GetRootMetadataReader(); |
| PW_TRY(trusted_root_reader.status()); |
| PW_CHECK_NOTNULL(trusted_root_reader.value()); |
| protobuf::Message trusted_root( |
| *trusted_root_reader.value(), |
| trusted_root_reader.value()->ConservativeReadLimit()); |
| |
| // TODO(pwbug/456): Check whether the bundle contains a root metadata that |
| // is different from the on-device trusted root. |
| |
| // Verify the signatures against the trusted root metadata. |
| Result<bool> verify_res = |
| VerifyRootMetadataSignatures(trusted_root, new_root); |
| PW_TRY(verify_res.status()); |
| if (!verify_res.value()) { |
| PW_LOG_INFO("Fail to verify signatures against the current root"); |
| return Status::Unauthenticated(); |
| } |
| |
| // TODO(pwbug/456): Verifiy the content of the new root metadata, including: |
| // 1) Check role magic field. |
| // 2) Check signature requirement. Specifically, check that no key is |
| // reused across different roles and keys are unique in the same |
| // requirement. |
| // 3) Check key mapping. Specifically, check that all keys are unique, |
| // ECDSA keys, and the key ids are exactly the SHA256 of `key type + |
| // key scheme + key value`. |
| |
| // Verify the signatures against the new root metadata. |
| verify_res = VerifyRootMetadataSignatures(new_root, new_root); |
| PW_TRY(verify_res.status()); |
| if (!verify_res.value()) { |
| PW_LOG_INFO("Fail to verify signatures against the new root"); |
| return Status::Unauthenticated(); |
| } |
| |
| // TODO(pwbug/456): Check rollback. |
| |
| // Persist the new root. |
| stream::IntervalReader new_root_reader = new_root.ToBytes().GetBytesReader(); |
| PW_TRY(backend_.SafelyPersistRootMetadata(new_root_reader)); |
| |
| // TODO(pwbug/456): Implement key change detection to determine whether |
| // rotation has occured or not. Delete the persisted targets metadata version |
| // if any of the targets keys has been rotated. |
| |
| return OkStatus(); |
| } |
| |
| } // namespace pw::software_update |