4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

M5Stack用暗号認証ユニットでFIDOデバイスを作る

Last updated at Posted at 2022-01-04

「M5Stack用暗号認証ユニット」という新しいM5ユニットが発売されています。
楕円暗号による公開鍵の演算が可能なコプロセッサATECC608Bを搭載しています。

チップの詳細は以下にあります。
https://ww1.microchip.com/downloads/en/DeviceDoc/ATECC608B-TNGTLS-CryptoAuthentication-Data-Sheet-DS40002250A.pdf

以前、M5StickCでFIDOデバイスエミュレータを作成していましたが、Node.jsサーバに転送しているだけでした。
 FIDOデバイスエミュレータようやく完成

そこで、M5StickCにM5Stack用暗号認証ユニットを接続して、M5StickC単体でFIDOデバイスにします。

以下のGitHubにアップデートしておきました。

poruruba/fido2_server

Fido2Gateway2の方です。
https://github.com/poruruba/fido2_server/tree/main/Fido2Gateway2

★以下の投稿で、もうNode.jsサーバは不要になりました。M5StickCのソースは以下のページにあるものをお使いください。  [M5Stack用暗号認証ユニットでFIDOデバイスを作る:の続き](https://qiita.com/poruruba/items/2956fd2fdd7e012f8b41)  https://github.com/poruruba/FidoU2fEmulator

(2022/1/9 追記)
その後、以下の投稿で、ESP32単体でX.509証明書を作成できるようにしました。
 M5Stack用暗号認証ユニットでFIDOデバイスを作る:の続き

(2022/1/30 追記)
なぜかAWSへのMFAデバイスとして認証できていませんでしたが、間違いを修正し、AWSでも使えるようになりました!

#機能要素

##ATECC608Bライブラリ

以下のサイトで示されているサンプルでは安定して動きませんでした。
https://github.com/sparkfun/SparkFun_ATECCX08A_Arduino_Library

そこで、ATECC608Bのアクセスには、以下を使わせていただきました。

ですが、M5Stack用暗号認証ユニットに搭載されているATECC608Bを扱うために少し修正します。また、M5StickCからのアクセスのためにも修正が必要でした。

ArduinoECCX08/src/ECCX08.cpp を修正します。

//43行目あたり
  _wire->begin();
→
  _wire->begin(32, 33);

//762行目あたり
  while (_wire->requestFrom((uint8_t)_address, (size_t)responseSize, (bool)true) != responseSize && retries--);
→
  while (_wire->requestFrom((uint8_t)_address, (uint8_t)responseSize, (uint8_t)true) != responseSize && retries--);

//780行目あたり
ECCX08Class ECCX08(Wire, 0x60);
→
ECCX08Class ECCX08(Wire, 0x35);

※スイッチサイエンスや本家M5のサイトには、I2Cアドレスは0x60とあるのですが、0x35が正しい模様。。。TNGTLSという形式でプリプロビジョニングされているため。 (2.1 ATECC608B-TNGTLS Configuration Zone参照)

以下、利用した関数です。これらがあれば、Node.jsサーバで行っていた処理をM5StickC上でもできそうです。

〇ATECC608Bのシリアル番号取得

  uint8_t serial[SERIAL_LENGTH];
  ret = ECCX08.serialNumber(serial);

〇SHA256生成

long make_sha256(const unsigned char *p_input, int input_length, unsigned char *p_hash)
{
  int ret = ECCX08.beginSHA256();
  if( !ret ){
    Serial.println("ECCX08.beginSHA256 Error");
    return -1;
  }
  for( int index = 0 ; index < input_length ; index += 64){
    if( (input_length - index) <= 64 ){
      ECCX08.endSHA256((const byte*)&p_input[index], input_length - index, p_hash);
      if( !ret ){
        Serial.println("ECCX08.endSHA256 Error");
        return -1;
      }
      break;
    }else{
      ECCX08.updateSHA256((const byte*)&p_input[index]);
      if( !ret ){
        Serial.println("ECCX08.updateSHA256 Error");
        return -1;
      }
    }
  }

  return 0;
}

〇公開鍵取得

  uint8_t publicKey[PUBLICKEY_LENGTH];
  publicKey[0] = 0x04;
  ret = ECCX08.generatePublicKey(DEFAULT_KEY_SLOT, &publicKey[1]);

〇署名生成

  unsigned char signature[RAW_SIGNATURE_LENGTH];
  ret = ECCX08.ecSign(DEFAULT_KEY_SLOT, hash, signature);

ただし、この署名結果はASN.1エンコードされていないため、以下のように成形します。

long set_signature(const unsigned char *raw_signature, unsigned char *p_payload)
{
  unsigned char total_len = 0x44;
  int index = 0;
  p_payload[index++] = 0x30;
  index++;
  p_payload[index++] = 0x02;
  if( raw_signature[0] >= 0x80 ){
    p_payload[index++] = 33;
    p_payload[index++] = 0x00;
    total_len++;
  }else{
    p_payload[index++] = 32;
  }
  memmove(&p_payload[index], &raw_signature[0], 32);
  index += 32;

  p_payload[index++] = 0x02;
  if( raw_signature[32] >= 0x80 ){
    p_payload[index++] = 33;
    p_payload[index++] = 0x00;
    total_len++;
  }else{
    p_payload[index++] = 32;
  }
  memmove(&p_payload[index], &raw_signature[32], 32);
  index += 32;

  p_payload[1] = total_len;

  return 2 + total_len;
}

〇不揮発データの読み書き

  byte data[4];
  ret = ECCX08.readSlot(DEFAULT_DATA_SLOT, data, sizeof(data));
  if( !ret ){
    Serial.println("readSlot error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  ret = ECCX08.writeSlot(DEFAULT_DATA_SLOT, data, sizeof(data));
  if( !ret ){
    Serial.println("writeSlot error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

〇認証回数カウンタ

よくみたら、認証回数をカウントアップするちょうどよい機能がありました。
ECCX08.c/hに追加実装しておきましょう。

ECCX08.cpp
int ECCX08Class::countUp(unsigned short index, unsigned long *p_counter)
{
  if (!wakeup()) {
    return 0;
  }

  if (index > 1) {
    return 0;
  }

  if (!sendCommand(0x24, 0x01, index)) {
    return 0;
  }

  delay(23);

  byte response[4];

  if (!receiveResponse(response, sizeof(response))) {
    return 0;
  }

  *p_counter = (response[3] << 24) | (response[2] << 16) | (response[1] << 8) | response[0];

  delay(1);

  idle();

  return 1;
}
ECCX08.h
  int countUp(unsigned short index, unsigned long *p_counter);

〇乱数生成

long make_random(int len, unsigned char *p_output)
{
  int ret = ECCX08.random(p_output, len);
  if( !ret ){
    Serial.println("ECCX08.random Error");
    return -1;
  }
  
  return 0;
}

〇準備
上記の操作をする際に、ATECC608Bが眠っている可能性があるようで、直前で以下を実施しておきます。

long auth_prepare(void)
{
  if (!ECCX08.begin()) {
    Serial.println("No ECCX08 present!");
    return -1;
  }

  if (!ECCX08.locked()) {
    Serial.println("ECCX08 not locked");
    return -1;
  }else{
    Serial.println("ECCX08 locked");
  }

  return 0;
}

#X.509証明書の生成要求

Node.jsサーバに、公開鍵を渡して、X.509証明書を作ってもらいます。

Node.js側ではこんな感じです。

async function u2f_certificate(pubKey) {
  var pubKeyHex = pubKey.toString('hex');
  var pubKeyObj = rs.KEYUTIL.getKey({
    'xy': pubKeyHex,
    'curve': 'secp256r1'
  });

  //サブジェクトキー識別子
  const ski = rs.KJUR.crypto.Util.hashHex(pubKeyHex, 'sha1');
  const derSKI = new rs.KJUR.asn1.DEROctetString({ hex: ski });

  // X.509証明書の作成
  sequence_no++;
  var cert = new rs.KJUR.asn1.x509.Certificate({
    version: 3,
    serial: { int: sequence_no },
    issuer: { str: "/CN=" + FIDO_ISSUER },
    notbefore: FIDO_EXPIRE_START,
    notafter: toUTCString(new Date(Date.now() + FIDO_EXPIRE * 24 * 60 * 60 * 1000)),
    subject: { str: "/CN=" + FIDO_SUBJECT + ("0000000000" + sequence_no).slice(-10) },
    sbjpubkey: pubKeyObj, // can specify public key object or PEM string
    sigalg: "SHA256withECDSA",
    ext: [
      {
        //サブジェクトキー識別子
        extname: "subjectKeyIdentifier",
        kid: {
//          hex: derSKI.getEncodedHex()
          hex: ski
        }
      },
      {
        // FIDO U2F certificate transports extension
        extname: "1.3.6.1.4.1.45724.2.1.1",
        extn: "03020640"
      }
    ],
    cakey: kp_cert
  });
  console.log(cert.getPEM());

  await jsonfile.write_json(FILE_BASE + STATE_FNAME, { sequence_no: sequence_no });

  return {
    cert: Buffer.from(cert.getEncodedHex(), 'hex'),
    sequence_no: sequence_no
  }
}

#ソースコード

##M5StickC側

#include <M5StickC.h>
#include <SPI.h>
#include <Wire.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include "BLE2902.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <ArduinoECCX08.h>
#include <base64.hpp>

#define LGFX_AUTODETECT
#include <LovyanGFX.hpp>

const char *wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char *wifi_password = "【WiFiアクセスポイントのパスワード】";

const char *endpoint_u2f_certificate = "【FIDOサーバのホスト名】/device/u2f_certificate";
const char *endpoint_u2f_register = "【FIDOサーバのホスト名】/device/u2f_register";
const char *endpoint_u2f_authenticate = "【FIDOサーバのホスト名】/device/u2f_authenticate";
const char *endpoint_u2f_version = "【FIDOサーバのホスト名】/device/u2f_version";

#define SERVICE_UUID_fido BLEUUID((uint16_t)0xfffd)
#define CHARACTERISTIC_UUID_fidoControlPoint "F1D0FFF1-DEAA-ECEE-B42F-C9BA7ED623BB"
#define CHARACTERISTIC_UUID_fidoStatus "F1D0FFF2-DEAA-ECEE-B42F-C9BA7ED623BB"
#define CHARACTERISTIC_UUID_fidoControlPointLength "F1D0FFF3-DEAA-ECEE-B42F-C9BA7ED623BB"
#define CHARACTERISTIC_UUID_fidoServiceRevisionBitfield "F1D0FFF4-DEAA-ECEE-B42F-C9BA7ED623BB"
#define CHARACTERISTIC_UUID_fidoServiceRevision BLEUUID((uint16_t)0x2A28)

#define SERVICE_UUID_DeviceInfo BLEUUID((uint16_t)0x180a)
#define CHARACTERISTIC_UUID_ManufacturerName BLEUUID((uint16_t)0x2A29)
#define CHARACTERISTIC_UUID_ModelNumber BLEUUID((uint16_t)0x2A24)
#define CHARACTERISTIC_UUID_FirmwareRevision BLEUUID((uint16_t)0x2A26)

#define DEVICE_NAME "Fido2Gateway"
#define BLE_PASSKEY 123456
#define DISCONNECT_WAIT 3000

static LGFX lcd;

bool connected = false;

const int capacity = 1024;
StaticJsonDocument<capacity> json_request;
StaticJsonDocument<capacity> json_response;
char json_buffer[1024];
unsigned short recv_len = 0;
unsigned short expected_len = 0;
unsigned char expected_slot = 0;
unsigned char recv_buffer[1024];

#define PACKET_BUFFER_SIZE 20

BLECharacteristic *pCharacteristic_fidoControlPoint;
BLECharacteristic *pCharacteristic_fidoStatus;
BLECharacteristic *pCharacteristic_fidoControlPointLength;
BLECharacteristic *pCharacteristic_fidoServiceRevisionBitfield;
BLECharacteristic *pCharacteristic_fidoServiceRevision;

uint8_t value_fidoControlPoint[PACKET_BUFFER_SIZE] = {0x00};
uint8_t value_fidoStatus[PACKET_BUFFER_SIZE] = {0x00};
uint8_t value_fidoControlPointLength[2] = {(PACKET_BUFFER_SIZE >> 8) & 0xff, PACKET_BUFFER_SIZE}; /* Length PACKET_BUFFER_SIZE */
uint8_t value_fidoServiceRevisionBitfield[1] = {0x80};                                            /* Version 1.1 */
uint8_t value_fidoServiceRevision[3] = {0x31, 0x2e, 0x30};                                        /* "1.0" */
uint8_t value_appearance[2] = {0x40, 0x03};

BLEAdvertising *g_pAdvertising = NULL;

#define DEFAULT_KEY_SLOT  2
#define DEFAULT_DATA_SLOT 8
#define DEFAULT_COUNTER_SLOT 1
#define PAYLOAD_BUFFER_LENGTH 1024
uint8_t payload[PAYLOAD_BUFFER_LENGTH];
#define APPLICATION_LENGTH  32
#define CHALLENGE_LENGTH    32
#define HASH_LENGTH         32
#define RAW_SIGNATURE_LENGTH  64
#define SERIAL_LENGTH       12
#define PUBLICKEY_LENGTH    (1 + 64)
#define KEYHANDLE_LENGTH  (4 + SERIAL_LENGTH)

long doHttpPost(String url, JsonDocument *p_input, JsonDocument *p_output);
long auth_prepare(void);
long make_sha256(const unsigned char *p_input, int input_length, unsigned char *p_hash);
long set_signature(const unsigned char *raw_signature, unsigned char *p_payload);
int process_register(const uint8_t *challenge, const uint8_t *application);
int process_authenticate(uint8_t control, const uint8_t *challenge, const uint8_t *application, uint8_t keyHandle_length, const uint8_t *keyHandle);
int process_version(void);

void dump_bin(const char *p_message, const uint8_t *p_bin, unsigned short len)
{
  Serial.printf("%s", p_message);
  for (unsigned short i = 0; i < len; i++){
    Serial.printf("%02x ", p_bin[i]);
  }
  Serial.println("");
}

void lcd_println(const char* p_message, bool clear = true){
  if( clear ){
    lcd.fillScreen();
    lcd.setCursor(0, 0);
  }

  lcd.println(p_message);
}

class MyCallbacks : public BLEServerCallbacks
{
  void onConnect(BLEServer *pServer)
  {
    connected = true;
    Serial.println("Connected\n");
    lcd_println("BLE Connected");
  }

  void onDisconnect(BLEServer *pServer)
  {
    connected = false;
    BLE2902 *desc = (BLE2902 *)pCharacteristic_fidoStatus->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
    desc->setNotifications(false);
    Serial.println("Disconnected\n");
    lcd_println("BLE Disconnected");

    g_pAdvertising->stop();
    delay(DISCONNECT_WAIT);
    g_pAdvertising->start();
    lcd_println("BLE Advertising");
  }
};

class MySecurity : public BLESecurityCallbacks
{
  bool onConfirmPIN(uint32_t pin)
  {
    Serial.println("onConfirmPIN number:");
    Serial.println(pin);
    return false;
  }

  uint32_t onPassKeyRequest()
  {
    Serial.println("onPassKeyRequest");
    return BLE_PASSKEY;
  }

  void onPassKeyNotify(uint32_t pass_key)
  {
    // ペアリング時のPINの表示
    Serial.println("onPassKeyNotify number");
//    Serial.println(pass_key);
  }

  bool onSecurityRequest()
  {
    /* ペアリング要否 */
    Serial.println("onSecurityRequest");
    return true;
  }

  void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl)
  {
    Serial.println("onAuthenticationComplete");
    if (cmpl.success)
    {
      // ペアリング完了
      Serial.println("auth success");
      lcd_println("Auth Success");
    }
    else
    {
      // ペアリング失敗
      Serial.println("auth failed");
      lcd_println("Auth Failed");
    }
  }
};

long auth_prepare(void)
{
  if (!ECCX08.begin()) {
    Serial.println("No ECCX08 present!");
    return -1;
  }

  if (!ECCX08.locked()) {
    Serial.println("ECCX08 not locked");
    return -1;
  }else{
    Serial.println("ECCX08 locked");
  }

  return 0;
}

long make_keyHandle(unsigned long sequence_no, const unsigned char *serial_no, unsigned char *p_keyHandle)
{
  // ToDo keyhandle may be random
  p_keyHandle[0] = (sequence_no >> 24) & 0xff;
  p_keyHandle[1] = (sequence_no >> 16) & 0xff;
  p_keyHandle[2] = (sequence_no >> 8) & 0xff;
  p_keyHandle[3] = (sequence_no) & 0xff;

  memmove(&p_keyHandle[4], serial_no, SERIAL_LENGTH);

  return 0;
}

long check_keyHandle(const unsigned char *p_keyHandle, unsigned char keyHandle_length, const unsigned char *serial_no)
{
  if( keyHandle_length != KEYHANDLE_LENGTH || memcmp(serial_no, &p_keyHandle[4], SERIAL_LENGTH) != 0 )
    return -1;
  return 0;
}

long make_sha256(const unsigned char *p_input, int input_length, unsigned char *p_hash)
{
  int ret = ECCX08.beginSHA256();
  if( !ret ){
    Serial.println("ECCX08.beginSHA256 Error");
    return -1;
  }
  for( int index = 0 ; index < input_length ; index += 64){
    if( (input_length - index) <= 64 ){
      ECCX08.endSHA256((const byte*)&p_input[index], input_length - index, p_hash);
      if( !ret ){
        Serial.println("ECCX08.endSHA256 Error");
        return -1;
      }
      break;
    }else{
      ECCX08.updateSHA256((const byte*)&p_input[index]);
      if( !ret ){
        Serial.println("ECCX08.updateSHA256 Error");
        return -1;
      }
    }
  }

  return 0;
}

long set_signature(const unsigned char *raw_signature, unsigned char *p_payload)
{
  unsigned char total_len = 0x44;
  int index = 0;
  p_payload[index++] = 0x30;
  index++;
  p_payload[index++] = 0x02;
  if( raw_signature[0] >= 0x80 ){
    p_payload[index++] = 33;
    p_payload[index++] = 0x00;
    total_len++;
  }else{
    p_payload[index++] = 32;
  }
  memmove(&p_payload[index], &raw_signature[0], 32);
  index += 32;

  p_payload[index++] = 0x02;
  if( raw_signature[32] >= 0x80 ){
    p_payload[index++] = 33;
    p_payload[index++] = 0x00;
    total_len++;
  }else{
    p_payload[index++] = 32;
  }
  memmove(&p_payload[index], &raw_signature[32], 32);
  index += 32;

  p_payload[1] = total_len;

  return 2 + total_len;
}

int process_register(const uint8_t *challenge, const uint8_t *application)
{
  Serial.println("process_register");

  long ret = auth_prepare();
  if( ret != 0 ){
    Serial.println("auth_prepare error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  uint8_t serial[SERIAL_LENGTH];
  ret = ECCX08.serialNumber(serial);
  if( !ret ){
    Serial.println("serialNumber error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  uint8_t publicKey[PUBLICKEY_LENGTH];
  publicKey[0] = 0x04;
  ret = ECCX08.generatePublicKey(DEFAULT_KEY_SLOT, &publicKey[1]);
  if( !ret ){
    Serial.println("ECCX08.generatePublicKey Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  json_request.clear();
  int length = encode_base64_length(PUBLICKEY_LENGTH);
  unsigned char *buffer = (unsigned char*)malloc(length + 1);
  encode_base64(publicKey, PUBLICKEY_LENGTH, buffer);
  buffer[length] = '\0';
  json_request["pubkey"] = (const char*)buffer;

  ret = doHttpPost(endpoint_u2f_certificate, &json_request, &json_response);
  free(buffer);
  if( ret != 0 ){
    Serial.println("doHttpPost Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  uint8_t keyHandle[KEYHANDLE_LENGTH];
  unsigned long sequence_no = json_response["result"]["sequence_no"];
  ret = make_keyHandle(sequence_no, serial, keyHandle);
  if( ret != 0 ){
    Serial.println("make_keyHandle Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  int index = 0;
  payload[index++] = 0x05;
  memmove(&payload[index], publicKey, PUBLICKEY_LENGTH);
  index += PUBLICKEY_LENGTH;

  payload[index++] = KEYHANDLE_LENGTH;
  memmove(&payload[index], keyHandle, KEYHANDLE_LENGTH);
  index += KEYHANDLE_LENGTH;

  const char* cert = json_response["result"]["cert"];
  length = decode_base64_length((unsigned char*)cert);
  decode_base64((unsigned char *)cert, &payload[index]);
  index += length;

  uint8_t input[1 + APPLICATION_LENGTH + CHALLENGE_LENGTH + KEYHANDLE_LENGTH + PUBLICKEY_LENGTH];
  input[0] = 0x00;
  memmove(&input[1], application, APPLICATION_LENGTH);
  memmove(&input[1 + APPLICATION_LENGTH], challenge, CHALLENGE_LENGTH);
  memmove(&input[1 + APPLICATION_LENGTH + CHALLENGE_LENGTH], keyHandle, KEYHANDLE_LENGTH);
  memmove(&input[1 + APPLICATION_LENGTH + CHALLENGE_LENGTH + KEYHANDLE_LENGTH], publicKey, PUBLICKEY_LENGTH);

  unsigned char hash[HASH_LENGTH];
  ret = make_sha256(input, sizeof(input), hash);
  if( ret != 0 ){
    Serial.println("make_sha256 Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  unsigned char signature[RAW_SIGNATURE_LENGTH];
  ret = ECCX08.ecSign(DEFAULT_KEY_SLOT, hash, signature);
  if( !ret ){
    Serial.println("ECCX08.ecSign Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  ret = set_signature(signature, &payload[index]);
  index += ret;

  payload[index++] = 0x90;
  payload[index++] = 0x00;

  return index;
}

int process_authenticate(uint8_t control, const uint8_t *challenge, const uint8_t *application, uint8_t keyHandle_length, const uint8_t *keyHandle)
{
  Serial.println("process_authenticate");

  long ret = auth_prepare();
  if( ret != 0 ){
    Serial.println("auth_prepare error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  uint8_t serial[SERIAL_LENGTH];
  ret = ECCX08.serialNumber(serial);
  if( !ret ){
    Serial.println("serialNumber error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  ret = check_keyHandle(keyHandle, keyHandle_length, serial);
  if( ret != 0 ){
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  if( control == 0x07 ){
    payload[0] = 0x69;
    payload[1] = 0x85;
    return 2;
  }

  unsigned long counter;
  ret = ECCX08.countUp(DEFAULT_COUNTER_SLOT, &counter);
  if( !ret ){
    Serial.println("ECCX08.readSlot Error");
    while (1);
  }

  uint8_t userPresence = 0x01;

  unsigned char buffer[APPLICATION_LENGTH + 1 + 4 + CHALLENGE_LENGTH];
  memmove(&buffer[0], application, APPLICATION_LENGTH);
  buffer[APPLICATION_LENGTH] = userPresence;
  buffer[APPLICATION_LENGTH + 1] = (counter >> 24) & 0xff;
  buffer[APPLICATION_LENGTH + 1 + 1] = (counter >> 16) & 0xff;
  buffer[APPLICATION_LENGTH + 1 + 2] = (counter >> 8) & 0xff;
  buffer[APPLICATION_LENGTH + 1 + 3] = (counter) & 0xff;
  memmove(&buffer[APPLICATION_LENGTH + 1 + 4], challenge, CHALLENGE_LENGTH);

  unsigned char hash[HASH_LENGTH];
  ret = make_sha256(buffer, sizeof(buffer), hash);
  if( ret != 0 ){
    Serial.println("make_sha256 Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  unsigned char signature[RAW_SIGNATURE_LENGTH];
  ret = ECCX08.ecSign(DEFAULT_KEY_SLOT, hash, signature);
  if( !ret ){
    Serial.println("ECCX08.ecSign Error");
    payload[0] = 0x6a;
    payload[1] = 0x80;
    return 2;
  }

  int index = 0;
  payload[index++] = userPresence;
  payload[index++] = (counter >> 24) & 0xff;
  payload[index++] = (counter >> 16) & 0xff;
  payload[index++] = (counter >> 8) & 0xff;
  payload[index++] = (counter) & 0xff;
  ret = set_signature(signature, &payload[index]);
  index += ret;

  payload[index++] = 0x90;
  payload[index++] = 0x00;

  return index;
}

int process_version(void)
{
  Serial.println("process_version");

  const static char version[] = "U2F_V2";
  int index = 0;
  memmove(&payload[index], version, strlen(version));
  index += strlen(version);

  payload[index++] = 0x90;
  payload[index++] = 0x00;

  return index;
}

class MyCharacteristicCallbacks : public BLECharacteristicCallbacks
{
  void onWrite(BLECharacteristic *pCharacteristic)
  {
    uint8_t *value = pCharacteristic->getData();
    std::string str = pCharacteristic->getValue();

    dump_bin("onWrite : ", value, str.length());

    if (expected_len > 0 && value[0] != expected_slot)
      expected_len = 0;

    if (expected_len == 0){
      if (value[0] != 0x83)
        return;
      recv_len = 0;
      expected_len = (value[1] << 8) | value[2];
      memmove(&recv_buffer[recv_len], &value[3], str.length() - 3);
      recv_len += str.length() - 3;
      expected_slot = 0;
      if (recv_len < expected_len)
        return;
    }
    else
    {
      memmove(&recv_buffer[recv_len], &value[1], str.length() - 1);
      recv_len += str.length() - 1;
      expected_slot++;
      if (recv_len < expected_len)
        return;
    }
    expected_len = 0;

    int resp_len = 0;
    switch(recv_buffer[1]){
      case 0x01:{
        resp_len = process_register(&recv_buffer[7], &recv_buffer[7 + 32]);
        if( resp_len < 0 ){
          Serial.println("process_registrater Error");
          return;
        }
        break;
      }
      case 0x02:{
        resp_len = process_authenticate(recv_buffer[2], &recv_buffer[7], &recv_buffer[7 + 32], recv_buffer[7 + 32 + 32], &recv_buffer[7 + 32 + 32 + 1]);
        if( resp_len < 0 ){
          Serial.println("process_authenticate Error");
          return;
        }
        break;
      }
      case 0x03:{
        resp_len = process_version();
        if( resp_len < 0 ){
          Serial.println("process_version Error");
          return;
        }
        break;
      }
      default:{
        Serial.println("Unknown INS");
        lcd_println("unknown INS");
        payload[0] = 0x6a;
        payload[1] = 0x80;
        resp_len = 2;
        break;
      }
    }

    int offset = 0;
    int slot = 0;
    int packet_size = 0;
    do{
      if (offset == 0){
        value_fidoStatus[0] = 0x83;
        value_fidoStatus[1] = (resp_len >> 8) & 0xff;
        value_fidoStatus[2] = resp_len & 0xff;
        packet_size = resp_len - offset;
        if (packet_size > (PACKET_BUFFER_SIZE - 3))
          packet_size = PACKET_BUFFER_SIZE - 3;
        memmove(&value_fidoStatus[3], &payload[offset], packet_size);

        dump_bin("Notify : ", value_fidoStatus, packet_size + 3);

        pCharacteristic_fidoStatus->setValue(value_fidoStatus, packet_size + 3);
        pCharacteristic_fidoStatus->notify(true);

        offset += packet_size;
        packet_size += 3;
      }else{
        value_fidoStatus[0] = slot++;
        packet_size = resp_len - offset;
        if (packet_size > (PACKET_BUFFER_SIZE - 1))
          packet_size = PACKET_BUFFER_SIZE - 1;
        memmove(&value_fidoStatus[1], &payload[offset], packet_size);

        dump_bin("Notify : ", value_fidoStatus, packet_size + 1);

        pCharacteristic_fidoStatus->setValue(value_fidoStatus, packet_size + 1);
        pCharacteristic_fidoStatus->notify(true);

        offset += packet_size;
        packet_size += 1;
      }
    } while (packet_size >= PACKET_BUFFER_SIZE);

    lcd_println("process end", false);
  }
};

void taskServer(void *)
{
  BLEDevice::init(DEVICE_NAME);
  /* ESP_BLE_SEC_ENCRYPT_MITM, ESP_BLE_SEC_ENCRYPT */
  BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM);
  BLEDevice::setSecurityCallbacks(new MySecurity());

  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyCallbacks());

  BLESecurity *pSecurity = new BLESecurity();
  pSecurity->setKeySize(16);
  //  pSecurity->setStaticPIN(BLE_PASSKEY);

  /* ESP_LE_AUTH_NO_BOND, ESP_LE_AUTH_BOND, ESP_LE_AUTH_REQ_MITM */
  //  pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_MITM);
  pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);

  /* for fixed passkey */
  uint32_t passkey = BLE_PASSKEY;
  esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t));

  /* ESP_IO_CAP_IN, ESP_IO_CAP_OUT, ESP_IO_CAP_KBDISP */
  pSecurity->setCapability(ESP_IO_CAP_OUT);
  //  pSecurity->setCapability(ESP_IO_CAP_IN);
  pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);

  BLEService *pService = pServer->createService(SERVICE_UUID_fido);

  pCharacteristic_fidoControlPoint = pService->createCharacteristic(
      CHARACTERISTIC_UUID_fidoControlPoint,
      BLECharacteristic::PROPERTY_WRITE);
  pCharacteristic_fidoControlPoint->setAccessPermissions(ESP_GATT_PERM_WRITE /* ESP_GATT_PERM_WRITE_ENCRYPTED */);
  pCharacteristic_fidoControlPoint->setValue(value_fidoControlPoint, sizeof(value_fidoControlPoint));
  pCharacteristic_fidoControlPoint->setCallbacks(new MyCharacteristicCallbacks());

  pCharacteristic_fidoStatus = pService->createCharacteristic(
      CHARACTERISTIC_UUID_fidoStatus,
      BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic_fidoStatus->addDescriptor(new BLE2902());

  pCharacteristic_fidoControlPointLength = pService->createCharacteristic(
      CHARACTERISTIC_UUID_fidoControlPointLength,
      BLECharacteristic::PROPERTY_READ);
  pCharacteristic_fidoControlPointLength->setAccessPermissions(ESP_GATT_PERM_READ);
  pCharacteristic_fidoControlPointLength->setValue(value_fidoControlPointLength, sizeof(value_fidoControlPointLength));

  pCharacteristic_fidoServiceRevisionBitfield = pService->createCharacteristic(
      CHARACTERISTIC_UUID_fidoServiceRevisionBitfield,
      BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
  pCharacteristic_fidoServiceRevisionBitfield->setAccessPermissions(ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE);
  pCharacteristic_fidoServiceRevisionBitfield->setValue(value_fidoServiceRevisionBitfield, sizeof(value_fidoServiceRevisionBitfield));

  pCharacteristic_fidoServiceRevision = pService->createCharacteristic(
      CHARACTERISTIC_UUID_fidoServiceRevision,
      BLECharacteristic::PROPERTY_READ);
  pCharacteristic_fidoServiceRevision->setAccessPermissions(ESP_GATT_PERM_READ);
  pCharacteristic_fidoServiceRevision->setValue(value_fidoServiceRevision, sizeof(value_fidoServiceRevision));

  pService->start();

  BLECharacteristic *pCharacteristic;

  BLEService *pService_DeviceInfo = pServer->createService(SERVICE_UUID_DeviceInfo);

  pCharacteristic = pService_DeviceInfo->createCharacteristic(
      CHARACTERISTIC_UUID_ManufacturerName,
      BLECharacteristic::PROPERTY_READ);
  pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ);
  pCharacteristic->setValue("SampleModel");

  pCharacteristic = pService_DeviceInfo->createCharacteristic(
      CHARACTERISTIC_UUID_ModelNumber,
      BLECharacteristic::PROPERTY_READ);
  pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ);
  pCharacteristic->setValue("M1.0");

  pCharacteristic = pService_DeviceInfo->createCharacteristic(
      CHARACTERISTIC_UUID_FirmwareRevision,
      BLECharacteristic::PROPERTY_READ);
  pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ);
  pCharacteristic->setValue("F1.0");

  pService_DeviceInfo->start();

  g_pAdvertising = pServer->getAdvertising();
  g_pAdvertising->addServiceUUID(SERVICE_UUID_fido);
  g_pAdvertising->start();

  lcd_println("BLE Advertising");

  vTaskDelay(portMAX_DELAY); //delay(portMAX_DELAY);
}

void setup()
{
  M5.begin(true, true, true);
  Serial.begin(115200);
  Serial.println("Starting setup");

  lcd.init();
  lcd.setRotation(1);
  lcd.setBrightness(128);
  lcd.fillScreen();
  lcd.setTextSize(2);

  Serial.printf("capacity=%d\n", capacity);

  WiFi.begin(wifi_ssid, wifi_password);
  Serial.println("Connecting to Wifi AP...");
  lcd_println("WiFi Connecting");
  while (WiFi.status() != WL_CONNECTED){
    delay(1000);
    Serial.print(".");
  }
  Serial.println(WiFi.localIP());
  lcd_println("WiFi Connected", false);

  Serial.println("Starting BLE work!");
  xTaskCreate(taskServer, "server", 30000, NULL, 5, NULL);
}

void loop()
{
  if (connected){
    // do something
  }
}

long doHttpPost(String url, JsonDocument *p_input, JsonDocument *p_output)
{
  Serial.println(url);
  HTTPClient http;
  http.begin(url);
  http.addHeader("Content-Type", "application/json");

  Serial.println("http.POST");
  size_t len;
  len = serializeJson(*p_input, json_buffer, sizeof(json_buffer));
  if (len < 0 || len >= sizeof(json_buffer)){
    Serial.println("Error: serializeJson");
    return -1;
  }
  Serial.println(json_buffer);
  int status_code = http.POST((uint8_t *)json_buffer, len);
  Serial.printf("status_code=%d\r\n", status_code);
  if (status_code != 200){
    http.end();
    return status_code;
  }

  Stream *resp = http.getStreamPtr();
  DeserializationError err = deserializeJson(*p_output, *resp);
  http.end();

  if (err){
    Serial.println("Error: deserializeJson");
    Serial.println(err.f_str());
    return -1;
  }

  return 0;
}

##Node.js側

X.509証明書を作成するエンドポイント「/device/u2f_certificate」を追加しています。

'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const jsonfile = require(HELPER_BASE + 'jsonfile-utils');

const rs = require('jsrsasign');
const crypto = require('crypto');
const fsp = require('fs').promises;
const fs = require('fs');

const FIDO_ISSUER = process.env.FIDO_ISSUER || 'FT FIDO 0200';
const FIDO_SUBJECT = process.env.FIDO_SUBJECT || 'FT FIDO P200';
const FIDO_EXPIRE = Number(process.env.FIDO_EXPIRE) || 3650;
const FIDO_EXPIRE_START = process.env.FIDO_EXPIRE_START || '210620150000Z'; // 形式:YYMMDDHHMMSSZ (UTC時間)

const FILE_BASE = process.env.THIS_BASE_PATH + '/data/fido2_device/';
const PRIV_FNAME = "privkey.pem";
const STATE_FNAME = "state.json";

var kp_cert;
var sequence_no = 0;

(async () => {
  // X509証明書の楕円暗号公開鍵ペアの作成
  if (fs.existsSync(FILE_BASE + PRIV_FNAME)) {
    var pem = await fsp.readFile(FILE_BASE + PRIV_FNAME);
    kp_cert = rs.KEYUTIL.getKey(pem.toString());
  } else {
    var kp = rs.KEYUTIL.generateKeypair('EC', 'secp256r1');
    kp_cert = kp.prvKeyObj;
    fsp.writeFile(FILE_BASE + PRIV_FNAME, rs.KEYUTIL.getPEM(kp_cert, "PKCS1PRV"));
  }
  
  // シーケンス番号の復旧
  var state = await jsonfile.read_json(FILE_BASE + STATE_FNAME, { sequence_no: 0 } );
  sequence_no = state.sequence_no;
})();

exports.handler = async (event, context, callback) => {
  if (event.path == "/device/u2f_certificate") {
    var body = JSON.parse(event.body);
    var result = await u2f_certificate(Buffer.from(body.pubkey, 'base64'));

    return new Response({
      result: {
        cert: result.cert.toString('base64'),
        sequence_no: result.sequence_no
      }
    });
  }else
  if (event.path == "/device/u2f_register") {
    var body = JSON.parse(event.body);
    console.log(body);

    var input = Buffer.from(body.input, 'hex');
    var result = await u2f_register(input.subarray(7, 7 + 32), input.subarray(7 + 32, 7 + 32 + 32));

    return new Response({
      result: Buffer.concat([result, Buffer.from([0x90, 0x00])]).toString('hex')
    });
  } else
    if (event.path == "/device/u2f_authenticate") {
      var body = JSON.parse(event.body);
      console.log(body);

      var input = Buffer.from(body.input, 'hex');
      try {
        var control = input[2];
        var challenge = input.subarray(7, 7 + 32);
        var application = input.subarray(7 + 32, 7 + 32 + 32);
        var keyHandle = input.subarray(7 + 32 + 32 + 1, 7 + 32 + 32 + 1 + input[7 + 32 + 32]);
        var result = await u2f_authenticate(control, challenge, application, keyHandle);
      } catch (sw) {
        return new Response({
          result: sw.toString('hex')
        });
      };

      return new Response({
        result: Buffer.concat([result, Buffer.from([0x90, 0x00])]).toString('hex')
      });
    } else
      if (event.path == "/device/u2f_version") {
        var result = await u2f_version();
        return new Response({
          result: Buffer.concat([result, Buffer.from([0x90, 0x00])]).toString('hex')
        });
      }
};

async function u2f_certificate(pubKey) {
  var pubKeyHex = pubKey.toString('hex');
  var pubKeyObj = rs.KEYUTIL.getKey({
    'xy': pubKeyHex,
    'curve': 'secp256r1'
  });

  //サブジェクトキー識別子
  const ski = rs.KJUR.crypto.Util.hashHex(pubKeyHex, 'sha1');
  const derSKI = new rs.KJUR.asn1.DEROctetString({ hex: ski });

  // X.509証明書の作成
  sequence_no++;
  var cert = new rs.KJUR.asn1.x509.Certificate({
    version: 3,
    serial: { int: sequence_no },
    issuer: { str: "/CN=" + FIDO_ISSUER },
    notbefore: FIDO_EXPIRE_START,
    notafter: toUTCString(new Date(Date.now() + FIDO_EXPIRE * 24 * 60 * 60 * 1000)),
    subject: { str: "/CN=" + FIDO_SUBJECT + ("0000000000" + sequence_no).slice(-10) },
    sbjpubkey: pubKeyObj, // can specify public key object or PEM string
    sigalg: "SHA256withECDSA",
    ext: [
      {
        //サブジェクトキー識別子
        extname: "subjectKeyIdentifier",
        kid: {
//          hex: derSKI.getEncodedHex()
          hex: ski
        }
      },
      {
        // FIDO U2F certificate transports extension
        extname: "1.3.6.1.4.1.45724.2.1.1",
        extn: "03020640"
      }
    ],
    cakey: kp_cert
  });
  console.log(cert.getPEM());

  await jsonfile.write_json(FILE_BASE + STATE_FNAME, { sequence_no: sequence_no });

  return {
    cert: Buffer.from(cert.getEncodedHex(), 'hex'),
    sequence_no: sequence_no
  }
}


async function u2f_register(challenge, application) {
  console.log('application=', application.toString('hex'));

  // 楕円暗号公開鍵ペアの作成
  var kp = rs.KEYUTIL.generateKeypair('EC', 'secp256r1');
  console.log(kp.pubKeyObj.pubKeyHex);
  var userPublicKey = Buffer.from(kp.pubKeyObj.pubKeyHex, 'hex');

  // 内部管理用のKeyIDの決定
  sequence_no++;
  var uuid = crypto.randomBytes(16);
  uuid[0] = (sequence_no >> 24) & 0xff;
  uuid[1] = (sequence_no >> 16) & 0xff;
  uuid[2] = (sequence_no >> 8) & 0xff;
  uuid[3] = (sequence_no) & 0xff;
  var key_id = uuid.toString('hex');
  console.log('key_id=' + key_id);

  await writeCertFile(key_id, {
    application: application.toString('hex'),
    privkey: rs.KEYUTIL.getPEM(kp.prvKeyObj, "PKCS1PRV"),
    counter: 1,
    created_at: new Date().getTime()
  });

  // KeyHandleの作成
  var keyHandle = Buffer.concat([uuid]);
  var keyLength = Buffer.from([keyHandle.length]);

  //サブジェクトキー識別子
  const ski = rs.KJUR.crypto.Util.hashHex(kp.pubKeyObj.pubKeyHex, 'sha1');
  const derSKI = new rs.KJUR.asn1.DEROctetString({ hex: ski });

  // X.509証明書の作成
  var cert = new rs.KJUR.asn1.x509.Certificate({
    version: 3,
    serial: { int: sequence_no },
    issuer: { str: "/CN=" + FIDO_ISSUER },
    notbefore: FIDO_EXPIRE_START,
    notafter: toUTCString(new Date(Date.now() + FIDO_EXPIRE * 24 * 60 * 60 * 1000)),
    subject: { str: "/CN=" + FIDO_SUBJECT + ("0000000000" + sequence_no).slice(-10) },
    sbjpubkey: kp.pubKeyObj, // can specify public key object or PEM string
    sigalg: "SHA256withECDSA",
    ext: [
      {
        //サブジェクトキー識別子
        extname: "subjectKeyIdentifier",
        kid: {
//          hex: derSKI.getEncodedHex()
          hex: ski
        }
      },
      {
        // FIDO U2F certificate transports extension
        extname: "1.3.6.1.4.1.45724.2.1.1",
        extn: "03020640"
      }
    ],
    cakey: kp_cert
  });
  console.log(cert.getPEM());

  var attestationCert = Buffer.from(cert.getEncodedHex(), 'hex');

  // 署名の生成
  var input = Buffer.concat([
    Buffer.from([0x00]),
    application,
    challenge,
    keyHandle,
    userPublicKey
  ]);
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(input);
  var signature = sign.sign(rs.KEYUTIL.getPEM(kp.prvKeyObj, "PKCS1PRV"));

  console.log('userPublicKey(' + userPublicKey.length + ')=' + userPublicKey.toString('hex'));
  console.log('keyHandle(' + keyHandle.length + ')=' + keyHandle.toString('hex'));
  console.log('attestationCert(' + attestationCert.length + ')=' + attestationCert.toString('hex'));
  console.log('signature(' + signature.length + ')=' + signature.toString('hex'));

  // レスポンスの生成(concat)
  return Buffer.concat([
    Buffer.from([0x05]),
    userPublicKey,
    keyLength,
    keyHandle,
    attestationCert,
    signature
  ]);
}

async function u2f_authenticate(control, challenge, application, keyHandle) {
  console.log('control=', control);
  console.log('application=', application.toString('hex'));

  var userPresence = Buffer.from([0x01]);

  // 内部管理用のKeyIDの抽出
  var key_id = keyHandle.slice(0, 16).toString('hex');
  console.log('key_id=' + key_id);
  if (!checkAlnum(key_id)) {
    console.log('key_id invalid')
    throw Buffer.from([0x6a, 0x80]);
  }

  var cert = await readCertFile(key_id);
  if (!cert) {
    console.log('key_id not found');
    throw Buffer.from([0x6a, 0x80]);
  }

  if (cert.application.toLowerCase() != application.toString('hex').toLowerCase()) {
    console.log('application mismatch');
    throw Buffer.from([0x6a, 0x80]);
  }

  if (control == 0x07) {
    throw Buffer.from([0x69, 0x85]);
  }

  // 署名回数カウンタのインクリメント
  cert.counter++;
  cert.lastauthed_at = new Date().getTime();
  await writeCertFile(key_id, cert);
  console.log('counter=' + cert.counter);
  var counter = Buffer.from([(cert.counter >> 24) & 0xff, (cert.counter >> 16) & 0xff, (cert.counter >> 8) & 0xff, cert.counter & 0xff])

  // 署名生成
  var input = Buffer.concat([
    application,
    userPresence,
    counter,
    challenge
  ]);
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(input);
  var signature = sign.sign(cert.privkey);

  console.log('input(' + input.length + ')=' + input.toString('hex'));
  console.log('sigunature(' + signature.length + ')=' + signature.toString('hex'));

  // verify sample code
  /*
    const verify = crypto.createVerify('RSA-SHA256')
    verify.write(input)
    verify.end();
  
    var result =  verify.verify(
      privateKey.asPublic().toPEM(),
      signature
    );
    console.log('verify result=' + result);
  */

  // レスポンスの生成(concat)
  return Buffer.concat([
    userPresence,
    counter,
    signature
  ]);
}

async function u2f_version() {
  var version = Buffer.from('U2F_V2');
  return Promise.resolve(version);
}

function toUTCString(date) {
  var year = date.getUTCFullYear();
  var month = date.getUTCMonth() + 1;
  var day = date.getUTCDate();
  var hour = date.getUTCHours();
  var minutes = date.getUTCMinutes();
  var seconds = date.getUTCSeconds();

  return to2d(year % 100) + to2d(month) + to2d(day) + to2d(hour) + to2d(minutes) + to2d(seconds) + "Z";
}

function to2d(num) {
  if (num < 10)
    return '0' + String(num);
  else
    return String(num);
}

function checkAlnum(str) {
  var ret = str.match(/([a-z]|[A-Z]|[0-9])/gi);
  return (ret.length == str.length)
}

async function readCertFile(uuid) {
  try {
    var file = await fsp.readFile(FILE_BASE + uuid + '.json', 'utf8');
    return JSON.parse(file);
  } catch (error) {
    console.log(error);
    return null;
  }
}

async function writeCertFile(uuid, cert) {
  await fsp.writeFile(FILE_BASE + uuid + '.json', JSON.stringify(cert, null, 2), 'utf8');
}

#終わりに

以下の課題は解決されました!(2022/1/30)
・なぜ今まで動いていたのに、今回に限ってAWS管理コンソールでの利用ではじかれるのかわからない。。。(←解決済み)
・X.509証明書の拡張領域の作り方がわかれば、M5StickCだけで実現できるのになあ。あと少しか。。。(←解決済み)

#参考

以下の投稿を参考にさせていただきました。ありがとうございました。
 OpenSSLで楕円曲線暗号を使う

以上

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?