blob: 246d09d19b4ab6b4fdd603b149d4fde996e91eed [file] [log] [blame] [edit]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cryptohome/cryptorecovery/inclusion_proof.h"
#include <string>
#include <utility>
#include <vector>
#include <absl/strings/numbers.h>
#include <base/base64.h>
#include <base/base64url.h>
#include <base/containers/span.h>
#include <base/numerics/byte_conversions.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_tokenizer.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <brillo/data_encoding.h>
#include <brillo/secure_blob.h>
#include <brillo/strings/string_utils.h>
#include <crypto/scoped_openssl_types.h>
#include <libhwsec-foundation/crypto/error_util.h>
#include <libhwsec-foundation/crypto/sha.h>
#include <openssl/ec.h>
#include <openssl/x509.h>
#include "cryptohome/cryptorecovery/inclusion_proof_util.h"
#include "cryptohome/cryptorecovery/recovery_crypto_hsm_cbor_serialization.h"
#include "cryptohome/cryptorecovery/recovery_crypto_util.h"
namespace cryptohome::cryptorecovery {
namespace {
// The number of checkpoint note fields should be 2: the signaute and the text.
constexpr int kCheckpointNoteSize = 2;
// The number of checkpoint fields should be 3: origin, size, hash.
constexpr int kCheckpointSize = 3;
// Signature hash is defined as the first 4 bytes from signature string from the
// server.
constexpr int kSignatureHashSize = 4;
// This value is reflecting to the value from the server side.
constexpr int kMaxSignatureNumber = 100;
// Allowed difference between the timestamp in the logged record and
// now.
// The value is the length of two epochs (one epoch is 15 minutes) + one minute
// tolerance.
constexpr base::TimeDelta kTimestampCheckAllowedDelta = base::Minutes(31);
// Allowed difference between the timestamp and calculated public timestamp.
constexpr base::TimeDelta kPublicTimestampConversionAllowedDelta =
base::Days(1);
struct Signature {
std::string name;
uint32_t key_hash = 0;
bool is_verified = false;
std::string openssl_error;
};
std::string SerializeSignatures(const std::vector<Signature> signatures) {
std::string result;
for (const auto& sig : signatures) {
result += base::StringPrintf(
"{name=%s,key_hash=%u,is_verified=%d,openssl_error=%s}",
sig.name.c_str(), sig.key_hash, sig.is_verified,
sig.openssl_error.c_str());
result += ",";
}
return result;
}
// CalculateInnerProofSize breaks down inclusion proof for a leaf at the
// specified |index| in a tree of the specified |size| into 2 components. The
// splitting point between them is where paths to leaves |index| and |size-1|
// diverge. Returns lengths of the bottom proof parts.
// clang-format off
// NOLINTNEXTLINE(whitespace/line_length)
// See google3/third_party/chromeos_hsm_reverse_proxy/lua/hsm/fortanix/verify.lua?l=123.
// clang-format on
int CalculateInnerProofSize(int index, int size) {
CHECK_GT(index, -1);
CHECK_GT(size, 0);
int xor_number = index ^ (size - 1);
int bits_number = 0;
while (xor_number > 0) {
xor_number = xor_number / 2;
bits_number++;
}
return bits_number;
}
// clang-format off
// NOLINTNEXTLINE(whitespace/line_length)
// See google3/third_party/chromeos_hsm_reverse_proxy/lua/hsm/fortanix/verify.lua?l=73.
// clang-format on
bool ReadSignatures(const std::string& text,
const std::string& signatures,
EC_KEY* ledger_key,
std::vector<Signature>* out_signatures) {
base::StringTokenizer tokenizer(signatures, kInclusionProofNewline);
tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS);
int num_sig = 0;
while (tokenizer.GetNext()) {
// `signature_line` has the format:
// "{prefix}{signature_name}{name_split}{base64_signature}".
// Where:
// - prefix = kInclusionProofSigPrefix,
// - name_split = kInclusionProofSigNameSplit.
std::string signature_line = tokenizer.token();
// Verify that the signature indeed ends with kInclusionProofNewline.
if (!tokenizer.GetNext() || tokenizer.token() != kInclusionProofNewline) {
LOG(ERROR) << "Failed to pull out one signature";
return false;
}
num_sig++;
// Avoid spending forever parsing a note with many signatures.
if (num_sig > kMaxSignatureNumber)
return false;
if (!base::StartsWith(signature_line, kInclusionProofSigPrefix,
base::CompareCase::SENSITIVE)) {
LOG(ERROR) << "No signature prefix is found.";
return false;
}
// The ledger's name (signature_tokens[0]) could be parsed out with
// separator of kInclusionProofSigNameSplit. And the signature and the key
// hash (signature_tokens[1]) is located after kInclusionProofSigNameSplit.
std::vector<std::string> signature_tokens = base::SplitString(
signature_line.substr(strlen(kInclusionProofSigPrefix),
signature_line.length()),
kInclusionProofSigNameSplit, base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
if (signature_tokens.size() != 2) {
LOG(ERROR) << "No signature name split is found.";
return false;
}
std::string signature_name = signature_tokens[0];
// `signature_str` has the format: "{key_hash}{signature_bytes}".
std::string signature_str;
if (!brillo::data_encoding::Base64Decode(signature_tokens[1],
&signature_str)) {
LOG(ERROR) << "Failed to convert base64 string to string.";
return false;
}
if (signature_str.length() < kSignatureHashSize) {
LOG(ERROR) << "The length of the signature is not long enough.";
return false;
}
uint32_t key_hash = base::numerics::U32FromBigEndian(
base::as_byte_span(signature_str.substr(0, kSignatureHashSize))
.first<4u>());
brillo::SecureBlob text_hash = hwsec_foundation::Sha256(
brillo::SecureBlob(text + kInclusionProofSigSplit[0]));
signature_str = signature_str.substr(kSignatureHashSize);
// Verify the signature and the hash.
bool is_verified =
ECDSA_verify(
0, reinterpret_cast<const unsigned char*>(text_hash.char_data()),
text_hash.size(),
reinterpret_cast<const unsigned char*>(signature_str.c_str()),
signature_str.length(), ledger_key) == 1;
Signature signature{
.name = signature_name,
.key_hash = key_hash,
.is_verified = is_verified,
};
if (!is_verified) {
signature.openssl_error = hwsec_foundation::GetOpenSSLErrors();
}
out_signatures->push_back(signature);
}
return true;
}
bool VerifySignature(const std::string& text,
const std::string& signatures,
const LedgerInfo& ledger_info) {
if (ledger_info.name.empty()) {
LOG(ERROR) << "Ledger name is empty.";
return false;
}
if (ledger_info.public_key->empty()) {
LOG(ERROR) << "Ledger public key is not present.";
return false;
}
// Import Public key of PKIX, ASN.1 DER form to EC_KEY.
std::string ledger_public_key_decoded;
if (!base::Base64UrlDecode(
brillo::BlobToString(ledger_info.public_key.value()),
base::Base64UrlDecodePolicy::IGNORE_PADDING,
&ledger_public_key_decoded)) {
LOG(ERROR) << "Failed at decoding from url base64.";
return false;
}
const unsigned char* asn1_ptr =
reinterpret_cast<const unsigned char*>(ledger_public_key_decoded.c_str());
crypto::ScopedEC_KEY public_key(
d2i_EC_PUBKEY(nullptr, &asn1_ptr, ledger_public_key_decoded.length()));
if (!public_key.get() || !EC_KEY_check_key(public_key.get())) {
LOG(ERROR) << "Failed to decode ECC public key.";
return false;
}
std::vector<Signature> signatures_list;
if (!ReadSignatures(text, signatures, public_key.get(), &signatures_list)) {
LOG(ERROR) << "Failed to read signatures.";
return false;
}
for (const auto& sig : signatures_list) {
if (!sig.is_verified || sig.name != ledger_info.name ||
sig.key_hash != ledger_info.key_hash.value()) {
// Signature is not verified or unknown.
continue;
}
// Known signature is verified.
return true;
}
// No verified known signatures.
LOG(ERROR) << "No verified known signatures found: "
<< SerializeSignatures(signatures_list);
return false;
}
// ParseCheckpoint takes a raw checkpoint string and returns a parsed
// checkpoint, providing that:
// * at least one valid log signature is found; and
// * the checkpoint unmarshals correctly; and
// * TODO(b/281486839): verify that the log origin is as expected.
// Note: Only the ledger signature will be checked.
// clang-format off
// NOLINTNEXTLINE(whitespace/line_length)
// See google3/third_party/chromeos_hsm_reverse_proxy/lua/hsm/fortanix/verify.lua?l=65.
// clang-format on
bool ParseCheckPoint(std::string checkpoint_note_str,
const LedgerInfo& ledger_info,
Checkpoint* check_point) {
// `checkpoint_note_str` has the format:
// "{text}{kInclusionProofSigSplit}{signatures}".
std::vector<std::string> checkpoint_note_fields = brillo::string_utils::Split(
checkpoint_note_str, kInclusionProofSigSplit, /*trim_whitespaces=*/false,
/*purge_empty_strings=*/false);
if (checkpoint_note_fields.size() != kCheckpointNoteSize) {
LOG(ERROR) << "Checkpoint note is not valid.";
return false;
}
std::string text = checkpoint_note_fields[0];
std::string signatures = checkpoint_note_fields[1];
if (!VerifySignature(text, signatures, ledger_info)) {
LOG(ERROR) << "Failed to verify the signature of the checkpoint note.";
return false;
}
// The ledger has signed this checkpoint. It is now safe to parse.
// `checkpoint_fields` has the format: "{origin}\n{size}\n{base64_hash}".
// clang-format off
// NOLINTNEXTLINE(whitespace/line_length)
// See google3/third_party/chromeos_hsm_reverse_proxy/lua/hsm/fortanix/verify.lua?l=109.
// clang-format on
std::vector<std::string> checkpoint_fields =
brillo::string_utils::Split(text, kInclusionProofNewline);
if (checkpoint_fields.size() != kCheckpointSize) {
LOG(ERROR) << "Checkpoint is not valid.";
return false;
}
check_point->origin = checkpoint_fields[0];
if (!base::StringToInt64(checkpoint_fields[1], &check_point->size)) {
LOG(ERROR) << "Failed to convert checkpoint size string to int64_t.";
return false;
}
if (check_point->size < 1) {
LOG(ERROR) << "Checkpoint is not valid: size < 1.";
return false;
}
std::string check_point_hash_str;
if (!brillo::data_encoding::Base64Decode(checkpoint_fields[2],
&check_point_hash_str)) {
LOG(ERROR) << "Failed to decode base64 checkpoint hash.";
return false;
}
check_point->hash = brillo::BlobFromString(check_point_hash_str);
return true;
}
// CalculateRootHash calculates the expected root hash for a tree of the
// given size, provided a leaf index and leaf content with the corresponding
// inclusion proof. Requires 0 <= `leaf_index` < `size`.
// clang-format off
// NOLINTNEXTLINE(whitespace/line_length)
// See google3/third_party/chromeos_hsm_reverse_proxy/lua/hsm/fortanix/verify.lua?l=148.
// clang-format on
bool CalculateRootHash(const brillo::Blob& leaf,
const std::vector<brillo::Blob>& inclusion_proof,
int64_t leaf_index,
int64_t size,
brillo::Blob* root_hash) {
if (leaf_index < 0 || size < 1) {
LOG(ERROR) << "Leaf index or inclusion proof size is not valid.";
return false;
}
int64_t index = 0;
int inner_proof_size = CalculateInnerProofSize(leaf_index, /*size=*/size);
if (inner_proof_size > inclusion_proof.size()) {
LOG(ERROR) << "Calculated inner proof size is not valid.";
return false;
}
brillo::Blob seed = HashLeaf(leaf);
while (index < inner_proof_size) {
if (((leaf_index >> index) & 1) == 0) {
seed = HashChildren(seed, inclusion_proof[index]);
} else {
seed = HashChildren(inclusion_proof[index], seed);
}
index++;
}
while (index < inclusion_proof.size()) {
seed = HashChildren(inclusion_proof[index], seed);
index++;
}
*root_hash = seed;
return true;
}
std::string GetOnboardingMetadataDiff(const OnboardingMetadata& expected,
const OnboardingMetadata& actual) {
std::vector<std::string> messages;
if (expected.cryptohome_user_type != actual.cryptohome_user_type) {
messages.push_back(
base::StringPrintf("cryptohome_user_type: expected '%d' but got '%d'",
static_cast<int>(expected.cryptohome_user_type),
static_cast<int>(actual.cryptohome_user_type)));
}
if (expected.cryptohome_user != actual.cryptohome_user) {
messages.push_back(base::StringPrintf(
"cryptohome_user: expected '%s' but got '%s'",
expected.cryptohome_user.c_str(), actual.cryptohome_user.c_str()));
}
if (expected.device_user_id != actual.device_user_id) {
messages.push_back(base::StringPrintf(
"device_user_id: expected '%s' but got '%s'",
expected.device_user_id.c_str(), actual.device_user_id.c_str()));
}
if (expected.board_name != actual.board_name) {
messages.push_back(base::StringPrintf(
"board_name: expected '%s' but got '%s'", expected.board_name.c_str(),
actual.board_name.c_str()));
}
if (expected.form_factor != actual.form_factor) {
messages.push_back(base::StringPrintf(
"form_factor: expected '%s' but got '%s'", expected.form_factor.c_str(),
actual.form_factor.c_str()));
}
if (expected.rlz_code != actual.rlz_code) {
messages.push_back(
base::StringPrintf("rlz_code: expected '%s' but got '%s'",
expected.rlz_code.c_str(), actual.rlz_code.c_str()));
}
if (expected.recovery_id != actual.recovery_id) {
messages.push_back(base::StringPrintf(
"recovery_id: expected '%s' but got '%s'", expected.recovery_id.c_str(),
actual.recovery_id.c_str()));
}
return base::JoinString(messages, "; ");
}
bool VerifyMetadata(const LoggedRecord& logged_record,
const OnboardingMetadata& metadata) {
if (metadata.recovery_id != logged_record.public_ledger_entry.recovery_id) {
LOG(ERROR) << "Recovery id in public ledger entry doesn't match.";
return false;
}
if (metadata != logged_record.private_log_entry.onboarding_meta_data) {
LOG(ERROR) << "Onboarding metadata in private log entry doesn't match: "
<< GetOnboardingMetadataDiff(
metadata,
logged_record.private_log_entry.onboarding_meta_data);
return false;
}
brillo::Blob private_log_hash = hwsec_foundation::Sha256(
brillo::Blob(logged_record.serialized_private_log_entry));
if (private_log_hash != logged_record.public_ledger_entry.log_entry_hash) {
LOG(ERROR) << "Log entry hash in public ledger entry doesn't match.";
return false;
}
auto now = base::Time::Now();
auto timestamp =
base::Time::FromTimeT(logged_record.private_log_entry.timestamp);
auto public_timestamp =
base::Time::FromTimeT(logged_record.private_log_entry.public_timestamp);
if ((timestamp.UTCMidnight() - public_timestamp).magnitude() >
kPublicTimestampConversionAllowedDelta) {
LOG(ERROR) << "Public timestamp " << public_timestamp
<< " doesn't match the timestamp " << timestamp;
return false;
}
auto time_diff = (timestamp - now).magnitude();
if (time_diff > kTimestampCheckAllowedDelta) {
LOG(ERROR) << "The timestamp " << timestamp
<< " doesn't match the current time " << now
<< ", the difference is " << time_diff;
return false;
}
return true;
}
} // namespace
// clang-format off
// NOLINTNEXTLINE(whitespace/line_length)
// See google3/chromeos/identity/go/cryptorecovery/shared/ledger/ledgerproof.go?q=verify_proof, and
// google3/third_party/chromeos_hsm_reverse_proxy/lua/hsm/fortanix/verify.lua?q=verify_proof.
// clang-format on
bool VerifyInclusionProof(const LedgerSignedProof& ledger_signed_proof,
const LedgerInfo& ledger_info,
const OnboardingMetadata& metadata) {
// Check the metadata.
if (!VerifyMetadata(ledger_signed_proof.logged_record, metadata)) {
LOG(ERROR) << "Failed to verify metadata.";
return false;
}
// Parse checkpoint note.
Checkpoint check_point;
if (!ParseCheckPoint(
brillo::BlobToString(ledger_signed_proof.checkpoint_note),
ledger_info, &check_point)) {
LOG(ERROR) << "Failed to parse checkpoint note.";
return false;
}
// Calculate tree root.
brillo::Blob calculated_root_hash;
if (!CalculateRootHash(
ledger_signed_proof.logged_record.serialized_public_ledger_entry,
ledger_signed_proof.inclusion_proof,
ledger_signed_proof.logged_record.leaf_index, check_point.size,
&calculated_root_hash)) {
LOG(ERROR) << "Failed to calculate root hash.";
return false;
}
// Verify if the root hash is as expected.
return calculated_root_hash == check_point.hash;
}
} // namespace cryptohome::cryptorecovery