blob: 77cd5eb2a3d53c578ef422c84ec611a4b27ed785 [file] [log] [blame]
/* Copyright 2015 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 <common/gnubby.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <libusb-1.0/libusb.h>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>
#include <common/aes.h>
#include <common/ecdh.h>
#include <string>
#define MAX_APDU_SIZE 1200
#define LIBUSB_ERR -1
extern bool FLAGS_verbose;
// Largely from gnubby ifd_driver.c
// -----
typedef int RESPONSECODE;
typedef uint8_t UCHAR;
typedef UCHAR* PUCHAR;
typedef uint32_t DWORD;
typedef DWORD* PDWORD;
#define IFD_SUCCESS 0
#define IFD_COMMUNICATION_ERROR -1
#define DLOG(...) do{if(FLAGS_verbose){fprintf(stderr, __VA_ARGS__);}}while(0)
// usb gnubby commands
#define CMD_ATR 0x81
#define CMD_APDU 0x83
#define CMD_LOCK 0x84
#define CMD_WINK 0x88
// Helper to dump bits to console.
static
void printHex(const char* text, const void* data, int len) {
int i;
const uint8_t* d = (const uint8_t*)(data);
(void)d;
DLOG("%s: ", text);
for (i = 0; i < len; ++i) {
DLOG("%02x", *d++);
if (i == 3) DLOG(":");
if (i == 4) DLOG("|");
}
DLOG("\n");
}
// Construct usb framed request.
// data can be NULL iff len == 0.
// Returns frame size > 0 on success.
static
RESPONSECODE construct_usb_frame(uint8_t cmd,
const void* data, DWORD len,
uint8_t* out, PDWORD out_len) {
const uint8_t* d = (const uint8_t*)(data);
DWORD i;
if (*out_len < len + 7) return IFD_COMMUNICATION_ERROR;
// use pid as channel id
out[0] = getpid() >> 0;
out[1] = getpid() >> 8;
out[2] = getpid() >> 16;
out[3] = getpid() >> 24;
out[4] = cmd;
out[5] = len >> 8;
out[6] = len;
// Append the actual payload.
for (i = 0; i < len; ++i) out[7 + i] = d[i];
// Return total length
*out_len = 7 + len;
return IFD_SUCCESS;
}
// Send cmd to gnubby and receive response.
static
RESPONSECODE gnubby_exchange(libusb_device_handle* dev_handle,
void* buf, int res,
void* rsp, PDWORD rsp_len) {
int sent_len = 0;
uint8_t rcv[2048];
int recv_len = 0;
DLOG("gnubby_exchange(%p, %p, %d, %p, *%u)\n",
dev_handle, buf, res, rsp, *rsp_len);
printHex(">", buf, res);
// Send to gnubby
res = libusb_bulk_transfer(dev_handle, (1 | LIBUSB_ENDPOINT_OUT),
(unsigned char*)(buf), res,
&sent_len, 0);
DLOG(">: libusb_bulk_transfer: %d [%d]\n", res, sent_len);
if (res < 0) return IFD_COMMUNICATION_ERROR;
// Read from gnubby
memset(rcv, 0, sizeof(rcv)); // start clean.
res = libusb_bulk_transfer(dev_handle, (1 | LIBUSB_ENDPOINT_IN),
rcv, sizeof rcv, &recv_len, 0);
DLOG("<: libusb_bulk_transfer: %d [%d]\n", res, recv_len);
if (res < 0) return IFD_COMMUNICATION_ERROR;
if (recv_len > 0) {
printHex("<", rcv, recv_len);
// Check return header.
// rcv[0..4] should be equal to request.
// rcv[5..6] is response payload length.
// rcv[recv_len-2..recv_len-1] is smartcard response code (9000 etc.)
if (memcmp(buf, rcv, 5)) return IFD_COMMUNICATION_ERROR;
uint16_t plen = rcv[5] * 256 + rcv[6];
if (plen + 7 < recv_len) return IFD_COMMUNICATION_ERROR;
if (*rsp_len < plen) return IFD_COMMUNICATION_ERROR;
// Copy response payload.
memcpy(rsp, rcv + 7, plen);
// Return payload length.
*rsp_len = plen;
return IFD_SUCCESS;
}
return IFD_COMMUNICATION_ERROR;
}
#if 0
static
RESPONSECODE gnubby_atr(libusb_device_handle* handle, PUCHAR Atr, PDWORD AtrLength) {
uint8_t cmd[10];
DWORD cmd_len = sizeof(cmd);
RESPONSECODE res;
DLOG("gnubby_atr(%p, %p, *%u)\n", handle, Atr, *AtrLength);
memset(Atr, 0, *AtrLength);
res = construct_usb_frame(CMD_ATR, NULL, 0, cmd, &cmd_len);
if (res != IFD_SUCCESS) return res;
res = gnubby_exchange(handle, cmd, cmd_len, Atr, AtrLength);
if (res == IFD_SUCCESS) {
// Present an ATR that can do T=1
// Gnubby ATR appears to not advertise that capability.
memcpy(Atr, "\x3B\xF0\x13\x00\x00\x81\x31\xFE\x45\xE8", 10);
*AtrLength = 10;
}
return res;
}
#endif
static
RESPONSECODE gnubby_apdu(libusb_device_handle* handle,
PUCHAR tx, DWORD txLen,
PUCHAR rx, PDWORD rxLen) {
uint8_t cmd[2048];
DWORD cmd_len = sizeof(cmd);
RESPONSECODE res = IFD_SUCCESS;
DLOG("gnubby_apdu(%p, %p, %u, %p, *%u)\n",
handle, tx, txLen, rx, *rxLen);
res = construct_usb_frame(CMD_APDU, tx, txLen, cmd, &cmd_len);
if (res != IFD_SUCCESS) return res;
res = gnubby_exchange(handle, cmd, cmd_len, rx, rxLen);
if (res != IFD_SUCCESS) *rxLen = 0;
return res;
}
static
RESPONSECODE gnubby_lock(libusb_device_handle* handle, UCHAR seconds) {
uint8_t cmd[10];
DWORD cmd_len = sizeof(cmd);
uint8_t rsp[10];
DWORD rsp_len = sizeof(rsp);
RESPONSECODE res = IFD_SUCCESS;
res = construct_usb_frame(CMD_LOCK, &seconds, 1, cmd, &cmd_len);
if (res != IFD_SUCCESS) return res;
res = gnubby_exchange(handle, cmd, cmd_len, rsp, &rsp_len);
if (res != IFD_SUCCESS) return res;
if ((rsp_len == 1 && rsp[0] == 0) ||
rsp_len == 0) {
return IFD_SUCCESS;
}
return IFD_COMMUNICATION_ERROR;
}
static
RESPONSECODE gnubby_wink(libusb_device_handle* handle) {
uint8_t cmd[10];
DWORD cmd_len = sizeof(cmd);
uint8_t rsp[10];
DWORD rsp_len = sizeof(rsp);
RESPONSECODE res = IFD_SUCCESS;
res = construct_usb_frame(CMD_WINK, NULL, 0, cmd, &cmd_len);
if (res != IFD_SUCCESS) return res;
res = gnubby_exchange(handle, cmd, cmd_len, rsp, &rsp_len);
if (res != IFD_SUCCESS) return res;
return res;
}
// -----
// end of ifd_driver cut&paste
// Open a usb device and return (handle_, context).
Gnubby::Gnubby()
: handle_(NULL) {
libusb_init(&ctx_);
libusb_set_debug(ctx_, 3);
}
// Close a usb device.
Gnubby::~Gnubby() {
// Close handle_ if non-zero.
if (handle_) {
int rc = libusb_release_interface(handle_, 0);
DLOG("gnubby release : %d\n", rc);
libusb_close(handle_);
(void) rc;
}
// Close context.
libusb_exit(ctx_);
}
static
int getSW12(const uint8_t* buf, size_t buflen) {
if (buflen < 2) return -1;
return buf[buflen - 2] * 256 + buf[buflen - 1];
}
static
void getPIN(uint8_t* out) {
srand(time(NULL)); // yuk
for (int i = 0; i < 16; ++i) out[i] = (uint32_t)rand() >> (i+1);
const char* pin = getpass("Gnubby PIN: ");
int len = strlen(pin);
if (len == 6) {
// Exactly 6, copy direct.
memcpy(out, pin, 6);
} else {
// SHA256, take first 6.
uint8_t digest[SHA256_DIGEST_LENGTH];
SHA256_CTX sha;
SHA256_Init(&sha);
SHA256_Update(&sha, pin, len);
SHA256_Final(digest, &sha);
memcpy(out, digest, 6);
}
}
static
std::string tokenFilename(const uint8_t* fp) {
const char* home = getenv("HOME");
if (home == NULL)
home = getpwuid(getuid())->pw_dir;
std::string s(home);
s.append("/.tmp/");
for (int i = 0; i < 32; ++i) {
s.push_back("0123456789abcdef"[fp[i]>>4]);
s.push_back("0123456789abcdef"[fp[i]&15]);
}
s.append(".token");
return s;
}
static
bool getToken(const uint8_t* fp, uint8_t* out) {
int fd = open(tokenFilename(fp).c_str(), O_RDONLY);
if (fd < 0) return false;
int n = read(fd, out, 16);
close(fd);
DLOG("read %d from %s\n", n, tokenFilename(fp).c_str());
return n == 16;
}
static
void saveToken(const uint8_t* fp, const uint8_t* token) {
int fd = open(tokenFilename(fp).c_str(), O_CREAT|O_TRUNC|O_APPEND|O_NOFOLLOW|O_WRONLY, 0600);
if (fd >= 0) {
int n = write(fd, token, 16);
DLOG("wrote %d to %s\n", n, tokenFilename(fp).c_str());
close(fd);
(void) n;
}
}
static
void forgetToken(const uint8_t* fp) {
DLOG("forgetting token %s\n", tokenFilename(fp).c_str());
unlink(tokenFilename(fp).c_str());
}
static
bool isGnubby(libusb_device *dev) {
struct libusb_device_descriptor lsb_desc;
if (!libusb_get_device_descriptor(dev, &lsb_desc)) {
return lsb_desc.idVendor == 0x1050 && lsb_desc.idProduct == 0x0211;
}
return false;
}
int Gnubby::doSign(EVP_MD_CTX* ctx, uint8_t* padded_req, uint8_t* signature,
uint32_t* siglen, EVP_PKEY* key) {
RESPONSECODE result = -1;
uint8_t fp[32];
ECDH ecdh;
uint8_t secret[32] = {0};
AES aes;
uint8_t pin[16];
uint8_t token[16];
UCHAR req[1024];
UCHAR resp[2048];
DWORD resp_len = 0;
// lock(100)
result = gnubby_lock(handle_, (UCHAR)100);
if (result != 0) goto __fail;
// TODO: handle busy etc.
// select ssh applet
resp_len = sizeof(resp);
result = gnubby_apdu(handle_,
(PUCHAR)"\x00\xa4\x04\x00\x06\x53\x53\x48\x00\x01\x01", 11,
resp, &resp_len);
if (result != 0) goto __fail;
if (getSW12(resp, resp_len) != 0x9000) goto __fail;
__again:
// read slot 0x66 under challenge
resp_len = sizeof(resp);
result = gnubby_apdu(handle_,
(PUCHAR)"\x00\x43\x66\x00\x00\x00\x10\xff\xee\xdd\xcc\xbb\xaa\x99\x88\x77\x66\x55\x44\x33\x22\x11\x00", 5 + 2 + 16,
resp, &resp_len);
if (result != 0) goto __fail;
if (getSW12(resp, resp_len) != 0x9000) goto __fail;
// save device fingerprint
memcpy(fp, resp + 1 + 256 + 65 + 65, 32);
uint8_t pubkey[256];
if (key->type != EVP_PKEY_RSA
|| EVP_PKEY_size(key) != 256
|| BN_bn2bin(key->pkey.rsa->n, pubkey) != 256) {
goto __fail;
}
if (memcmp(pubkey, resp + 1, 256)) {
// Key mis-match, wrong gnubby selected?
DLOG("pubkey mis-match, at device handle: %p", handle_);
goto __fail;
}
if (!getToken(fp, token)) {
// PIN unlock required.
getPIN(pin);
ecdh.compute_secret(resp + 1 + 256, secret);
aes.set_key(secret);
memcpy(req, "\x00\x42\x00\x00\x51", 5);
ecdh.get_point(req + 5);
printHex("req", req, 5 + 65);
aes.encrypt_block(pin, req + 5 + 65);
printHex("req", req, 5 + 65 + 16);
resp_len = sizeof(resp);
result = gnubby_apdu(handle_,
req, 5 + 65 + 16,
resp, &resp_len);
if (result != 0) goto __fail;
if ((getSW12(resp, resp_len) & 0xfff0) == 0x63c0) {
// Wrong PIN.
goto __again;
}
if (getSW12(resp, resp_len) != 0x9000) goto __fail;
aes.set_key(secret + 16);
aes.decrypt_block(resp, token);
saveToken(fp, token);
}
// Build sign request using slot 0x66.
memset(req, 0, sizeof(req));
memcpy(req, "\x00\x40\x66\x00\x00\x01\x10", 7);
memcpy(req + 7 + 16, padded_req, 256);
aes.set_key(token);
aes.cmac(req + 7 + 16, 256, req + 7);
for (;;) {
resp_len = sizeof(resp);
result = gnubby_apdu(handle_,
req, 7 + 256 + 16,
resp, &resp_len);
if (result != 0) goto __fail;
if (getSW12(resp, resp_len) == 0x6985) { // touch
gnubby_wink(handle_);
fprintf(stderr, "touch..");
fflush(stderr);
usleep(200000); // slow down, buddy
continue;
}
if (getSW12(resp, resp_len) == 0x63ca) { // pin
forgetToken(fp);
goto __again;
}
break;
}
if (getSW12(resp, resp_len) != 0x9000) goto __fail;
// Return signature.
memcpy(signature, resp, 256);
*siglen = 256;
// profit!
result = 1;
__fail:
// (always try to) unlock
gnubby_lock(handle_, 0);
return result;
}
int Gnubby::sign(EVP_MD_CTX* ctx, uint8_t* signature, uint32_t* siglen,
EVP_PKEY* key) {
// build pkcs1.5 request for ctx hash
// lock(100)
// select ssh
// read slot 0x66
// compare against key
// try read token from ~/.tmp/attest-fp
// loop
// - send sign request
// - handle PIN, touch
// unlock()
// profit!
RESPONSECODE result = -1;
DWORD image_hash_len = SHA256_DIGEST_LENGTH;
libusb_device **device_list;
ssize_t num_devices = libusb_get_device_list(ctx_, &device_list);
// Compute pkc15 padded inputs for requested sha256.
// Brutal hard-coding ftw.
uint8_t padded_req[256];
memset(padded_req, 0xff, sizeof(padded_req));
padded_req[0] = 0x00;
padded_req[1] = 0x01;
// Fixed asn1 riddle for sha256
memcpy(padded_req + 256 - 32 - 20,
"\x00\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20",
20);
// Append sha256
EVP_DigestFinal_ex(ctx,
padded_req + sizeof(padded_req) - SHA256_DIGEST_LENGTH,
&image_hash_len);
for (int i = 0; i < num_devices; i++) {
if (!isGnubby(device_list[i])) {
continue;
}
int rc = libusb_open(device_list[i], &handle_);
if (rc) {
DLOG("libusb_open() @ device index: %d failed: %d\n", i, rc);
continue;
}
rc = libusb_claim_interface(handle_, 0);
if (rc) {
DLOG("libusb_claim_interface() @ device index: %d failed: %d\n", i, rc);
if (handle_) {
libusb_close(handle_);
handle_ = NULL;
}
continue;
}
rc = doSign(ctx, padded_req, signature, siglen, key);
libusb_release_interface(handle_, 0);
libusb_close(handle_);
handle_ = NULL;
if (rc == 1) {
result = 1;
break;
}
// Try next device.
}
libusb_free_device_list(device_list, 1);
return result;
}
// Open a gnubby, unspecified selection made when multiple plugged in.
int Gnubby::open() {
RESPONSECODE result = -1;
handle_ = libusb_open_device_with_vid_pid(
ctx_,
0x1050, // Gnubby Vendor ID (VID)
0x0211 // Gnubby Product ID (PID)
);
DLOG("gnubby dev_handle_ %p\n", handle_);
int rc = libusb_claim_interface(handle_, 0);
DLOG("gnubby claim : %d\n", rc);
if (rc != 0) {
if (handle_) libusb_close(handle_);
} else {
result = 1;
}
return result;
}
int Gnubby::write_bn(uint8_t p1, BIGNUM* n, size_t length) {
RESPONSECODE result = -1;
UCHAR req[1024];
UCHAR resp[1024];
DWORD resp_len = 0;
memcpy(req, "\x00\x66\x00\x00\x00\x00\x00", 7);
req[2] = p1;
req[5] = length >> 8;
req[6] = length;
if (BN_bn2bin(n, req + 7) != int(length)) goto __fail;
resp_len = sizeof(resp);
result = gnubby_apdu(handle_,
req, 7 + length,
resp, &resp_len);
if (result != 0) goto __fail;
result = 1;
if (getSW12(resp, resp_len) != 0x9000) goto __fail;
result = 0;
__fail:
return result;
}
int Gnubby::write(RSA* rsa) {
RESPONSECODE result = -1;
UCHAR resp[2048];
DWORD resp_len = 0;
if (!handle_) {
result = (open() != 1);
if (result) goto __fail;
}
// lock(100)
result = gnubby_lock(handle_, (UCHAR)100);
if (result != 0) goto __fail;
// TODO: handle busy etc.
// select ssh applet
resp_len = sizeof(resp);
result = gnubby_apdu(handle_,
(PUCHAR)"\x00\xa4\x04\x00\x06\x53\x53\x48\x00\x01\x01", 11,
resp, &resp_len);
if (result != 0) goto __fail;
result = 1;
if (getSW12(resp, resp_len) != 0x9000) goto __fail;
result = 0;
result = result || write_bn(0, rsa->p, 128);
result = result || write_bn(1, rsa->q, 128);
result = result || write_bn(2, rsa->dmp1, 128);
result = result || write_bn(3, rsa->dmq1, 128);
result = result || write_bn(4, rsa->iqmp, 128);
result = result || write_bn(5, rsa->n, 256);
result = result || write_bn(6, rsa->e, 1);
__fail:
// (always try to) unlock
if (handle_) gnubby_lock(handle_, 0);
return result;
}