/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; }