blob: 9959c0fd83e2740db706a82ca48ba71d9d2bc34d [file] [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 <vector>
#include <absl/strings/numbers.h>
#include <base/base64.h>
#include <base/base64url.h>
#include <base/big_endian.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 <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/sha.h>
#include <openssl/ec.h>
#include <openssl/x509.h>
#include "cryptohome/cryptorecovery/recovery_crypto_util.h"
namespace cryptohome {
namespace cryptorecovery {
namespace {
constexpr char kSigSplit[] = "\n\n";
constexpr char kNewline[] = "\n";
constexpr char kSigPrefix[] = "— ";
constexpr char kSigNameSplit[] = " ";
constexpr int kLeafHashPrefix = 0;
constexpr int kNodeHashPrefix = 1;
// 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;
// Checkpoint represents a minimal log checkpoint (STH).
struct Checkpoint {
// Origin is the string identifying the log which issued this checkpoint.
std::string origin;
// Size is the number of entries in the log at this checkpoint.
int64_t size;
// Hash is the hash which commits to the contents of the entire log.
brillo::Blob hash;
};
// 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.
int CalculateInnerProofSize(int index, int size) {
DCHECK_GT(index, -1);
DCHECK_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;
}
// HashLeaf computes the hash of a leaf that exists.
brillo::Blob HashLeaf(const brillo::Blob& leaf_text) {
brillo::Blob prefix;
prefix.push_back(kLeafHashPrefix);
return hwsec_foundation::Sha256(brillo::CombineBlobs({prefix, leaf_text}));
}
// HashChildren computes interior nodes.
brillo::Blob HashChildren(const brillo::Blob& left, const brillo::Blob& right) {
brillo::Blob prefix;
prefix.push_back(kNodeHashPrefix);
return hwsec_foundation::Sha256(brillo::CombineBlobs({prefix, left, right}));
}
bool VerifySignature(const std::string& text,
const std::string& signatures,
const LedgerInfo& ledger_info) {
int num_sig = 0;
base::StringTokenizer tokenizer(signatures, kNewline);
tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS);
while (tokenizer.GetNext()) {
std::string signature_line = tokenizer.token();
// Verify that the signature indeed ends with kNewline.
if (!tokenizer.GetNext() || tokenizer.token() != kNewline) {
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, kSigPrefix,
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 kSigNameSplit. And the signature and the key hash
// (signature_tokens[1]) is located after kSigNameSplit.
std::vector<std::string> signature_tokens = base::SplitString(
signature_line.substr(strlen(kSigPrefix), signature_line.length()),
kSigNameSplit, 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_str;
if (!brillo::data_encoding::Base64Decode(signature_tokens[1],
&signature_str)) {
LOG(ERROR) << "Failed to convert base64 string to string.";
return false;
}
// Determine which ledger public key to use, dev or prod, based on the
// ledger's name and key hash.
if (signature_str.length() < kSignatureHashSize) {
LOG(ERROR) << "The length of the signature is not long enough.";
return false;
}
uint32_t key_hash;
base::ReadBigEndian(
reinterpret_cast<const uint8_t*>(
signature_str.substr(0, kSignatureHashSize).c_str()),
&key_hash);
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;
}
if (signature_tokens[0] != ledger_info.name ||
key_hash != ledger_info.key_hash.value()) {
LOG(ERROR) << "Unknown ledger key hash or name.";
return false;
}
// Impoty Public key of PKIX, ASN.1 DER form to EC_KEY.
std::string ledger_public_key_decoded;
if (!base::Base64UrlDecode(ledger_info.public_key.value().to_string(),
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;
}
brillo::SecureBlob signature_hash =
hwsec_foundation::Sha256(brillo::SecureBlob(text + kSigSplit[0]));
signature_str = signature_str.substr(kSignatureHashSize);
// Verify the signature and the hash.
if (ECDSA_verify(
0,
reinterpret_cast<const unsigned char*>(signature_hash.char_data()),
signature_hash.size(),
reinterpret_cast<const unsigned char*>(signature_str.c_str()),
signature_str.length(), public_key.get()) != 1) {
return false;
}
}
// Note had no verifiable signatures.
if (num_sig == 0)
return false;
return true;
}
// ParseCheckpoint takes a raw checkpoint string and returns a parsed checkpoint
// and any otherData in the body, providing that:
// * a valid log signature is found; and
// * the checkpoint unmarshals correctly; and
// * the log origin is that expected.
// The signatures on the note will include the log signature if no error is
// returned, plus any signatures from otherVerifiers that were found.
bool ParseCheckPoint(std::string checkpoint_note_str,
const LedgerInfo& ledger_info,
Checkpoint* check_point) {
std::vector<std::string> checkpoint_note_fields = brillo::string_utils::Split(
checkpoint_note_str, kSigSplit, /*trim_whitespaces=*/false,
/*purge_empty_strings=*/false);
if (checkpoint_note_fields.size() != kCheckpointNoteSize) {
LOG(ERROR) << "Checkpoint note is not valid.";
return false;
}
if (!VerifySignature(checkpoint_note_fields[0], checkpoint_note_fields[1],
ledger_info)) {
LOG(ERROR) << "Failed to verify the signature of the checkpoint note.";
return false;
}
std::vector<std::string> checkpoint_fields =
brillo::string_utils::Split(checkpoint_note_fields[0], kNewline);
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 string to int64_t";
return false;
}
if (check_point->size < 1) {
LOG(ERROR) << "Checkpoint is not valid.";
return false;
}
std::string check_point_hash_str;
if (!brillo::data_encoding::Base64Decode(checkpoint_fields[2],
&check_point_hash_str)) {
LOG(ERROR) << "Failed to convert base64 string to string.";
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 hash with the corresponding inclusion
// proof. Requires 0 <= index < size.
bool CalculateRootHash(const brillo::Blob& leaf_hash,
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 = leaf_hash;
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;
}
} // namespace
bool VerifyInclusionProof(const LedgerSignedProof& ledger_signed_proof,
const LedgerInfo& ledger_info) {
// 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(
HashLeaf(ledger_signed_proof.logged_record.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 cryptorecovery
} // namespace cryptohome