feat: add support for validating asar archives on macOS (#30667)

* feat: add support for validating asar archives on macOS

* chore: fix lint

* chore: update as per feedback

* feat: switch implementation to asar integrity hash checks

* feat: make ranged requests work with the asar file validator DataSourceFilter

* chore: fix lint

* chore: fix missing log include on non-darwin

* fix: do not pull block size out of missing optional

* fix: match ValidateOrDie symbol on non-darwin

* chore: fix up asar specs by repacking archives

* fix: maintain integrity chain, do not load file integrity if header integrity was not loaded

* debug test

* Update node-spec.ts

* fix: initialize header_validated_

* chore: update PR per feedback

* chore: update per feedback

* build: use final asar module

* Update fuses.json5
This commit is contained in:
Samuel Attard 2021-09-09 14:49:01 -07:00 committed by GitHub
parent fcad531f2e
commit 57d088517c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 705 additions and 43 deletions

View file

@ -18,6 +18,8 @@
#include "base/task/post_task.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "electron/fuses.h"
#include "shell/common/asar/asar_util.h"
#include "shell/common/asar/scoped_temporary_file.h"
#if defined(OS_WIN)
@ -95,6 +97,7 @@ bool GetNodeFromPath(std::string path,
bool FillFileInfoWithNode(Archive::FileInfo* info,
uint32_t header_size,
bool load_integrity,
const base::DictionaryValue* node) {
int size;
if (!node->GetInteger("size", &size))
@ -113,6 +116,42 @@ bool FillFileInfoWithNode(Archive::FileInfo* info,
node->GetBoolean("executable", &info->executable);
#if defined(OS_MAC)
if (load_integrity &&
electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled()) {
const base::DictionaryValue* integrity;
if (node->GetDictionary("integrity", &integrity)) {
IntegrityPayload integrity_payload;
std::string algorithm;
const base::ListValue* blocks;
int block_size;
if (integrity->GetString("algorithm", &algorithm) &&
integrity->GetString("hash", &integrity_payload.hash) &&
integrity->GetInteger("blockSize", &block_size) &&
integrity->GetList("blocks", &blocks) && block_size > 0) {
integrity_payload.block_size = static_cast<uint32_t>(block_size);
for (size_t i = 0; i < blocks->GetSize(); i++) {
std::string block;
if (!blocks->GetString(i, &block)) {
LOG(FATAL)
<< "Invalid block integrity value for file in ASAR archive";
}
integrity_payload.blocks.push_back(block);
}
if (algorithm == "SHA256") {
integrity_payload.algorithm = HashAlgorithm::SHA256;
info->integrity = std::move(integrity_payload);
}
}
}
if (!info->integrity.has_value()) {
LOG(FATAL) << "Failed to read integrity for file in ASAR archive";
return false;
}
}
#endif
return true;
}
@ -191,6 +230,32 @@ bool Archive::Init() {
return false;
}
#if defined(OS_MAC)
// Validate header signature if required and possible
if (electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled() &&
RelativePath().has_value()) {
absl::optional<IntegrityPayload> integrity = HeaderIntegrity();
if (!integrity.has_value()) {
LOG(FATAL) << "Failed to get integrity for validatable asar archive: "
<< RelativePath().value();
return false;
}
// Currently we only support the sha256 algorithm, we can add support for
// more below ensure we read them in preference order from most secure to
// least
if (integrity.value().algorithm != HashAlgorithm::NONE) {
ValidateIntegrityOrDie(header.c_str(), header.length(),
integrity.value());
} else {
LOG(FATAL) << "No eligible hash for validatable asar archive: "
<< RelativePath().value();
}
header_validated_ = true;
}
#endif
absl::optional<base::Value> value = base::JSONReader::Read(header);
if (!value || !value->is_dict()) {
LOG(ERROR) << "Failed to parse header";
@ -203,6 +268,16 @@ bool Archive::Init() {
return true;
}
#if !defined(OS_MAC)
absl::optional<IntegrityPayload> Archive::HeaderIntegrity() const {
return absl::optional<IntegrityPayload>();
}
absl::optional<base::FilePath> Archive::RelativePath() const {
return absl::optional<base::FilePath>();
}
#endif
bool Archive::GetFileInfo(const base::FilePath& path, FileInfo* info) const {
if (!header_)
return false;
@ -215,7 +290,7 @@ bool Archive::GetFileInfo(const base::FilePath& path, FileInfo* info) const {
if (node->GetString("link", &link))
return GetFileInfo(base::FilePath::FromUTF8Unsafe(link), info);
return FillFileInfoWithNode(info, header_size_, node);
return FillFileInfoWithNode(info, header_size_, header_validated_, node);
}
bool Archive::Stat(const base::FilePath& path, Stats* stats) const {
@ -238,7 +313,7 @@ bool Archive::Stat(const base::FilePath& path, Stats* stats) const {
return true;
}
return FillFileInfoWithNode(stats, header_size_, node);
return FillFileInfoWithNode(stats, header_size_, header_validated_, node);
}
bool Archive::Readdir(const base::FilePath& path,
@ -304,7 +379,8 @@ bool Archive::CopyFileOut(const base::FilePath& path, base::FilePath* out) {
auto temp_file = std::make_unique<ScopedTemporaryFile>();
base::FilePath::StringType ext = path.Extension();
if (!temp_file->InitFromFile(&file_, ext, info.offset, info.size))
if (!temp_file->InitFromFile(&file_, ext, info.offset, info.size,
info.integrity))
return false;
#if defined(OS_POSIX)
@ -319,7 +395,7 @@ bool Archive::CopyFileOut(const base::FilePath& path, base::FilePath* out) {
return true;
}
int Archive::GetFD() const {
int Archive::GetUnsafeFD() const {
return fd_;
}