libfido2を理解する その１


最近、iOSデバイス搭載の生体認証をWebAuthnで使うためにはどうしたら良いのかを調べています。
WebAuthn自体の情報はネットに山ほどありますが、残念ながら外部認証機器を連携させるためのCTAPについての情報はあまり見つかりません。
FIDOの推進企業であるYubicoがgithubで公開しているlibfido2の内部を理解できれば、方法が分かるのではないかと思い勉強をしています。

自分の備忘と同じ悩みを持っている人のために得た情報を残します。

# libfido2のビルド
ビルド環境: macOS 10.14
pkgマネージャ: homebrew
Xcodeバージョン: 10.2

- ソースコードをgithubより入手する
 
```$ git clone https://github.com/Yubico/libfido2.git```

- homebrewよりopenssl@1.1、pkg-config、cmakeをインストール

```$ brew install openssl@1.1 pkg-config cmake```

- クローンしたlibfido2へ移動し下記のコマンドを実行

```$ sh .travis/build-osx-clang```

※このときにXcodeのコマンドラインツールがインストールされていなければインストール確認が入ります

# libfido2 exampleコマンドの実行

libfido2をビルドすると、build/example配下にlibfido2を利用したサンプルプログラムができます。
もしも、Yubikeyなどの認証機器を持っていたらMacにそれを指した状態でコマンドを実行するとWebAuthnで規定されたCredencialの発行および検証をコマンドラインから行うことができます。
（ここでの目的はlibfido2の中身の理解のため、それぞれのサンプルプログラムの使い方については触れません）

また、プログラムの内部を理解する上でデバッグログを表示させたいのでサンプルの実行前にデバッグフラグ環境変数を定義しておきます。

```$ export FIDO_DEBUG=1```

次に、接続されているFIDOデバイスを一覧表示するmanifestを実行してみます、Yubikeyを接続した状態でコマンド実行すると下記のような情報が出力されます。

```
$ examples/manifest
IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS01@14100000/YubiKey OTP+FIDO+CCID@14100000/IOUSBHostInterface@1/AppleUserUSBHostHIDDevice: vendor=0x1050, product=0x0407 (Yubico YubiKey OTP+FIDO+CCID)
```

# manifestの中身を理解する

manifest.cの中身をみるとやっていることは単純で、fido_dev_info_manifest()で取得した情報を一覧表示しているだけです。

``` C
int
main(void)
{
	fido_dev_info_t	*devlist;
	size_t		 ndevs;
	int		 r;

	fido_init(0);

	if ((devlist = fido_dev_info_new(64)) == NULL)
		errx(1, "fido_dev_info_new");

	if ((r = fido_dev_info_manifest(devlist, 64, &ndevs)) != FIDO_OK)
		errx(1, "fido_dev_info_manifest: %s (0x%x)", fido_strerr(r), r);

	for (size_t i = 0; i < ndevs; i++) {
		const fido_dev_info_t *di = fido_dev_info_ptr(devlist, i);
		printf("%s: vendor=0x%04x, product=0x%04x (%s %s)\n",
		    fido_dev_info_path(di),
		    (uint16_t)fido_dev_info_vendor(di),
		    (uint16_t)fido_dev_info_product(di),
		    fido_dev_info_manufacturer_string(di),
		    fido_dev_info_product_string(di));
	}

	fido_dev_info_free(&devlist, ndevs);

	exit(0);
}
```

肝心のfido_dev_info_manifest()の中身をみると、IOKit経由でHIDデバイスを一覧表示させているだけのようです。
尚、IOHIDManagerSetDeviceMatchingの第二引数でNULLを指定していますが、NULLを指定すると現在Macに接続されている全てのHIDデバイスを検索します。
ここで[IOHIDKeys.h](https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-421.6/IOHIDFamily/IOHIDKeys.h.auto.html)に定義されているエレメントキーをキーに実際のHIDデバイスのProductIDの値を指定してやればそのデバイスのみを検索するようになります。

``` C
int
fido_dev_info_manifest(fido_dev_info_t *devlist, size_t ilen, size_t *olen)
{
	IOHIDManagerRef	manager = NULL;
	CFSetRef	devset = NULL;
	CFIndex		devcnt;
	IOHIDDeviceRef *devs = NULL;
	int		r = FIDO_ERR_INTERNAL;

	*olen = 0;

	if (ilen == 0)
		return (FIDO_OK); /* nothing to do */

	if (devlist == NULL)
		return (FIDO_ERR_INVALID_ARGUMENT);

	if ((manager = IOHIDManagerCreate(kCFAllocatorDefault,
	    kIOHIDManagerOptionNone)) == NULL) {
		log_debug("%s: IOHIDManagerCreate", __func__);
		goto fail;
	}

	IOHIDManagerSetDeviceMatching(manager, NULL);

	if ((devset = IOHIDManagerCopyDevices(manager)) == NULL) {
		log_debug("%s: IOHIDManagerCopyDevices", __func__);
		goto fail;
	}

	if ((devcnt = CFSetGetCount(devset)) < 0) {
		log_debug("%s: CFSetGetCount", __func__);
		goto fail;
	}

	if ((devs = calloc(devcnt, sizeof(*devs))) == NULL) {
		log_debug("%s: calloc", __func__);
		goto fail;
	}

	CFSetGetValues(devset, (void *)devs);

	for (CFIndex i = 0; i < devcnt; i++) {
		if (copy_info(&devlist[*olen], devs[i]) == 0) {
			if (++(*olen) == ilen)
				break;
		}
	}

	r = FIDO_OK;
fail:
	if (manager != NULL)
		CFRelease(manager);
	if (devset != NULL)
		CFRelease(devset);

	free(devs);

	return (r);
}
```

# FIDOデバイスの判断方法

ここでひとつの疑問が湧きます。
先程のコマンド結果を見て分かる通り、manifestの結果ではFIDOデバイスしか表示されません。
通常であれば ```$ system_profiler SPUSBDataType``` を実行した時に表示される他のデバイスも表示されるはずです。
つまり、どこかでFIDOデバイスの判断をしていることになります。

調べてみたところ、copy_info()の中でis_fido()という関数が呼ばれその中で

- レポートサイズが64と定義されている
- レポートディスクリプタのusage_pageが0xf1d0となっている

HIDデバイスをFIDOデバイスと判断していました。
これについてはCTAPのプロトコル仕様の「[8.1.8.2. HID report descriptor and device discovery](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#usb-discovery)」にしっかり情報が定義されていました。


``` C
static bool
is_fido(IOHIDDeviceRef dev)
{
	uint32_t	usage_page;
	int32_t		report_len;

	if (get_int32(dev, CFSTR(kIOHIDMaxInputReportSizeKey),
	    &report_len) < 0 || report_len != REPORT_LEN - 1) {
		log_debug("%s: unsupported report len", __func__);
		return (false);
	}

	return (get_int32(dev, CFSTR(kIOHIDPrimaryUsagePageKey),
	    (int32_t *)&usage_page) == 0 && usage_page == 0xf1d0);
}
```

ここの情報はかなり重要です。もし、iPhoneを接続した時にMac側に条件を満たしたHIDデバイスだと認識して貰えればFIDOの認証機器として扱ってくれると思われます。