blob: 2ad41cf9b82625f52ef1839691e9d3df5d1e5080 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <base/logging.h>
#include <base/macros.h>
#include <base/rand_util.h>
#include <base/stl_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <base/timer/elapsed_timer.h>
#include <brillo/syslog_logging.h>
#include <crypto/random.h>
#include <trunks/cr50_headers/u2f.h>
#include "u2fd/g2f_tools/g2f_client.h"
namespace {
constexpr size_t kFrameSize = u2f::kU2fReportSize;
brillo::Blob GetRandomData(size_t size) {
brillo::Blob blob(size);
base::RandBytes(blob.data(), size);
return blob;
}
} // namespace
namespace g2f_client {
struct FrameInitHeader {
HidDevice::Cid cid;
uint8_t cmd;
uint8_t bcnth;
uint8_t bcntl;
} __attribute__((__packed__));
struct FrameInit {
FrameInitHeader header;
uint8_t data[kFrameSize - sizeof(FrameInitHeader)];
constexpr size_t PayloadSize() const {
return (static_cast<size_t>(header.bcnth) << 8) + header.bcntl;
}
constexpr static size_t MaxDataSize() { return base::size(decltype(data){}); }
constexpr static size_t DataFits(size_t all_data_size) {
return std::min(all_data_size, MaxDataSize());
}
} __attribute__((__packed__));
static_assert(sizeof(FrameInit) == kFrameSize, "Bad FromInit size");
struct FrameContHeader {
HidDevice::Cid cid;
uint8_t seq;
} __attribute__((__packed__));
struct FrameCont {
FrameContHeader header;
uint8_t data[kFrameSize - sizeof(FrameContHeader)];
constexpr static size_t MaxDataSize() { return base::size(decltype(data){}); }
constexpr static size_t DataFits(size_t all_data_size) {
return std::min(all_data_size, MaxDataSize());
}
} __attribute__((__packed__));
static_assert(sizeof(FrameCont) == kFrameSize, "Bad FrameCont size");
struct FrameBlob {
uint8_t report_id;
union {
HidDevice::Cid cid;
FrameInit init;
FrameCont cont;
} frame;
constexpr bool IsInit() const {
return frame.init.header.cmd & u2f::kFrameTypeInit;
}
constexpr size_t MaxDataSize() const {
return IsInit() ? FrameInit::MaxDataSize() : FrameCont::MaxDataSize();
}
constexpr size_t DataFits(size_t all_data_size) const {
return IsInit() ? FrameInit::DataFits(all_data_size)
: FrameCont::DataFits(all_data_size);
}
constexpr uint8_t* Data() {
return IsInit() ? frame.init.data : frame.cont.data;
}
constexpr const uint8_t* Data() const {
return IsInit() ? frame.init.data : frame.cont.data;
}
constexpr unsigned PrintableCmdValue() const {
return static_cast<unsigned>(frame.init.header.cmd);
}
} __attribute__((__packed__));
static_assert(sizeof(FrameBlob) == kFrameSize + 1, "Bad FrameBlob size");
HidDevice::HidDevice(const std::string& path) : path_(path) {}
HidDevice::~HidDevice() {
Close();
}
bool HidDevice::Open() {
if (!IsOpened()) {
dev_ = hid_open_path(path_.c_str());
if (!IsOpened()) {
LOG(ERROR) << "Failed to open " << path_;
return false;
}
}
return true;
}
void HidDevice::Close() {
if (dev_) {
hid_close(dev_);
dev_ = nullptr;
}
}
bool HidDevice::SendRequest(const Cid& cid,
uint8_t cmd,
const brillo::Blob& payload) {
size_t size = payload.size();
if (size > UINT16_MAX) {
LOG(ERROR) << "Too large payload (" << size << ") for cmd " << cmd;
return false;
}
if (!dev_) {
LOG(ERROR) << "SendRequest on closed device.";
return false;
}
FrameBlob blob;
blob.report_id = 0;
blob.frame.init.header.cid = cid;
blob.frame.init.header.cmd = cmd | u2f::kFrameTypeInit;
blob.frame.init.header.bcnth = static_cast<uint8_t>((size >> 8) & 0xFFu);
blob.frame.init.header.bcntl = static_cast<uint8_t>(size & 0xFFu);
size_t data_size = FrameInit::DataFits(size);
const uint8_t* data_start = payload.data();
std::copy(data_start, data_start + data_size, blob.frame.init.data);
uint8_t seq = 0;
do {
if (!WriteBlob(blob)) {
return false;
}
size -= data_size;
data_start += data_size;
blob.frame.cont.header.cid = cid;
blob.frame.cont.header.seq = seq++;
data_size = FrameCont::DataFits(size);
std::copy(data_start, data_start + data_size, blob.frame.cont.data);
} while (size);
return true;
}
bool HidDevice::RecvResponse(const Cid& cid,
uint8_t* cmd,
brillo::Blob* payload,
int timeout_ms) {
CHECK(cmd);
CHECK(payload);
base::ElapsedTimer timer;
FrameBlob blob;
bool wait_for_init = true;
size_t size = 0;
uint8_t seq = 0;
uint8_t* payload_ptr = nullptr;
if (!dev_) {
LOG(ERROR) << "RecvResponse on closed device.";
return false;
}
do {
if (!ReadBlob(&blob, timeout_ms)) {
return false;
}
if (blob.frame.cid.value() != cid.value()) {
LOG(WARNING) << "Unexpected response from cid " << blob.frame.cid.value();
continue;
}
if (wait_for_init) {
if (!blob.IsInit()) {
LOG(WARNING) << "Unexpected CONT from cid " << cid.value() << ": "
<< blob.PrintableCmdValue();
continue;
}
*cmd = blob.frame.init.header.cmd & ~u2f::kFrameTypeMask;
size = blob.frame.init.PayloadSize();
payload->resize(size);
payload_ptr = payload->data();
wait_for_init = false;
} else {
if (blob.IsInit()) {
LOG(ERROR) << "Unexpected INIT from cid " << cid.value() << ": "
<< blob.PrintableCmdValue();
return false;
}
if (blob.frame.cont.header.seq != seq++) {
LOG(ERROR) << "Unexpected SEQ from cid " << cid.value() << ": "
<< static_cast<unsigned>(blob.frame.cont.header.seq)
<< " instead of " << static_cast<unsigned>(seq - 1);
return false;
}
}
size_t data_size = blob.DataFits(size);
const uint8_t* data_start = blob.Data();
std::copy(data_start, data_start + data_size, payload_ptr);
payload_ptr += data_size;
size -= data_size;
} while ((wait_for_init || size) &&
(timeout_ms < 0 || timer.Elapsed().InMilliseconds() < timeout_ms));
return !(wait_for_init || size);
}
bool HidDevice::WriteBlob(const FrameBlob& blob) {
const unsigned char* data =
static_cast<const unsigned char*>(&blob.report_id);
VLOG(3) << "HID Send Frame " << base::HexEncode(data + 1, kFrameSize);
return CheckDeviceError("hid_write", hid_write(dev_, data, sizeof(FrameBlob)),
sizeof(FrameBlob));
}
bool HidDevice::ReadBlob(FrameBlob* blob, int timeout_ms) {
CHECK(blob);
blob->report_id = 0;
unsigned char* data = static_cast<unsigned char*>(&blob->report_id) + 1;
if (!CheckDeviceError("hid_read_timeout",
hid_read_timeout(dev_, data, kFrameSize, timeout_ms),
kFrameSize)) {
return false;
}
VLOG(3) << "HID Recv Frame " << base::HexEncode(data, kFrameSize);
return true;
}
bool HidDevice::CheckDeviceError(const std::string& func,
int res,
int expected) {
if (res == expected) {
return true;
}
LOG(ERROR) << func << " for " << path_ << " failed (" << res
<< " != " << expected << "): " << hid_error(dev_);
return false;
}
bool U2FHid::Command::CheckSuccess(const std::string& descr) const {
if (!IsError()) {
return true;
}
LOG(ERROR) << descr << " failed: " << static_cast<unsigned>(ErrorCode())
<< " (" << ErrorName() << ")";
return false;
}
constexpr uint8_t U2FHid::Command::ErrorCode() const {
if (!IsError()) {
return static_cast<uint8_t>(ErrorCode::kNone);
}
return payload.empty() ? static_cast<uint8_t>(ErrorCode::kOther) : payload[0];
}
std::string U2FHid::Command::Description() const {
return base::StringPrintf("%u (%s) [%zu bytes]", cmd, CommandName().c_str(),
payload.size());
}
std::string U2FHid::Command::FullDump() const {
return Description() + ": " + base::HexEncode(payload.data(), payload.size());
}
std::string U2FHid::Command::CommandName() const {
switch (static_cast<CommandCode>(cmd)) {
case CommandCode::kPing:
return "Ping";
case CommandCode::kMsg:
return "Msg";
case CommandCode::kLock:
return "Lock";
case CommandCode::kVendorSysInfo:
return "VendorSysInfo";
case CommandCode::kInit:
return "Init";
case CommandCode::kWink:
return "Wink";
case CommandCode::kError:
return "Error";
}
return "?";
}
std::string U2FHid::Command::ErrorName() const {
switch (static_cast<U2FHid::ErrorCode>(ErrorCode())) {
case ErrorCode::kNone:
return "None";
case ErrorCode::kInvalidCmd:
return "InvalidCmd";
case ErrorCode::kInvalidPar:
return "InvalidPar";
case ErrorCode::kInvalidLen:
return "InvalidLen";
case ErrorCode::kInvalidSeq:
return "InvalidSeq";
case ErrorCode::kMsgTimeout:
return "MsgTimeout";
case ErrorCode::kChannelBusy:
return "ChannelBusy";
case ErrorCode::kLockRequired:
return "LockRequired";
case ErrorCode::kInvalidCid:
return "InvalidCid";
case ErrorCode::kOther:
return "Other";
}
return "?";
}
U2FHid::U2FHid(HidDevice* hid_device) : hid_device_(hid_device) {}
bool U2FHid::RawCommand(const Command& request, Command* response) {
CHECK(response);
if (!hid_device_) {
LOG(ERROR) << "No hid device provided";
return false;
}
if (!hid_device_->Open()) {
return false;
}
VLOG(2) << "U2F SEND " << request.FullDump();
if (!hid_device_->SendRequest(cid_, request.cmd, request.payload)) {
return false;
}
if (!hid_device_->RecvResponse(cid_, &response->cmd, &response->payload,
timeout_ms_)) {
return false;
}
VLOG(2) << "U2F RECV " << response->FullDump();
return true;
}
bool U2FHid::GetSuccessfulResponse(const Command& request, Command* response) {
if (!RawCommand(request, response)) {
LOG(ERROR) << "Sending " << request.Description() << " failed";
return false;
}
return response->CheckSuccess("Command");
}
bool U2FHid::Init(bool force_realloc) {
struct InitResponse {
uint8_t nonce[u2f::kInitNonceSize];
HidDevice::Cid cid;
Version version;
uint8_t caps;
} __attribute__((__packed__));
if (Initialized()) {
if (!force_realloc) {
return true;
}
VLOG(1) << "Forcing re-initialization.";
cid_ = HidDevice::kCidBroadcast;
}
Command req = {CommandCode::kInit, GetRandomData(u2f::kInitNonceSize)};
Command rsp;
InitResponse parsed_response;
if (req.payload.size() != u2f::kInitNonceSize) {
LOG(ERROR) << "Failed to generate nonce, size = " << req.payload.size();
return false;
}
if (!GetSuccessfulResponse(req, &rsp)) {
return false;
}
if (rsp.payload.size() < sizeof(InitResponse)) {
LOG(ERROR) << "Bad INIT response size: " << rsp.payload.size();
return false;
}
std::copy(rsp.payload.data(), rsp.payload.data() + sizeof(InitResponse),
parsed_response.nonce);
if (std::memcmp(parsed_response.nonce, req.payload.data(),
u2f::kInitNonceSize)) {
LOG(ERROR) << "INIT nonce mismatch";
return false;
}
if (parsed_response.cid.IsBroadcast()) {
LOG(ERROR) << base::StringPrintf("Bad cid from init: %#08x",
parsed_response.cid.value());
return false;
}
caps_ = parsed_response.caps;
version_ = parsed_response.version;
cid_ = parsed_response.cid;
VLOG(2) << base::StringPrintf("INIT: Version = %u.%u.%u.%u",
version_.protocol, version_.major,
version_.minor, version_.build);
VLOG(2) << base::StringPrintf("INIT: Caps = %#x", caps_);
VLOG(1) << base::StringPrintf("Using cid %u (%#08x)", cid_.value(),
cid_.value());
return true;
}
bool U2FHid::Lock(uint8_t lock_timeout_seconds) {
if (!Init(false /* force_realloc */)) {
return false;
}
if (lock_timeout_seconds > 10) {
LOG(WARNING) << "Too large Lock timeout: "
<< static_cast<unsigned>(lock_timeout_seconds);
}
Command req = {CommandCode::kLock, {lock_timeout_seconds}};
Command rsp;
if (!GetSuccessfulResponse(req, &rsp)) {
return false;
}
if (!rsp.payload.empty()) {
LOG(ERROR) << "Lock response contains unexpected data";
return false;
}
return true;
}
bool U2FHid::Msg(const brillo::Blob& request, brillo::Blob* response) {
CHECK(response);
if (!Init(false /* force_realloc */)) {
return false;
}
Command req = {CommandCode::kMsg, request};
Command rsp;
if (!GetSuccessfulResponse(req, &rsp)) {
return false;
}
*response = rsp.payload;
return true;
}
bool U2FHid::Ping(size_t size) {
if (!Init(false /* force_realloc */)) {
return false;
}
Command req = {CommandCode::kPing, GetRandomData(size)};
Command rsp;
if (!GetSuccessfulResponse(req, &rsp)) {
return false;
}
if (rsp.payload != req.payload) {
LOG(ERROR) << "Ping response data mismatch";
return false;
}
return true;
}
bool U2FHid::Wink() {
if (!Init(false /* force_realloc */)) {
return false;
}
Command req = {CommandCode::kWink, {}};
Command rsp;
if (!GetSuccessfulResponse(req, &rsp)) {
return false;
}
if (!rsp.payload.empty()) {
LOG(ERROR) << "Wink response contains unexpected data";
return false;
}
return true;
}
bool U2F::SendMsg(const brillo::Blob& request,
int min_response_size,
brillo::Blob* response) {
brillo::Blob raw_response;
if (!u2f_device_->Msg(request, response)) {
LOG(ERROR) << "Sending U2F message failed";
return false;
}
if (response->size() < 2 /* SW1 + SW2 */) {
LOG(ERROR) << "Malformed response received, size: " << response->size();
return false;
}
uint16_t status;
// The final two bytes of the response are SW1 and SW2,
// which respectively make up the MSB and LSB of the
// 16-bit return status word.
status = response->back();
response->pop_back();
status |= response->back() << 8;
response->pop_back();
if (status != U2F_SW_NO_ERROR) {
LOG(ERROR) << "Bad status received: 0x" << std::hex << status;
return false;
}
if (response->size() < min_response_size) {
LOG(ERROR) << "Response too short, size: " << response->size();
return false;
}
return true;
}
void U2F::AppendBlob(const brillo::Blob& from, brillo::Blob* to) {
std::copy(from.begin(), from.end(), std::back_inserter(*to));
}
bool U2F::AppendFromResponse(const brillo::Blob& from,
brillo::Blob::iterator* from_it,
size_t length,
brillo::Blob* to) {
if (*from_it + length > from.end()) {
LOG(ERROR) << "Attempting to copy " << length << " bytes from response, "
<< "but only " << from.end() - *from_it << " available. ";
return false;
}
std::copy_n(*from_it, length, std::back_inserter(*to));
*from_it += length;
return true;
}
namespace {
bool CheckBlobLength(const brillo::Blob& blob,
int expected_length,
const std::string& name) {
if (blob.size() != expected_length) {
LOG(ERROR) << "Invalid " << name << " length, expected " << expected_length
<< ", but got " << blob.size();
return false;
} else {
return true;
}
}
} // namespace
bool U2F::Register(base::Optional<uint8_t> p1,
const brillo::Blob& challenge,
const brillo::Blob& application,
bool use_g2f_att_key,
brillo::Blob* public_key,
brillo::Blob* key_handle,
brillo::Blob* certificate_and_signature) {
bool challenge_valid = CheckBlobLength(challenge, 32, "challenge");
bool application_valid = CheckBlobLength(application, 32, "application");
if (!challenge_valid || !application_valid) {
return false;
}
// If P1 is not set, use default value of 0x3 - "check-user-presence"
p1 = p1 ? *p1 : U2F_AUTH_ENFORCE;
if (use_g2f_att_key)
*p1 |= G2F_ATTEST;
// Message format defined in sections 3 and 4.1 of the
// U2F Raw Message Formats Spec v1.2.
brillo::Blob request = {
0x00, // CLA
0x01, // INS: U2F_REGISTER
*p1, // P1
0x00, // P2
64, // Request length
};
AppendBlob(challenge, &request);
AppendBlob(application, &request);
// Final byte is maximum response length.
request.push_back(255);
brillo::Blob response;
if (!SendMsg(request, kRegResponseMinSize, &response)) {
LOG(ERROR) << "Register failed.";
return false;
}
// There is a legacy byte at the beginning; skip over it.
auto response_iter = response.begin() + kRegResponseStartOffset;
// Public key has a fixed length. Size check above guarantees we have
// enough data in response to read this.
AppendFromResponse(response, &response_iter, kRegResponsePublicKeyLength,
public_key);
// Key handle length is variable and provided in the response. Size
// check above guarantees we can read the length, but the length could
// be invalid, so reading the number of bytes it specifies could fail.
size_t key_handle_length = *response_iter++;
if (!AppendFromResponse(response, &response_iter, key_handle_length,
key_handle)) {
LOG(ERROR) << "Invalid key handle length provided in response.";
return false;
}
// Certificate/signature is the final item and takes up the
// remainder of the response. If the message is malformed,
// the certificate could be missing.
size_t cert_sig_length = response.end() - response_iter;
if (cert_sig_length == 0) {
LOG(ERROR) << "Certificate missing in register response.";
return false;
}
// Length is calculated based on remaining bytes, so this will succeed.
AppendFromResponse(response, &response_iter, cert_sig_length,
certificate_and_signature);
return true;
}
bool U2F::Authenticate(base::Optional<uint8_t> p1,
const brillo::Blob& challenge,
const brillo::Blob& application,
const brillo::Blob& key_handle,
bool* presence_verified,
brillo::Blob* counter,
brillo::Blob* signature) {
bool challenge_valid = CheckBlobLength(challenge, 32, "challenge");
bool application_valid = CheckBlobLength(application, 32, "application");
if (!challenge_valid || !application_valid) {
return false;
}
if (key_handle.size() > 190) {
LOG(ERROR) << "Key handle size too large";
return false;
}
uint8_t req_length = 65 + key_handle.size();
// If P1 is not set, use default value of 0x3 - "check-user-presence-and-sign"
p1 = p1 ? *p1 : U2F_AUTH_ENFORCE;
// Message format defined in sections 3 and 4.1 of the
// U2F Raw Message Formats Spec v1.2.
brillo::Blob request = {
0x00, // CLA
0x02, // INS: U2F_AUTHENTICATE
*p1, // P1
0x00, // P2
req_length, // Request length
};
AppendBlob(challenge, &request);
AppendBlob(application, &request);
request.push_back(key_handle.size());
AppendBlob(key_handle, &request);
// Final byte is maximum response length.
request.push_back(255);
brillo::Blob response;
if (!SendMsg(request, kAuthResponseMinSize, &response)) {
LOG(ERROR) << "Authenticate failed.";
return false;
}
// This is stored in the LSB of the first byte.
*presence_verified = response[0] & 1;
// Counter is fixed length.
auto response_iter = response.begin() + kAuthResponseCounterOffset;
AppendFromResponse(response, &response_iter, kAuthResponseCounterLength,
counter);
// Signature is the final field and occupies the remainder of
// the response. Size check above guarantees we have at least
// one byte of the response left to read.
AppendFromResponse(response, &response_iter, response.end() - response_iter,
signature);
return true;
}
} // namespace g2f_client