blob: 7308aa8f4328540e0880f38662a96077cd95fbfc [file] [edit]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "crypto/unexportable_key_win.h"
#include <ncrypt.h>
#include <tbs.h>
#include <array>
#include <concepts>
#include <functional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/check_deref.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/containers/span_reader.h"
#include "base/containers/span_rust.h"
#include "base/containers/span_writer.h"
#include "base/containers/to_vector.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/numerics/byte_conversions.h"
#include "base/numerics/safe_conversions.h"
#include "base/numerics/safe_math.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/string_util_win.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/scoped_thread_priority.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/types/optional_util.h"
#include "base/win/delayload_helpers.h"
#include "crypto/ecdsa_utils.h"
#include "crypto/hash.h"
#include "crypto/keypair.h"
#include "crypto/random.h"
#include "crypto/sign.h"
#include "crypto/tpm_parser.h"
#include "crypto/unexportable_key.h"
#include "crypto/unexportable_key_metrics.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/boringssl/src/include/openssl/ec.h"
namespace crypto {
namespace {
// Persistent Storage Root Key (SRK) handles used as parent keys for TPM 2.0
// keys by the Microsoft Platform Crypto Provider (PCP).
//
// In the TCG TPM 2.0 handle registry, 0x81000001 is reserved for the primary
// RSA Storage Root Key (SRK), while 0x81000002 is recommended for ECC.
//
// In Windows (PCPKsp.dll), handle 0x81000002 is repurposed as an RSA signing
// key, and the ECC Storage Root Key is hardcoded to handle 0x81000009:
// - TpmKey20Ecc::ReadParent / GetEccSrk explicitly probes handle 0x81000009
// via TPM2_ReadPublic.
// - If absent, GetEccSrk creates a NIST P-256 primary key under TPM_RH_OWNER
// (objectAttributes = 0x00030472) and persists it to handle 0x81000009 via
// TPM2_EvictControl.
// - When importing opaque blobs (NCryptImportKey), PCPKsp.dll unconditionally
// passes 0x81000009 (ECC) or 0x81000001 (RSA) as the parentHandle to
// TPM2_Load.
//
// Both handles can be verified on a provisioned machine by executing
// TPM2_ReadPublic(0x81000001) and TPM2_ReadPublic(0x81000009) via TBS, which
// return TPM_ALG_RSA and TPM_ALG_ECC keys with attributes 0x00030472.
enum class WindowsSrkHandle : uint32_t {
kRsa = 0x81000001,
kEcc = 0x81000009,
};
const char kMetricVirtualCreateKeyError[] = "Crypto.TpmError.VirtualCreateKey";
const char kMetricVirtualFinalizeKeyError[] =
"Crypto.TpmError.VirtualFinalizeKey";
const char kMetricVirtualOpenKeyError[] = "Crypto.TpmError.VirtualOpenKey";
const char kMetricVirtualOpenStorageError[] =
"Crypto.TpmError.VirtualOpenStorage";
enum class ProviderType {
// Keys will be backed by a TPM. Requires TPM support.
kTPM,
// Keys will be backed by software. Widely available.
kSoftware
};
// Identifies the purpose of the key to be generated.
enum class KeyUsage {
// The key will be used for signing data (e.g. a session binding key).
kSigning,
// The key will be used as an attestation key (e.g. an AIK).
kAttestation
};
// Holds the results of a successful key generation or loading.
struct KeyDetails {
// The handle to the key.
ScopedNCryptKey key;
// The wrapped key blob that can be used to restore the key later.
std::vector<uint8_t> wrapped_key;
// The SubjectPublicKeyInfo for the public key.
std::vector<uint8_t> spki;
// The algorithm used for the key.
sign::SignatureKind algo = sign::ECDSA_SHA256;
};
// WinKeyImpl shares common implementation for unexportable keys on Windows.
template <typename BaseInterface>
class WinKeyImpl : public BaseInterface {
public:
WinKeyImpl(ProviderType provider_type, KeyDetails details)
: provider_type_(provider_type),
key_(std::move(details.key)),
wrapped_key_(std::move(details.wrapped_key)),
spki_(std::move(details.spki)),
algo_(details.algo) {}
sign::SignatureKind Algorithm() const override { return algo_; }
std::vector<uint8_t> GetSubjectPublicKeyInfo() const override {
return spki_;
}
std::vector<uint8_t> GetWrappedKey() const override { return wrapped_key_; }
bool IsHardwareBacked() const override {
return provider_type_ == ProviderType::kTPM;
}
NCRYPT_KEY_HANDLE GetNCryptKeyHandle() const override { return key_.get(); }
protected:
const ProviderType provider_type_;
ScopedNCryptKey key_;
const std::vector<uint8_t> wrapped_key_;
const std::vector<uint8_t> spki_;
const sign::SignatureKind algo_;
};
LPCWSTR GetWindowsIdentifierForProvider(ProviderType type) {
switch (type) {
case ProviderType::kTPM:
return MS_PLATFORM_CRYPTO_PROVIDER;
case ProviderType::kSoftware:
return MS_KEY_STORAGE_PROVIDER;
}
}
std::u16string KeyIdToWindowsLabel(base::span<const uint8_t> key_id) {
return u"unexportable-key-" + base::UTF8ToUTF16(base::Base64Encode(key_id));
}
template <typename T>
using SecurityStatusOr = base::expected<T, SECURITY_STATUS>;
template <typename T>
SecurityStatusOr<T> GetNCryptProperty(NCRYPT_HANDLE handle, LPCWSTR property) {
T value{};
DWORD cb_value = 0;
SECURITY_STATUS status =
NCryptGetProperty(handle, property, reinterpret_cast<PBYTE>(&value),
sizeof(value), &cb_value, 0);
if (FAILED(status)) {
return base::unexpected(status);
}
CHECK_EQ(cb_value, sizeof(value));
return base::ok(value);
}
template <typename T>
SecurityStatusOr<void> SetNCryptProperty(NCRYPT_HANDLE handle,
LPCWSTR property,
T value) {
SECURITY_STATUS status = NCryptSetProperty(
handle, property, reinterpret_cast<PBYTE>(&value), sizeof(value), 0);
return SUCCEEDED(status) ? SecurityStatusOr<void>()
: base::unexpected(status);
}
// Logs `status` and `selected_algorithm` to an error histogram capturing that
// `operation` failed for a TPM-backed key.
void LogTPMOperationError(TPMOperation operation,
HRESULT status,
std::optional<sign::SignatureKind> selected_algorithm,
bool open_storage_provider_error = false) {
static constexpr char kTPMOperationErrorHistogramFormat[] =
"Crypto.TPMOperation.Win.%s%s.Error";
// There are two cases that can be recorded without a `selected_algorithm`:
// 1- OpenStorageProvider errors because these happen before an algorithm
// is chosen.
// 2- Errors during `kWrappedKeyCreation` TPM operation.
if (!open_storage_provider_error) {
CHECK_EQ(!selected_algorithm.has_value(),
(operation == TPMOperation::kWrappedKeyCreation ||
operation == TPMOperation::kWrappedAttestationKeyCreation));
}
std::string algorithm_string =
selected_algorithm ? AlgorithmToString(*selected_algorithm) : "";
base::UmaHistogramSparse(
base::StringPrintf(kTPMOperationErrorHistogramFormat,
OperationToString(operation).c_str(),
algorithm_string.c_str()),
status);
}
// BCryptAlgorithmFor returns the BCrypt algorithm ID for the given Chromium
// signing algorithm.
std::optional<LPCWSTR> BCryptAlgorithmFor(sign::SignatureKind algo) {
switch (algo) {
case sign::RSA_PKCS1_SHA256:
return BCRYPT_RSA_ALGORITHM;
case sign::ECDSA_SHA256:
return BCRYPT_ECDSA_P256_ALGORITHM;
default:
return std::nullopt;
}
}
// GetSrkHandleFor returns the persistent Storage Root Key (SRK) handle used as
// the parent key when creating TPM 2.0 keys for the given algorithm.
WindowsSrkHandle GetSrkHandleFor(sign::SignatureKind algo) {
switch (algo) {
case sign::RSA_PKCS1_SHA1:
case sign::RSA_PKCS1_SHA256:
case sign::RSA_PKCS1_SHA384:
case sign::RSA_PKCS1_SHA512:
case sign::RSA_PSS_SHA256:
case sign::RSA_PSS_SHA384:
case sign::RSA_PSS_SHA512:
return WindowsSrkHandle::kRsa;
case sign::ECDSA_SHA1:
case sign::ECDSA_SHA256:
case sign::ECDSA_SHA384:
case sign::ECDSA_SHA512:
return WindowsSrkHandle::kEcc;
case sign::ED25519:
case sign::MLDSA_44:
case sign::MLDSA_65:
case sign::MLDSA_87:
NOTREACHED();
}
}
// GetBestSupported returns the first element of |acceptable_algorithms| that
// |provider| supports, or |nullopt| if there isn't any.
std::optional<sign::SignatureKind> GetBestSupported(
NCRYPT_PROV_HANDLE provider,
base::span<const sign::SignatureKind> acceptable_algorithms) {
for (auto algo : acceptable_algorithms) {
std::optional<LPCWSTR> bcrypto_algo_name = BCryptAlgorithmFor(algo);
if (!bcrypto_algo_name) {
continue;
}
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptIsAlgSupported(provider, *bcrypto_algo_name,
/*flags=*/0);
if (FAILED(status)) {
// `NTE_NOT_SUPPORTED` is expected when an algorithm is not supported.
// Avoid recording it as an error as it may unnecessarily clutter the
// metrics.
//
// https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptisalgsupported#return-value
if (status != NTE_NOT_SUPPORTED) {
LogTPMOperationError(TPMOperation::kSelectAlgorithm, status, algo);
}
continue;
}
return algo;
}
return std::nullopt;
}
// GetKeyProperty returns the given NCrypt key property of |key|.
std::optional<std::vector<uint8_t>> GetKeyProperty(NCRYPT_KEY_HANDLE key,
LPCWSTR property) {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
DWORD size;
if (FAILED(NCryptGetProperty(key, property, nullptr, 0, &size, 0))) {
return std::nullopt;
}
std::vector<uint8_t> ret(size);
if (FAILED(
NCryptGetProperty(key, property, ret.data(), ret.size(), &size, 0))) {
return std::nullopt;
}
CHECK_EQ(ret.size(), size);
return ret;
}
// GetKeyStringProperty returns the given NCrypt key property of `key` as a
// string, removing the trailing null character if present.
std::optional<std::wstring> GetKeyStringProperty(NCRYPT_KEY_HANDLE key,
LPCWSTR property) {
return GetKeyProperty(key, property)
.transform([](base::span<const uint8_t> bytes) {
auto chars = base::subtle::reinterpret_span<const wchar_t>(bytes);
std::wstring_view str = {chars.data(), chars.size()};
if (str.ends_with(L'\0')) {
str.remove_suffix(1);
}
return std::wstring(str);
});
}
// Returns true if the key has the NCRYPT_PCP_IDENTITY_KEY flag set in its
// usage policy. This flag indicates that the key is an Attestation Identity
// Key (AIK) restricted by the TPM, meaning it cannot be used to sign arbitrary
// data.
bool IsIdentityKey(NCRYPT_KEY_HANDLE key) {
auto usage_policy =
GetNCryptProperty<DWORD>(key, NCRYPT_PCP_KEY_USAGE_POLICY_PROPERTY);
return usage_policy.has_value() &&
((*usage_policy & NCRYPT_PCP_IDENTITY_KEY) != 0);
}
// ExportKey returns |key| exported in the given format or nullopt on error.
SecurityStatusOr<std::vector<uint8_t>> ExportKey(NCRYPT_KEY_HANDLE key,
LPCWSTR format) {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
DWORD output_size;
SECURITY_STATUS status =
NCryptExportKey(key, 0, format, nullptr, nullptr, 0, &output_size, 0);
if (FAILED(status)) {
return base::unexpected(status);
}
std::vector<uint8_t> output(output_size);
status = NCryptExportKey(key, 0, format, nullptr, output.data(),
output.size(), &output_size, 0);
if (FAILED(status)) {
return base::unexpected(status);
}
CHECK_EQ(output.size(), output_size);
return output;
}
std::optional<std::vector<uint8_t>> GetP256ECDSASPKI(NCRYPT_KEY_HANDLE key) {
ASSIGN_OR_RETURN(const std::vector<uint8_t> pub_key,
ExportKey(key, BCRYPT_ECCPUBLIC_BLOB),
[](auto) { return std::nullopt; });
// The exported key is a `BCRYPT_ECCKEY_BLOB` followed by the bytes of the
// public key itself.
// https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_ecckey_blob
base::SpanReader reader(base::span{pub_key});
ASSIGN_OR_RETURN(const auto header,
reader.ReadNativeEndian<BCRYPT_ECCKEY_BLOB>());
base::span key_bytes = reader.remaining_span();
// |cbKey| is documented[1] as "the length, in bytes, of the key". It is
// not. For ECDSA public keys it is the length of a field element.
if ((header.dwMagic != BCRYPT_ECDSA_PUBLIC_P256_MAGIC &&
header.dwMagic != BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC) ||
header.cbKey != 256 / 8 || key_bytes.size() != 64) {
return std::nullopt;
}
// Sometimes NCrypt will return a generic dwMagic even when asked for a P-256
// key. In that case, do extra validation to make sure that `key` is in fact
// a P-256 key.
if (header.dwMagic == BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC) {
if (GetKeyStringProperty(key, NCRYPT_ECC_CURVE_NAME_PROPERTY) !=
BCRYPT_ECC_CURVE_NISTP256) {
return std::nullopt;
}
}
std::array<uint8_t, 1 + 32 + 32> x962 = {POINT_CONVERSION_UNCOMPRESSED};
base::span(x962).last<64>().copy_from(key_bytes);
return keypair::PublicKey::FromEcP256Point(x962).transform(
[](const auto& key) { return key.ToSubjectPublicKeyInfo(); });
}
std::optional<std::vector<uint8_t>> GetRSASPKI(NCRYPT_KEY_HANDLE key) {
ASSIGN_OR_RETURN(const std::vector<uint8_t> pub_key,
ExportKey(key, BCRYPT_RSAPUBLIC_BLOB),
[](auto) { return std::nullopt; });
// The exported key is a `BCRYPT_RSAKEY_BLOB` followed by the bytes of the
// key itself.
// https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_rsakey_blob
base::SpanReader reader(base::span{pub_key});
ASSIGN_OR_RETURN(const auto header,
reader.ReadNativeEndian<BCRYPT_RSAKEY_BLOB>());
base::span key_bytes = reader.remaining_span();
if (header.Magic != static_cast<ULONG>(BCRYPT_RSAPUBLIC_MAGIC)) {
return std::nullopt;
}
if (key_bytes.size() <
base::ClampedNumeric<size_t>(header.cbPublicExp) + header.cbModulus) {
return std::nullopt;
}
auto [e_bytes, rest_bytes] = key_bytes.split_at(header.cbPublicExp);
auto n_bytes = rest_bytes.first(header.cbModulus);
return keypair::PublicKey::FromRsaPublicKeyComponents(n_bytes, e_bytes)
.transform([](const auto& key) { return key.ToSubjectPublicKeyInfo(); });
}
SecurityStatusOr<std::vector<uint8_t>> SignECDSA(
NCRYPT_KEY_HANDLE key,
base::span<const uint8_t> data) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
std::array<uint8_t, hash::kSha256Size> digest = hash::Sha256(data);
// The signature is written as a pair of big-endian field elements for P-256
// ECDSA.
std::vector<uint8_t> sig(64);
DWORD sig_size;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status =
NCryptSignHash(key, nullptr, digest.data(), digest.size(), sig.data(),
sig.size(), &sig_size, NCRYPT_SILENT_FLAG);
if (FAILED(status)) {
return base::unexpected(status);
}
}
CHECK_EQ(sig.size(), sig_size);
auto [r_bytes, s_bytes] = base::span(sig).split_at<32>();
return base::OptionalToExpected(
ConvertEcdsaRawComponentsToDer(r_bytes, s_bytes), NTE_FAIL);
}
SecurityStatusOr<std::vector<uint8_t>> SignRSA(NCRYPT_KEY_HANDLE key,
base::span<const uint8_t> data) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
std::array<uint8_t, hash::kSha256Size> digest = hash::Sha256(data);
BCRYPT_PKCS1_PADDING_INFO padding_info = {0};
padding_info.pszAlgId = NCRYPT_SHA256_ALGORITHM;
DWORD sig_size;
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status =
NCryptSignHash(key, &padding_info, digest.data(), digest.size(), nullptr,
0, &sig_size, NCRYPT_SILENT_FLAG | BCRYPT_PAD_PKCS1);
if (FAILED(status)) {
return base::unexpected(status);
}
std::vector<uint8_t> sig(sig_size);
status = NCryptSignHash(key, &padding_info, digest.data(), digest.size(),
sig.data(), sig.size(), &sig_size,
NCRYPT_SILENT_FLAG | BCRYPT_PAD_PKCS1);
if (FAILED(status)) {
return base::unexpected(status);
}
CHECK_EQ(sig.size(), sig_size);
return sig;
}
ScopedNCryptKey LoadWrappedKey(base::span<const uint8_t> wrapped,
ProviderType provider_type,
KeyUsage usage) {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
ScopedNCryptProvider provider;
SECURITY_STATUS status =
NCryptOpenStorageProvider(ScopedNCryptProvider::Receiver(provider).get(),
GetWindowsIdentifierForProvider(provider_type),
/*flags=*/0);
TPMOperation operation = usage == KeyUsage::kAttestation
? TPMOperation::kWrappedAttestationKeyCreation
: TPMOperation::kWrappedKeyCreation;
if (FAILED(status)) {
LogTPMOperationError(operation, status, std::nullopt,
/*open_storage_provider_error=*/true);
return ScopedNCryptKey();
}
ScopedNCryptKey key;
SECURITY_STATUS import_status = -1;
if (provider_type == ProviderType::kSoftware) {
// Software keys are labelled with a random identifier. Attempt to obtain a
// handle from the identifier.
std::u16string key_label = KeyIdToWindowsLabel(wrapped);
import_status =
NCryptOpenKey(provider.get(), ScopedNCryptKey::Receiver(key).get(),
base::as_wcstr(key_label),
/*dwLegacyKeySpec=*/0, /*dwFlags=*/0);
} else {
// TPM keys use an undocumented Windows feature to export a wrapped key.
// Attempt to obtain a handle from the wrapped key.
import_status = NCryptImportKey(
provider.get(), /*hImportKey=*/NULL, BCRYPT_OPAQUE_KEY_BLOB,
/*pParameterList=*/nullptr, ScopedNCryptKey::Receiver(key).get(),
const_cast<PBYTE>(wrapped.data()), wrapped.size(),
/*dwFlags=*/NCRYPT_SILENT_FLAG);
}
if (FAILED(import_status)) {
LogTPMOperationError(operation, import_status, std::nullopt);
return ScopedNCryptKey();
}
return key;
}
// Builds a Windows Platform Crypto Provider (PCP) opaque key blob
// (BCRYPT_OPAQUE_KEY_BLOB) from the TPM2_Create response. This function is
// strictly TPM 2.0 only, formatting the TPM2_Create outputs into a
// PCP_KEY_BLOB_WIN8 structure with pcpType = 2 (PCPTYPE_TPM20).
//
// The binary layout corresponds to the PCP_KEY_BLOB_WIN8 structure used by the
// Microsoft Platform Crypto Provider for TPM 2.0 keys. See:
// https://github.com/microsoft/TSS.MSR/tree/main/PCPTool.v11
std::vector<uint8_t> BuildWrappedAttestationKey(
const tpm::CreateResponse& response) {
// Layout for BCRYPT_OPAQUE_KEY_BLOB under Windows 8+ for TPM 2.0 keys.
// See
// https://raw.githack.com/microsoft/TSS.MSR/master/PCPTool.v11/Using%20the%20Windows%208%20Platform%20Crypto%20Provider%20and%20Associated%20TPM%20Functionality.pdf#page=25
struct PCP_KEY_BLOB_WIN8 {
DWORD magic = 0x4D504350; // 'MPCP'
DWORD cbHeader = sizeof(PCP_KEY_BLOB_WIN8);
DWORD pcpType = 2; // PCP_TYPE_TPM20
DWORD flags = 0;
ULONG cbPublic = 0;
ULONG cbPrivate = 0;
ULONG cbMigrationPublic = 0;
ULONG cbMigrationPrivate = 0;
ULONG cbPolicyDigestList = 0;
ULONG cbPCRBinding = 0;
ULONG cbPCRDigest = 0;
ULONG cbEncryptedSecret = 0;
ULONG cbTpm12HostageBlob = 0;
};
const PCP_KEY_BLOB_WIN8 header{
.flags = NCRYPT_PCP_IDENTITY_KEY,
.cbPublic = base::checked_cast<ULONG>(response.out_public.size()),
.cbPrivate = base::checked_cast<ULONG>(response.out_private.size()),
};
std::vector<uint8_t> wrapped_key(header.cbHeader + header.cbPublic +
header.cbPrivate);
base::SpanWriter<uint8_t> writer(wrapped_key);
writer.Write(base::byte_span_from_ref(header));
writer.Write(response.out_public);
writer.Write(response.out_private);
CHECK_EQ(writer.remaining(), 0u);
return wrapped_key;
}
// ECDSASigningKey wraps a P-256 ECDSA key stored in the given provider.
class ECDSASigningKey : public WinKeyImpl<UnexportableSigningKey> {
public:
ECDSASigningKey(ProviderType provider_type, KeyDetails details)
: WinKeyImpl(provider_type, std::move(details)) {}
std::optional<std::vector<uint8_t>> SignSlowly(
base::span<const uint8_t> data) override {
return base::OptionalFromExpected(
SignECDSA(key_.get(), data)
.transform_error([&](SECURITY_STATUS status) {
LogTPMOperationError(TPMOperation::kMessageSigning, status,
Algorithm());
return status;
}));
}
bool SupportsTls13() override { return true; }
};
// RSASigningKey wraps a RSA key stored in the given provider.
class RSASigningKey : public WinKeyImpl<UnexportableSigningKey> {
public:
RSASigningKey(ProviderType provider_type, KeyDetails details)
: WinKeyImpl(provider_type, std::move(details)) {}
std::optional<std::vector<uint8_t>> SignSlowly(
base::span<const uint8_t> data) override {
return base::OptionalFromExpected(
SignRSA(key_.get(), data).transform_error([&](SECURITY_STATUS status) {
LogTPMOperationError(TPMOperation::kMessageSigning, status,
Algorithm());
return status;
}));
}
bool SupportsTls13() override {
if (!is_compatible_with_tls13.has_value()) {
is_compatible_with_tls13 = CanSignPssWithExpectedSaltLength();
}
return is_compatible_with_tls13.value();
}
private:
bool CanSignPssWithExpectedSaltLength() {
// TLS 1.3 requires support of RSA-PSS algorithm with Salt Length == Hash
// Length (32 bytes for SHA-256).
BCRYPT_PSS_PADDING_INFO padding_info = {0};
padding_info.pszAlgId = BCRYPT_SHA256_ALGORITHM;
padding_info.cbSalt = 32;
constexpr auto dummy_data = std::to_array<uint8_t>({
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
});
auto dummy_hash = hash::Sha256(dummy_data);
DWORD cb_signature = 0;
if (FAILED(NCryptSignHash(key_.get(), &padding_info, dummy_hash.data(),
dummy_hash.size(), nullptr, 0, &cb_signature,
NCRYPT_SILENT_FLAG | NCRYPT_PAD_PSS_FLAG))) {
return false;
}
std::vector<uint8_t> signature(cb_signature);
if (FAILED(NCryptSignHash(key_.get(), &padding_info, dummy_hash.data(),
dummy_hash.size(), signature.data(),
signature.size(), &cb_signature,
NCRYPT_SILENT_FLAG | NCRYPT_PAD_PSS_FLAG))) {
return false;
}
auto public_key = keypair::PublicKey::FromSubjectPublicKeyInfo(spki_);
if (!public_key) {
return false;
}
return sign::Verify(sign::SignatureKind::RSA_PSS_SHA256, public_key.value(),
dummy_data, signature);
}
std::optional<bool> is_compatible_with_tls13;
};
// Dynamically loading tbs.dll prevents the browser from crashing on startup
// if the Windows TPM Base Services are missing or disabled.
bool IsTbsAvailable() {
static const bool is_available = [] {
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
// Resolve all delay-loaded imports for tbs.dll on the first call to
// prevent failed loads being treated as a fatal failure later, which
// can happen in rare cases due to missing or corrupted DLL file.
base::expected<bool, HRESULT> load_result =
base::win::LoadAllImportsForDllUnchecked("tbs.dll");
bool available = load_result.value_or(false);
base::UmaHistogramSparse(
"Crypto.TPMOperation.Win.LoadTBSLibrary.Result",
available
? S_OK
: load_result.error_or(HRESULT_FROM_WIN32(ERROR_MOD_NOT_FOUND)));
return available;
}();
return is_available;
}
bool IsTpm20Available() {
if (!IsTbsAvailable()) {
return false;
}
TPM_DEVICE_INFO tpm_info{};
TBS_RESULT result = ::Tbsi_GetDeviceInfo(sizeof(tpm_info), &tpm_info);
return result == TBS_SUCCESS && tpm_info.tpmVersion >= TPM_VERSION_20;
}
// Maps a TPM operation (represented by a TPM command) to a TPMOperation enum.
//
// NOTE: Right now only restricted signing keys directly issue commands to the
// TPM. Regular signing keys are completely supported by the higher level NCrypt
// library. The mapping here needs to be changed should this cease to be the
// case.
std::optional<TPMOperation> TpmCommandToOperation(tpm::TpmCommand command) {
switch (command) {
case tpm::TpmCommand::kCreate:
return TPMOperation::kNewAttestationKeyCreation;
case tpm::TpmCommand::kSign:
return TPMOperation::kRestrictedMessageSigning;
case tpm::TpmCommand::kHash:
case tpm::TpmCommand::kHashSequenceStart:
case tpm::TpmCommand::kSequenceComplete:
case tpm::TpmCommand::kSequenceUpdate:
return TPMOperation::kMessageHashing;
case tpm::TpmCommand::kFlushContext:
return std::nullopt;
}
NOTREACHED();
}
void LogTpmExtractPropertyResult(tpm::TpmCommand command,
SECURITY_STATUS status,
sign::SignatureKind algorithm) {
base::UmaHistogramSparse(
absl::StrFormat("Crypto.TPMOperation.Win.Tpm%vExtractProperty.Result",
command),
status);
if (auto op = TpmCommandToOperation(command)) {
LogTPMOperationError(*op, status, algorithm);
}
}
std::optional<TBS_HCONTEXT> GetTbsContext(NCRYPT_KEY_HANDLE key_handle,
tpm::TpmCommand command,
sign::SignatureKind algorithm) {
auto log_extract_property_error = [&](SECURITY_STATUS status) {
LogTpmExtractPropertyResult(command, status, algorithm);
return std::nullopt;
};
ASSIGN_OR_RETURN(NCRYPT_PROV_HANDLE prov_handle,
GetNCryptProperty<NCRYPT_PROV_HANDLE>(
key_handle, NCRYPT_PROVIDER_HANDLE_PROPERTY),
log_extract_property_error);
ASSIGN_OR_RETURN(TBS_HCONTEXT h_context,
GetNCryptProperty<TBS_HCONTEXT>(
prov_handle, NCRYPT_PCP_PLATFORMHANDLE_PROPERTY),
log_extract_property_error);
return h_context;
}
std::optional<uint32_t> GetTpmPlatformHandle(NCRYPT_KEY_HANDLE key_handle,
tpm::TpmCommand command,
sign::SignatureKind algorithm) {
return base::OptionalFromExpected(
GetNCryptProperty<uint32_t>(key_handle,
NCRYPT_PCP_PLATFORMHANDLE_PROPERTY)
.transform_error([&](SECURITY_STATUS status) {
LogTpmExtractPropertyResult(command, status, algorithm);
return status;
}));
}
std::optional<std::vector<uint8_t>> SubmitTbsCommand(
TBS_HCONTEXT h_context,
tpm::TpmCommand command,
base::span<const uint8_t> cmd,
size_t max_resp_size,
sign::SignatureKind algorithm) {
// A max_resp_size buffer handles the maximum expected TPM response.
// Heap-allocating it protects the local stack from potential buffer
// overflow vulnerabilities in the OS API.
std::vector<uint8_t> resp(max_resp_size);
UINT32 resp_len = resp.size();
TBS_RESULT tbs_result = ::Tbsip_Submit_Command(
h_context, TBS_COMMAND_LOCALITY_ZERO, TBS_COMMAND_PRIORITY_NORMAL,
cmd.data(), cmd.size(), resp.data(), &resp_len);
// Overwriting tbs_result safely catches buggy API returns that indicate
// more bytes were written than the buffer size, preventing false "Success"
// codes from polluting UMA metrics.
if (tbs_result == TBS_SUCCESS && resp_len > resp.size()) {
tbs_result = TBS_E_INSUFFICIENT_BUFFER;
}
if (tbs_result != TBS_SUCCESS) {
base::UmaHistogramSparse("Crypto.TPMOperation.Win.TbsSubmitCommand.Error",
tbs_result);
ASSIGN_OR_RETURN(TPMOperation op, TpmCommandToOperation(command));
LogTPMOperationError(op, tbs_result, algorithm);
return std::nullopt;
}
resp.resize(resp_len);
return resp;
}
template <typename T>
std::optional<T> ToOptionalAndRecordParseMetrics(
tpm::TpmParseErrorOr<T> parsed_or_error) {
auto parse_error = parsed_or_error.error_or(
tpm::TpmParseError(tpm::kNoTpmParseErrorForMetrics));
base::UmaHistogramEnumeration(
absl::StrFormat("Crypto.TPMOperation.Win.Tpm%vParse.Result", T::kCommand),
parse_error.type);
base::UmaHistogramSparse(
absl::StrFormat("Crypto.TPMOperation.Win.Tpm%vResponse.TpmResponseCode",
T::kCommand),
parse_error.tpm_error_code.value_or(0));
return base::OptionalFromExpected(std::move(parsed_or_error));
}
// Maximum buffer size for a TPM2B_MAX_BUFFER structure (e.g. TPM2_Hash data
// payload).
constexpr size_t kMaxTpmHashBufferSize = 1024;
// Maximum expected response buffer size for TPM commands (e.g. TPM2_Sign).
constexpr size_t kMaxTpmResponseSize = 4096;
// Holds the digest and validation ticket produced by hashing data with the TPM.
struct HashResult {
std::vector<uint8_t> digest;
std::vector<uint8_t> validation_ticket;
};
// Extracts the hash algorithm (`crypto::hash::HashKind`) from a
// `sign::SignatureKind`.
constexpr std::optional<hash::HashKind> ToHashKind(
sign::SignatureKind algorithm) {
switch (algorithm) {
case sign::RSA_PKCS1_SHA1:
case sign::ECDSA_SHA1:
return hash::kSha1;
case sign::RSA_PKCS1_SHA256:
case sign::ECDSA_SHA256:
case sign::RSA_PSS_SHA256:
return hash::kSha256;
case sign::RSA_PKCS1_SHA384:
case sign::ECDSA_SHA384:
case sign::RSA_PSS_SHA384:
return hash::kSha384;
case sign::RSA_PKCS1_SHA512:
case sign::ECDSA_SHA512:
case sign::RSA_PSS_SHA512:
return hash::kSha512;
case sign::ED25519:
case sign::MLDSA_44:
case sign::MLDSA_65:
case sign::MLDSA_87:
return std::nullopt;
}
}
// Hashes data using either single-shot TPM2_Hash (if data <= 1024 bytes) or
// streaming TPM sequence commands (TPM2_HashSequenceStart, TPM2_SequenceUpdate,
// TPM2_SequenceComplete) for larger buffers.
std::optional<HashResult> HashDataSlowly(TBS_HCONTEXT h_context,
base::span<const uint8_t> data,
sign::SignatureKind algorithm) {
ASSIGN_OR_RETURN(const hash::HashKind hash_kind, ToHashKind(algorithm));
if (data.size() <= kMaxTpmHashBufferSize) {
std::vector<uint8_t> hash_cmd = tpm::BuildHashCommand(data, hash_kind);
ASSIGN_OR_RETURN(
std::vector<uint8_t> hash_resp,
SubmitTbsCommand(h_context, tpm::TpmCommand::kHash, hash_cmd,
kMaxTpmResponseSize, algorithm));
ASSIGN_OR_RETURN(
tpm::HashResponse hash_parsed,
ToOptionalAndRecordParseMetrics(tpm::ParseHashResponse(hash_resp)));
return HashResult{
.digest = std::move(hash_parsed.digest),
.validation_ticket = std::move(hash_parsed.validation_ticket),
};
}
// Multi-part hashing sequence for payloads larger than 1024 bytes.
// 1. TPM2_HashSequenceStart
std::vector<uint8_t> start_cmd =
tpm::BuildHashSequenceStartCommand(hash_kind);
ASSIGN_OR_RETURN(
std::vector<uint8_t> start_resp,
SubmitTbsCommand(h_context, tpm::TpmCommand::kHashSequenceStart,
start_cmd, kMaxTpmResponseSize, algorithm));
ASSIGN_OR_RETURN(tpm::HashSequenceStartResponse start_parsed,
ToOptionalAndRecordParseMetrics(
tpm::ParseHashSequenceStartResponse(start_resp)));
uint32_t sequence_handle = start_parsed.sequence_handle;
// TPM2_HashSequenceStart allocates a transient sequence handle in the TPM's
// volatile memory. If an error occurs before the sequence completes, we
// must flush the context via TPM2_FlushContext to prevent leaking limited
// TPM RAM resources (which would eventually cause TPM_RC_MEMORY /
// TPM_RC_HANDLES). On success, TPM2_SequenceComplete automatically frees the
// handle, so we cancel the cleanup guard.
absl::Cleanup flush_guard = [h_context, sequence_handle, algorithm] {
if (auto resp =
SubmitTbsCommand(h_context, tpm::TpmCommand::kFlushContext,
tpm::BuildFlushContextCommand(sequence_handle),
kMaxTpmResponseSize, algorithm)) {
ToOptionalAndRecordParseMetrics(tpm::ParseFlushContextResponse(*resp));
}
};
// 2. Loop TPM2_SequenceUpdate for all chunks while remaining > 1024 bytes.
base::span<const uint8_t> remaining = data;
while (remaining.size() > kMaxTpmHashBufferSize) {
auto chunk = remaining.take_first<kMaxTpmHashBufferSize>();
std::vector<uint8_t> update_cmd =
tpm::BuildSequenceUpdateCommand(sequence_handle, chunk);
ASSIGN_OR_RETURN(
std::vector<uint8_t> update_resp,
SubmitTbsCommand(h_context, tpm::TpmCommand::kSequenceUpdate,
update_cmd, kMaxTpmResponseSize, algorithm));
RETURN_IF_ERROR(ToOptionalAndRecordParseMetrics(
tpm::ParseSequenceUpdateResponse(update_resp)));
}
// 3. TPM2_SequenceComplete with remaining data (<= 1024 bytes).
std::vector<uint8_t> complete_cmd =
tpm::BuildSequenceCompleteCommand(sequence_handle, remaining);
ASSIGN_OR_RETURN(
std::vector<uint8_t> complete_resp,
SubmitTbsCommand(h_context, tpm::TpmCommand::kSequenceComplete,
complete_cmd, kMaxTpmResponseSize, algorithm));
ASSIGN_OR_RETURN(tpm::SequenceCompleteResponse complete_parsed,
ToOptionalAndRecordParseMetrics(
tpm::ParseSequenceCompleteResponse(complete_resp)));
std::move(flush_guard).Cancel();
return HashResult{
.digest = std::move(complete_parsed.digest),
.validation_ticket = std::move(complete_parsed.validation_ticket),
};
}
// Small helper to write a TPM2B sized buffer. Consisting of a uint16_t size and
// payload.
void WriteTpm2b(base::SpanWriter<uint8_t>& writer,
base::span<const uint8_t> data) {
CHECK(writer.WriteU16BigEndian(base::checked_cast<uint16_t>(data.size())));
CHECK(writer.Write(data));
}
// Converts raw signature bytes into a serialized TPMT_SIGNATURE binary
// structure. This is needed, because
// NCRYPT_PCP_TPM_WEB_AUTHN_ATTESTATION_STATEMENT version 1 returns the TPM
// signature in raw format, rather than a serialized `TPMT_SIGNATURE`. This is
// fixed in version 2, but requires Windows 11, version 23H2.
std::optional<std::vector<uint8_t>> ConvertRawToTpmtSignature(
sign::SignatureKind alg,
base::span<const uint8_t> raw_sig) {
switch (alg) {
case sign::ECDSA_SHA256: {
static constexpr size_t kPrimeSize = 32;
if (raw_sig.size() != kPrimeSize * 2) {
return std::nullopt;
}
auto sig_span = base::span<const uint8_t, kPrimeSize * 2>(raw_sig);
auto [r_bytes, s_bytes] = sig_span.split_at<kPrimeSize>();
constexpr size_t kEcdsaTpmSigSize = 2 + 2 + 2 * (2 + kPrimeSize);
std::vector<uint8_t> signature(kEcdsaTpmSigSize);
base::SpanWriter<uint8_t> sig_writer(signature);
sig_writer.WriteEnumBigEndian(tpm::TPM_ALG_ECDSA);
sig_writer.WriteEnumBigEndian(tpm::TPM_ALG_SHA256);
WriteTpm2b(sig_writer, r_bytes);
WriteTpm2b(sig_writer, s_bytes);
CHECK_EQ(sig_writer.remaining(), 0u);
return signature;
}
case sign::RSA_PKCS1_SHA256: {
constexpr size_t kRsa2048SigSize = 256;
if (raw_sig.size() != kRsa2048SigSize) {
return std::nullopt;
}
constexpr size_t kRsaTpmSigSize = 2 + 2 + 2 + kRsa2048SigSize;
std::vector<uint8_t> signature(kRsaTpmSigSize);
base::SpanWriter<uint8_t> sig_writer(signature);
sig_writer.WriteEnumBigEndian(tpm::TPM_ALG_RSASSA);
sig_writer.WriteEnumBigEndian(tpm::TPM_ALG_SHA256);
WriteTpm2b(sig_writer, raw_sig);
CHECK_EQ(sig_writer.remaining(), 0u);
return signature;
}
default:
return std::nullopt;
}
}
// Parses an NCRYPT_PCP_TPM_WEB_AUTHN_ATTESTATION_STATEMENT claim blob.
std::optional<AttestationStatement> ParseWebAuthnAttestationStatement(
sign::SignatureKind alg,
base::span<const uint8_t> claim_blob) {
// Magic value for NCRYPT_PCP_TPM_WEB_AUTHN_ATTESTATION_STATEMENT ('KAWA').
static constexpr uint32_t kPcpTpmWebAuthnAttestationMagic = 0x4B415741;
using Header = NCRYPT_PCP_TPM_WEB_AUTHN_ATTESTATION_STATEMENT;
base::SpanReader reader(claim_blob);
ASSIGN_OR_RETURN(const auto header, reader.ReadNativeEndian<Header>());
if (header.Magic != kPcpTpmWebAuthnAttestationMagic || header.Version != 1 ||
header.HeaderSize != sizeof(Header)) {
return std::nullopt;
}
ASSIGN_OR_RETURN(base::span certify_info, reader.Read(header.cbCertifyInfo));
ASSIGN_OR_RETURN(
std::vector tpmt_signature,
reader.Read(header.cbSignature)
.and_then(std::bind_front(ConvertRawToTpmtSignature, alg)));
ASSIGN_OR_RETURN(base::span tpm_public, reader.Read(header.cbTpmPublic));
return AttestationStatement{
.format = AttestationStatement::kTpm,
.statement = base::ToVector(certify_info),
.signature = std::move(tpmt_signature),
.subject_key = base::ToVector(tpm_public),
};
}
// AttestationKeyWin wraps an Attestation Identity Key (AIK) on Windows.
// While signing still communicates with the TPM directly via TBS (due to the
// restricted key policy preventing arbitrary message signing through NCrypt),
// key certification is performed via NCryptCreateClaim.
class AttestationKeyWin : public WinKeyImpl<UnexportableAttestationKey> {
public:
AttestationKeyWin(ProviderType provider_type, KeyDetails details)
: WinKeyImpl(provider_type, std::move(details)) {}
// UnexportableSigningKey:
std::optional<std::vector<uint8_t>> SignSlowly(
base::span<const uint8_t> data) override {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
// 1. Check TBS availability
if (!IsTbsAvailable()) {
return std::nullopt;
}
// 2. Extract Provider Context and TPM handles
ASSIGN_OR_RETURN(TBS_HCONTEXT h_context,
GetTbsContext(GetNCryptKeyHandle(), tpm::TpmCommand::kSign,
Algorithm()));
ASSIGN_OR_RETURN(uint32_t sign_handle,
GetTpmPlatformHandle(GetNCryptKeyHandle(),
tpm::TpmCommand::kSign, Algorithm()));
// 3. Hash Data (single-shot or streaming sequence)
ASSIGN_OR_RETURN(HashResult hash_result,
HashDataSlowly(h_context, data, Algorithm()));
// 4. Submit TPM2_Sign Command
// Attestation Identity Keys (AIKs) are restricted signing keys whose
// signature scheme is fixed in the key's public template upon creation.
// Per TPM 2.0 Part 3 Section 19.2 (TPM2_Sign), `inScheme` is set to
// `TPM_ALG_NULL` for restricted keys so the TPM uses the scheme defined in
// the key object itself. Setting `inScheme` to `TPM_ALG_NULL` specifies no
// scheme-specific parameters, meaning `hash_alg` is ignored in the
// serialized `TPMT_SIG_SCHEME`.
std::vector<uint8_t> sign_cmd = tpm::BuildSignCommand(
sign_handle, hash_result.digest, hash_result.validation_ticket);
ASSIGN_OR_RETURN(
std::vector<uint8_t> sign_resp,
SubmitTbsCommand(h_context, tpm::TpmCommand::kSign, sign_cmd,
kMaxTpmResponseSize, Algorithm()));
// 5. Parse TPM2_Sign Response
ASSIGN_OR_RETURN(
tpm::SignResponse sign_parsed,
ToOptionalAndRecordParseMetrics(tpm::ParseSignResponse(sign_resp)));
// 6. Normalize signature format (DER for ECDSA, raw for RSA)
return tpm::ParseTpmSignature(sign_parsed.signature);
}
bool SupportsTls13() override {
// TODO(crbug.com/530828835): Implement.
NOTIMPLEMENTED();
return false;
}
// UnexportableAttestationKey:
std::optional<AttestationStatement> CertifySlowly(
const UnexportableSigningKey& signing_key,
base::span<const uint8_t> challenge) override {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
const auto qualifying_data =
hash::Hash(CHECK_DEREF(ToHashKind(Algorithm())), challenge);
NCryptBuffer nonce_buffer{
.cbBuffer = static_cast<ULONG>(qualifying_data.size()),
.BufferType = NCRYPTBUFFER_CLAIM_KEYATTESTATION_NONCE,
.pvBuffer = const_cast<uint8_t*>(qualifying_data.data()),
};
NCryptBufferDesc parameter_list{
.ulVersion = BCRYPTBUFFER_VERSION,
.cBuffers = 1,
.pBuffers = &nonce_buffer,
};
// Pre-allocate a 1024-byte buffer which is sufficient for ECDSA P-256
// (~330 bytes) and RSA 2048 (~730 bytes) attestation statements. This
// avoids an extra TPM transaction for size querying.
std::vector<uint8_t> claim_blob(1024);
DWORD bytes_written = 0;
SECURITY_STATUS status = NCryptCreateClaim(
signing_key.GetNCryptKeyHandle(), GetNCryptKeyHandle(),
NCRYPT_CLAIM_WEB_AUTH_SUBJECT_ONLY, &parameter_list, claim_blob.data(),
static_cast<DWORD>(claim_blob.size()), &bytes_written, /*dwFlags=*/0);
if (FAILED(status)) {
LogTPMOperationError(TPMOperation::kKeyCertification, status,
Algorithm());
return std::nullopt;
}
claim_blob.resize(bytes_written);
return ParseWebAuthnAttestationStatement(Algorithm(), claim_blob);
}
};
// UnexportableKeyProviderWin uses NCrypt and the Platform Crypto
// Provider to expose TPM-backed keys on Windows.
class UnexportableKeyProviderWin : public UnexportableKeyProvider {
public:
explicit UnexportableKeyProviderWin(ProviderType provider_type)
: provider_type_(provider_type) {}
~UnexportableKeyProviderWin() override = default;
std::optional<sign::SignatureKind> SelectAlgorithm(
base::span<const sign::SignatureKind> acceptable_algorithms) override {
ScopedNCryptProvider provider;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
GetWindowsIdentifierForProvider(provider_type_), /*flags=*/0);
if (FAILED(status)) {
LogTPMOperationError(TPMOperation::kSelectAlgorithm, status,
std::nullopt,
/*open_storage_provider_error=*/true);
return std::nullopt;
}
}
return GetBestSupported(provider.get(), acceptable_algorithms);
}
std::unique_ptr<UnexportableSigningKey> GenerateSigningKeySlowly(
base::span<const sign::SignatureKind> acceptable_algorithms) override {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
ScopedNCryptProvider provider;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
GetWindowsIdentifierForProvider(provider_type_), /*flags=*/0);
if (FAILED(status)) {
LogTPMOperationError(TPMOperation::kNewKeyCreation, status,
std::nullopt,
/*open_storage_provider_error=*/true);
return nullptr;
}
}
ASSIGN_OR_RETURN(sign::SignatureKind algo,
GetBestSupported(provider.get(), acceptable_algorithms),
[] { return nullptr; });
std::vector<uint8_t> key_id;
ScopedNCryptKey key;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS creation_status;
if (provider_type_ == ProviderType::kSoftware) {
// Windows support for wrapped keys is undocumented, and doesn't seem to
// work for the software backend. The API wants Chrome to provide a
// label for the key, so we assign one randomly.
key_id = crypto::RandBytesAsVector(16);
std::u16string key_label = KeyIdToWindowsLabel(key_id);
creation_status = NCryptCreatePersistedKey(
provider.get(), ScopedNCryptKey::Receiver(key).get(),
BCryptAlgorithmFor(algo).value(), base::as_wcstr(key_label),
/*dwLegacyKeySpec=*/0, /*dwFlags=*/0);
} else {
// An empty key name stops the key being persisted to disk.
// TODO(crbug.com/398125799): assign labels to these keys instead.
creation_status = NCryptCreatePersistedKey(
provider.get(), ScopedNCryptKey::Receiver(key).get(),
BCryptAlgorithmFor(algo).value(),
/*pszKeyName=*/nullptr,
/*dwLegacyKeySpec=*/0, /*dwFlags=*/0);
}
if (FAILED(creation_status)) {
LogTPMOperationError(TPMOperation::kNewKeyCreation, creation_status,
algo);
return nullptr;
}
if (provider_type_ == ProviderType::kTPM &&
algo == sign::RSA_PKCS1_SHA256) {
// TPM 2.0 RSA keys created via the Platform Crypto Provider default to
// SHA-1 for signing if left unset. Restrict the key to SHA-256 instead.
RETURN_IF_ERROR(
SetNCryptProperty(key.get(),
NCRYPT_PCP_RSA_SCHEME_HASH_ALG_PROPERTY,
static_cast<DWORD>(tpm::TPM_ALG_SHA256)),
[&](SECURITY_STATUS status) {
LogTPMOperationError(TPMOperation::kNewKeyCreation, status, algo);
return nullptr;
});
}
if (FAILED(NCryptFinalizeKey(key.get(), NCRYPT_SILENT_FLAG))) {
return nullptr;
}
}
if (provider_type_ == ProviderType::kTPM) {
ASSIGN_OR_RETURN(key_id, ExportKey(key.get(), BCRYPT_OPAQUE_KEY_BLOB),
[&](SECURITY_STATUS status) {
LogTPMOperationError(TPMOperation::kWrappedKeyExport,
status, algo);
return nullptr;
});
}
ASSIGN_OR_RETURN(
std::vector<uint8_t> spki,
[&]() -> std::optional<std::vector<uint8_t>> {
switch (algo) {
case sign::ECDSA_SHA256:
return GetP256ECDSASPKI(key.get());
case sign::RSA_PKCS1_SHA256:
return GetRSASPKI(key.get());
default:
return std::nullopt;
}
}(),
[] { return nullptr; });
KeyDetails key_details{std::move(key), std::move(key_id), std::move(spki),
algo};
switch (algo) {
case sign::ECDSA_SHA256:
return std::make_unique<ECDSASigningKey>(provider_type_,
std::move(key_details));
case sign::RSA_PKCS1_SHA256:
return std::make_unique<RSASigningKey>(provider_type_,
std::move(key_details));
default:
return nullptr;
}
}
// Generates a TPM 2.0 Attestation Identity Key (AIK) by submitting a raw
// TPM2_Create command via TBS and importing the resulting opaque key blob
// into the Windows Platform Crypto Provider (PCP).
//
// Windows CNG does not support creating AIKs with modern parameters (e.g.,
// ECDSA P-256 with SHA-256) directly through NCryptCreatePersistedKey.
// Instead, we construct and issue TPM2_Create directly under the Storage Root
// Key (SRK), format the TPM2B_PUBLIC and TPM2B_PRIVATE into a
// BCRYPT_OPAQUE_KEY_BLOB (PCP_KEY_BLOB_WIN8), and import it via
// NCryptImportKey.
std::unique_ptr<UnexportableAttestationKey> GenerateAttestationKeySlowly(
base::span<const sign::SignatureKind> acceptable_algorithms) override {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
if (provider_type_ != ProviderType::kTPM || !IsTbsAvailable() ||
!IsTpm20Available()) {
return nullptr;
}
// 1. Open the Platform Crypto Provider and select the best supported
// algorithm.
ScopedNCryptProvider provider;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
GetWindowsIdentifierForProvider(provider_type_), /*flags=*/0);
if (FAILED(status)) {
LogTPMOperationError(TPMOperation::kNewAttestationKeyCreation, status,
std::nullopt,
/*open_storage_provider_error=*/true);
return nullptr;
}
}
ASSIGN_OR_RETURN(sign::SignatureKind algo,
GetBestSupported(provider.get(), acceptable_algorithms),
[] { return nullptr; });
// 2. Extract the underlying TBS context handle from the provider.
ASSIGN_OR_RETURN(TBS_HCONTEXT h_context,
GetNCryptProperty<TBS_HCONTEXT>(
provider.get(), NCRYPT_PCP_PLATFORMHANDLE_PROPERTY),
[&](SECURITY_STATUS status) {
LogTPMOperationError(
TPMOperation::kNewAttestationKeyCreation, status,
algo);
return nullptr;
});
// 3. Construct and submit the TPM2_Create command to generate the AIK under
// the Storage Root Key (SRK).
ASSIGN_OR_RETURN(std::vector<uint8_t> create_cmd,
tpm::BuildCreateAikCommand(
std::to_underlying(GetSrkHandleFor(algo)), algo),
[&] {
LogTPMOperationError(
TPMOperation::kNewAttestationKeyCreation,
NTE_NOT_SUPPORTED, algo);
return nullptr;
});
ASSIGN_OR_RETURN(std::vector<uint8_t> create_resp,
SubmitTbsCommand(h_context, tpm::TpmCommand::kCreate,
create_cmd, kMaxTpmResponseSize, algo),
[] { return nullptr; });
// 4. Parse the TPM2_Create response to extract the public and private
// key areas.
ASSIGN_OR_RETURN(
tpm::CreateResponse parsed_create,
ToOptionalAndRecordParseMetrics(tpm::ParseCreateResponse(create_resp)),
[] { return nullptr; });
// 5. Build a BCRYPT_OPAQUE_KEY_BLOB (PCP_KEY_BLOB_WIN8) from the
// TPM2_Create output and import it to obtain a functional key handle.
return FromWrappedAttestationKeySlowly(
BuildWrappedAttestationKey(parsed_create));
}
std::optional<KeyDetails> FromWrappedKeyImpl(
base::span<const uint8_t> wrapped,
KeyUsage usage) {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
ScopedNCryptKey key = LoadWrappedKey(wrapped, provider_type_, usage);
if (!key.is_valid()) {
return std::nullopt;
}
if ((usage == KeyUsage::kAttestation) != IsIdentityKey(key.get())) {
return std::nullopt;
}
// The documentation suggests that |NCRYPT_ALGORITHM_PROPERTY| should return
// the original algorithm, i.e. |BCRYPT_ECDSA_P256_ALGORITHM| for ECDSA. But
// it actually returns just "ECDSA" for keys backed by the TPM.
ASSIGN_OR_RETURN(
std::wstring algorithm,
GetKeyStringProperty(key.get(), NCRYPT_ALGORITHM_PROPERTY));
if (algorithm == BCRYPT_ECDSA_P256_ALGORITHM ||
algorithm == BCRYPT_ECDSA_ALGORITHM) {
ASSIGN_OR_RETURN(std::vector<uint8_t> spki, GetP256ECDSASPKI(key.get()));
return KeyDetails{std::move(key), base::ToVector(wrapped),
std::move(spki), sign::ECDSA_SHA256};
}
if (algorithm == BCRYPT_RSA_ALGORITHM) {
ASSIGN_OR_RETURN(std::vector<uint8_t> spki, GetRSASPKI(key.get()));
return KeyDetails{std::move(key), base::ToVector(wrapped),
std::move(spki), sign::RSA_PKCS1_SHA256};
}
return std::nullopt;
}
std::unique_ptr<UnexportableSigningKey> FromWrappedSigningKeySlowly(
base::span<const uint8_t> wrapped) override {
ASSIGN_OR_RETURN(KeyDetails key,
FromWrappedKeyImpl(wrapped, KeyUsage::kSigning),
[] { return nullptr; });
switch (key.algo) {
case sign::ECDSA_SHA256:
return std::make_unique<ECDSASigningKey>(provider_type_,
std::move(key));
case sign::RSA_PKCS1_SHA256:
return std::make_unique<RSASigningKey>(provider_type_, std::move(key));
default:
return nullptr;
}
}
std::unique_ptr<UnexportableAttestationKey> FromWrappedAttestationKeySlowly(
base::span<const uint8_t> wrapped) override {
ASSIGN_OR_RETURN(KeyDetails key,
FromWrappedKeyImpl(wrapped, KeyUsage::kAttestation),
[] { return nullptr; });
return std::make_unique<AttestationKeyWin>(provider_type_, std::move(key));
}
StatefulUnexportableKeyProvider* AsStatefulUnexportableKeyProvider()
override {
// Unexportable keys are stateless on Windows.
return nullptr;
}
private:
ProviderType provider_type_;
};
// ECDSASoftwareKey wraps a Credential Guard stored P-256 ECDSA key.
class ECDSASoftwareKey : public VirtualUnexportableSigningKey {
public:
ECDSASoftwareKey(ScopedNCryptKey key,
std::string name,
std::vector<uint8_t> spki)
: key_(std::move(key)), name_(std::move(name)), spki_(std::move(spki)) {}
sign::SignatureKind Algorithm() const override { return sign::ECDSA_SHA256; }
std::vector<uint8_t> GetSubjectPublicKeyInfo() const override {
return spki_;
}
std::string GetKeyName() const override { return name_; }
std::optional<std::vector<uint8_t>> Sign(
base::span<const uint8_t> data) override {
if (!key_.is_valid()) {
return std::nullopt;
}
return base::OptionalFromExpected(SignECDSA(key_.get(), data));
}
void DeleteKey() override {
if (!key_.is_valid()) {
return;
}
// If key deletion succeeds, NCryptDeleteKey frees the key. To avoid double
// free, we need to release the key from the ScopedNCryptKey RAII object.
// Key deletion can fail in circumstances which are not under the
// application's control. For these cases, ScopedNCrypt key should free the
// key.
if (NCryptDeleteKey(key_.get(), NCRYPT_SILENT_FLAG) == ERROR_SUCCESS) {
static_cast<void>(key_.release());
}
}
private:
ScopedNCryptKey key_;
const std::string name_;
const std::vector<uint8_t> spki_;
};
// RSASoftwareKey wraps a Credential Guard stored RSA key.
class RSASoftwareKey : public VirtualUnexportableSigningKey {
public:
RSASoftwareKey(ScopedNCryptKey key,
std::string name,
std::vector<uint8_t> spki)
: key_(std::move(key)), name_(std::move(name)), spki_(std::move(spki)) {}
sign::SignatureKind Algorithm() const override {
return sign::RSA_PKCS1_SHA256;
}
std::vector<uint8_t> GetSubjectPublicKeyInfo() const override {
return spki_;
}
std::string GetKeyName() const override { return name_; }
std::optional<std::vector<uint8_t>> Sign(
base::span<const uint8_t> data) override {
if (!key_.is_valid()) {
return std::nullopt;
}
return base::OptionalFromExpected(SignRSA(key_.get(), data));
}
void DeleteKey() override {
if (!key_.is_valid()) {
return;
}
// If key deletion succeeds, NCryptDeleteKey frees the key. To avoid double
// free, we need to release the key from the ScopedNCryptKey RAII object.
// Key deletion can fail in circumstances which are not under the
// application's control. For these cases, ScopedNCrypt key should free the
// key.
if (NCryptDeleteKey(key_.get(), NCRYPT_SILENT_FLAG) == ERROR_SUCCESS) {
static_cast<void>(key_.release());
}
}
private:
ScopedNCryptKey key_;
std::string name_;
const std::vector<uint8_t> spki_;
};
// UnexportableKeyProviderWin uses NCrypt and the Platform Crypto
// Provider to expose Credential Guard backed keys on Windows.
class VirtualUnexportableKeyProviderWin
: public VirtualUnexportableKeyProvider {
public:
~VirtualUnexportableKeyProviderWin() override = default;
std::optional<sign::SignatureKind> SelectAlgorithm(
base::span<const sign::SignatureKind> acceptable_algorithms) override {
ScopedNCryptProvider provider;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
MS_KEY_STORAGE_PROVIDER, /*dwFlags=*/0);
if (FAILED(status)) {
base::UmaHistogramSparse(kMetricVirtualOpenStorageError, status);
return std::nullopt;
}
}
return GetBestSupported(provider.get(), acceptable_algorithms);
}
std::unique_ptr<VirtualUnexportableSigningKey> GenerateSigningKey(
base::span<const sign::SignatureKind> acceptable_algorithms,
std::string name) override {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
ScopedNCryptProvider provider;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
MS_KEY_STORAGE_PROVIDER, /*dwFlags=*/0);
if (FAILED(status)) {
base::UmaHistogramSparse(kMetricVirtualOpenStorageError, status);
return nullptr;
}
}
std::optional<sign::SignatureKind> algo =
GetBestSupported(provider.get(), acceptable_algorithms);
if (!algo) {
return nullptr;
}
ScopedNCryptKey key;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
// An empty key name stops the key being persisted to disk.
SECURITY_STATUS status = NCryptCreatePersistedKey(
provider.get(), ScopedNCryptKey::Receiver(key).get(),
BCryptAlgorithmFor(*algo).value(), base::SysUTF8ToWide(name).c_str(),
/*dwLegacyKeySpec=*/0,
/*dwFlags=*/NCRYPT_USE_VIRTUAL_ISOLATION_FLAG);
if (FAILED(status)) {
base::UmaHistogramSparse(kMetricVirtualCreateKeyError, status);
return nullptr;
}
status = NCryptFinalizeKey(
key.get(), NCRYPT_PROTECT_TO_LOCAL_SYSTEM | NCRYPT_SILENT_FLAG);
if (FAILED(status)) {
base::UmaHistogramSparse(kMetricVirtualFinalizeKeyError, status);
return nullptr;
}
}
std::optional<std::vector<uint8_t>> spki;
switch (*algo) {
case sign::ECDSA_SHA256:
spki = GetP256ECDSASPKI(key.get());
if (!spki) {
return nullptr;
}
return std::make_unique<ECDSASoftwareKey>(std::move(key), name,
std::move(spki.value()));
case sign::RSA_PKCS1_SHA256:
spki = GetRSASPKI(key.get());
if (!spki) {
return nullptr;
}
return std::make_unique<RSASoftwareKey>(std::move(key), name,
std::move(spki.value()));
default:
return nullptr;
}
}
std::unique_ptr<VirtualUnexportableSigningKey> FromKeyName(
std::string name) override {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::WILL_BLOCK);
ScopedNCryptProvider provider;
ScopedNCryptKey key;
{
SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY();
SECURITY_STATUS status = NCryptOpenStorageProvider(
ScopedNCryptProvider::Receiver(provider).get(),
MS_KEY_STORAGE_PROVIDER, /*dwFlags=*/0);
if (FAILED(status)) {
base::UmaHistogramSparse(kMetricVirtualOpenStorageError, status);
return nullptr;
}
status = NCryptOpenKey(
provider.get(), ScopedNCryptKey::Receiver(key).get(),
base::SysUTF8ToWide(name).c_str(), /*dwLegacyKeySpec=*/0,
/*dwFlags*/ 0);
if (FAILED(status)) {
base::UmaHistogramSparse(kMetricVirtualOpenKeyError, status);
return nullptr;
}
}
ASSIGN_OR_RETURN(std::wstring algorithm,
GetKeyStringProperty(key.get(), NCRYPT_ALGORITHM_PROPERTY),
[] { return nullptr; });
// This is the expected behavior, but note it is different from TPM backed
// keys.
if (algorithm == BCRYPT_ECDSA_P256_ALGORITHM) {
ASSIGN_OR_RETURN(std::vector<uint8_t> spki, GetP256ECDSASPKI(key.get()),
[] { return nullptr; });
return std::make_unique<ECDSASoftwareKey>(std::move(key), name,
std::move(spki));
} else if (algorithm == BCRYPT_RSA_ALGORITHM) {
ASSIGN_OR_RETURN(std::vector<uint8_t> spki, GetRSASPKI(key.get()),
[] { return nullptr; });
return std::make_unique<RSASoftwareKey>(std::move(key), name,
std::move(spki));
}
return nullptr;
}
};
} // namespace
ScopedNCryptKey DuplicatePlatformKeyHandle(const UnexportableSigningKey& key) {
return LoadWrappedKey(
key.GetWrappedKey(),
key.IsHardwareBacked() ? ProviderType::kTPM : ProviderType::kSoftware,
IsIdentityKey(key.GetNCryptKeyHandle()) ? KeyUsage::kAttestation
: KeyUsage::kSigning);
}
std::unique_ptr<UnexportableKeyProvider> GetUnexportableKeyProviderWin() {
return std::make_unique<UnexportableKeyProviderWin>(ProviderType::kTPM);
}
std::unique_ptr<UnexportableKeyProvider>
GetMicrosoftSoftwareUnexportableKeyProviderWin() {
return std::make_unique<UnexportableKeyProviderWin>(ProviderType::kSoftware);
}
std::unique_ptr<VirtualUnexportableKeyProvider>
GetVirtualUnexportableKeyProviderWin() {
return std::make_unique<VirtualUnexportableKeyProviderWin>();
}
} // namespace crypto