blob: 5e676bf2b5fb27fe13bbe67ff2cf7653849cbedc [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* Very basic TPM driver
*
* Assumptions:
* - The system firmware is responsible for initializing the TPM and has
* already done so.
*/
#include "tpm.h"
#include <assert.h>
#include <endian.h>
#include <lib/driver-unit-test/utils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <zircon/types.h>
#include <utility>
#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/driver.h>
#include <ddk/io-buffer.h>
#include <ddk/platform-defs.h>
#include <ddk/protocol/i2c.h>
#include <explicit-memory/bytes.h>
#include <fbl/alloc_checker.h>
#include <fbl/auto_call.h>
#include <fbl/auto_lock.h>
#include "i2c-cr50.h"
#include "tpm-commands.h"
// This is arbitrary, we just want to limit the size of the response buffer
// that we need to allocate.
#define MAX_RAND_BYTES 256
namespace tpm {
// implement tpm protocol:
zx_status_t Device::GetRandom(void* buf, uint16_t count, size_t* actual) {
static_assert(MAX_RAND_BYTES <= UINT32_MAX, "");
if (count > MAX_RAND_BYTES) {
count = MAX_RAND_BYTES;
}
struct tpm_getrandom_cmd cmd;
uint32_t resp_len = tpm_init_getrandom(&cmd, count);
std::unique_ptr<uint8_t[]> resp_buf(new uint8_t[resp_len]);
size_t actual_read;
uint16_t bytes_returned;
zx_status_t status =
ExecuteCmd(0, (uint8_t*)&cmd, sizeof(cmd), resp_buf.get(), resp_len, &actual_read);
if (status != ZX_OK) {
return status;
}
auto resp = reinterpret_cast<tpm_getrandom_resp*>(resp_buf.get());
if (actual_read < sizeof(*resp) || actual_read != betoh32(resp->hdr.total_len)) {
return ZX_ERR_BAD_STATE;
}
bytes_returned = betoh16(resp->bytes_returned);
if (actual_read != sizeof(*resp) + bytes_returned ||
resp->hdr.tag != htobe16(TPM_ST_NO_SESSIONS) || bytes_returned > count ||
resp->hdr.return_code != htobe32(TPM_SUCCESS)) {
return ZX_ERR_BAD_STATE;
}
memcpy(buf, resp->bytes, bytes_returned);
mandatory_memset(resp->bytes, 0, bytes_returned);
*actual = bytes_returned;
return ZX_OK;
}
zx_status_t Device::ShutdownLocked(uint16_t type) {
struct tpm_shutdown_cmd cmd;
uint32_t resp_len = tpm_init_shutdown(&cmd, type);
struct tpm_shutdown_resp resp;
size_t actual;
zx_status_t status =
ExecuteCmdLocked(0, (uint8_t*)&cmd, sizeof(cmd), (uint8_t*)&resp, resp_len, &actual);
if (status != ZX_OK) {
return status;
}
if (actual < sizeof(resp) || actual != betoh32(resp.hdr.total_len) ||
resp.hdr.tag != htobe16(TPM_ST_NO_SESSIONS) || resp.hdr.return_code != htobe32(TPM_SUCCESS)) {
return ZX_ERR_BAD_STATE;
}
return ZX_OK;
}
zx_status_t Device::Create(void* ctx, zx_device_t* parent, std::unique_ptr<Device>* out) {
zx::handle irq;
i2c_protocol_t i2c;
zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_I2C, &i2c);
if (status != ZX_OK) {
zxlogf(ERROR, "tpm: could not get I2C protocol: %d\n", status);
return ZX_ERR_NOT_SUPPORTED;
}
status = i2c_get_interrupt(&i2c, 0, irq.reset_and_get_address());
if (status == ZX_OK) {
// irq contains garbage?
zx_handle_t ignored __UNUSED = irq.release();
}
std::unique_ptr<I2cCr50Interface> i2c_iface;
status = I2cCr50Interface::Create(parent, std::move(irq), &i2c_iface);
if (status != ZX_OK) {
return status;
}
*out = std::make_unique<Device>(parent, std::move(i2c_iface));
return ZX_OK;
}
zx_status_t Device::CreateAndBind(void* ctx, zx_device_t* parent) {
std::unique_ptr<Device> device;
zx_status_t status = Create(ctx, parent, &device);
if (status != ZX_OK) {
return status;
}
status = device->Bind();
if (status == ZX_OK) {
// DevMgr now owns this pointer, release it to avoid destroying the
// object when device goes out of scope.
__UNUSED auto* ptr = device.release();
}
return status;
}
zx_status_t Device::ExecuteCmd(Locality loc, const uint8_t* cmd, size_t len, uint8_t* resp,
size_t max_len, size_t* actual) {
fbl::AutoLock guard(&lock_);
return ExecuteCmdLocked(loc, cmd, len, resp, max_len, actual);
}
zx_status_t Device::ExecuteCmdLocked(Locality loc, const uint8_t* cmd, size_t len, uint8_t* resp,
size_t max_len, size_t* actual) {
zx_status_t status = SendCmdLocked(loc, cmd, len);
if (status != ZX_OK) {
return status;
}
return RecvRespLocked(loc, resp, max_len, actual);
}
void Device::DdkRelease() { delete this; }
void Device::DdkUnbind() {
{
fbl::AutoLock guard(&lock_);
ReleaseLocalityLocked(0);
}
DdkRemove();
}
zx_status_t Device::DdkSuspend(uint32_t flags) {
fbl::AutoLock guard(&lock_);
if (flags == DEVICE_SUSPEND_FLAG_SUSPEND_RAM) {
zx_status_t status = ShutdownLocked(TPM_SU_STATE);
if (status != ZX_OK) {
zxlogf(ERROR, "tpm: Failed to save state: %d\n", status);
return status;
}
}
zx_status_t status = ReleaseLocalityLocked(0);
if (status != ZX_OK) {
zxlogf(ERROR, "tpm: Failed to release locality: %d\n", status);
return status;
}
return status;
}
zx_status_t Device::Bind() {
zx_status_t status = DdkAdd("tpm", DEVICE_ADD_INVISIBLE);
if (status != ZX_OK) {
return status;
}
thrd_t thread;
int ret = thrd_create_with_name(&thread, InitThread, this, "tpm:slow_bind");
if (ret != thrd_success) {
DdkRemove();
return ZX_ERR_INTERNAL;
}
thrd_detach(thread);
return ZX_OK;
}
zx_status_t Device::Init() {
zx_status_t status = iface_->Validate();
if (status != ZX_OK) {
zxlogf(TRACE, "tpm: did not pass driver validation\n");
return status;
}
{
fbl::AutoLock guard(&lock_);
// tpm_request_use will fail if we're not at least 30ms past _TPM_INIT.
// The system firmware performs the init, so it's safe to assume that
// is 30 ms past. If we're on systems where we need to do init,
// we need to wait up to 30ms for the TPM_ACCESS register to be valid.
status = RequestLocalityLocked(0);
if (status != ZX_OK) {
zxlogf(ERROR, "tpm: Failed to request use: %d\n", status);
return status;
}
status = WaitForLocalityLocked(0);
if (status != ZX_OK) {
zxlogf(ERROR, "tpm: Waiting for locality failed: %d\n", status);
return status;
}
}
return ZX_OK;
}
zx_status_t Device::InitThread() {
uint8_t buf[32] = {0};
size_t bytes_read;
auto cleanup = fbl::MakeAutoCall([&] { DdkRemove(); });
zx_status_t status = Init();
if (status != ZX_OK) {
return status;
}
DdkMakeVisible();
// Make a best-effort attempt to give the kernel some more entropy
// TODO(security): Perform a more recurring seeding
status = GetRandom(buf, static_cast<uint16_t>(sizeof(buf)), &bytes_read);
if (status == ZX_OK) {
zx_cprng_add_entropy(buf, bytes_read);
mandatory_memset(buf, 0, sizeof(buf));
} else {
zxlogf(ERROR, "tpm: Failed to add entropy to kernel CPRNG\n");
}
cleanup.cancel();
return ZX_OK;
}
bool Device::RunUnitTests(void* ctx, zx_device_t* parent, zx_handle_t channel) {
return driver_unit_test::RunZxTests("TpmTests", parent, channel);
}
Device::Device(zx_device_t* parent, std::unique_ptr<HardwareInterface> iface)
: DeviceType(parent), iface_(std::move(iface)) {
ddk_proto_id_ = ZX_PROTOCOL_TPM;
}
Device::~Device() {}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = tpm::Device::CreateAndBind;
ops.run_unit_tests = tpm::Device::RunUnitTests;
return ops;
}();
} // namespace tpm
// clang-format off
ZIRCON_DRIVER_BEGIN(tpm, tpm::driver_ops, "zircon", "0.1", 3)
// Handle I2C
// TODO(teisenbe): Make this less hacky when we have a proper I2C protocol
BI_ABORT_IF(NE, BIND_PCI_VID, 0x8086),
BI_ABORT_IF(NE, BIND_PCI_DID, 0x9d61),
BI_MATCH_IF(EQ, BIND_TOPO_I2C, BIND_TOPO_I2C_PACK(0x0050)),
ZIRCON_DRIVER_END(tpm)
// clang-format on