ssh-keygen でセキュリティキーを使った鍵生成から Attestation 検証まで

# 1. 概要
OpenSSH のバージョン8.2 から yubikey などのセキュリティキーを使った ssh の鍵生成及び、認証が行えるようになっています。この記事では、

- yubikey を使った ssh 鍵生成
- 鍵フォーマット
- 生成した鍵の Attestation 検証

について、見ていこうと思います。

## 1.1 環境

```zsh
% openssl version
OpenSSL 1.1.1i  8 Dec 2020
% ssh -V
OpenSSH_8.4p1, OpenSSL 1.1.1i  8 Dec 2020
```
mac に元から入っている ssh は対応していないバージョンなので、 homebrew などでインストールして OpenSSH verion 8.2 以上のものを利用してください。

また、セキュリティキーとして Yubikey 5c Nano (firmware: 5.1.2)　を利用しています。

## 1.2 構成
2章ではとりあえず鍵生成からログインまで一連の流れを追っていきます。
3章ではセキュリティキーを使った鍵生成をすると何が嬉しいのかについて詳しくみていきます。まずは秘密鍵の秘密にすべき情報がセキュリティキー外部に漏れていないことを鍵のフォーマットから確認します。さらに、サーバが登録要求された公開鍵の素性を検証できることを Attestation という仕組みを用いて確認します。
4章でまとめて終わりです。

# 2. とりあえず試してみる
まずはセキュリティキーを使って 

1. ssh鍵生成を行い、
1. 生成した公開鍵をサーバへ登録、
1. そして登録したサーバへのログインを行ってみます。

セキュリティキーを使ってはいますが、やっていることは普段と変わらないことがわかると思います。

## 2.1 鍵生成
[man ssh-keygen](https://man.openbsd.org/ssh-keygen) によると `-t` で生成する鍵タイプを指定し、 `-f` で生成した秘密鍵の保存先ファイルを指定できます。今回は、主題であるセキュリティキーを使った鍵生成を行いたいため、 `-t ecdsa-sk` を指定しました。 なお yubikey 5c は `ed25519-sk` の鍵タイプに対応していません。

```zsh
% ssh-keygen -t ecdsa-sk -f id
Generating public/private ecdsa-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter PIN for authenticator:
```
鍵生成を開始すると、 Yubikey の PIN を入力するように促されます。これは僕が事前に Yubikey に対して FIDO2 用の PIN コードを設定しているからであり、設定していない場合は PIN コードの入力がスキップされます。
正しい PIN コードを入力すると yubikey をタッチするように促されます。
今回は、 秘密鍵ファイルのパスフレーズは設定しません。 

```
You may need to touch your authenticator (again) to authorize key generation.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id
Your public key has been saved in id.pub
The key fingerprint is:
SHA256:mEkKAVsin2lzeqI6DyLwwjfH7kxv/KcnVeudhXaIjfw hatake@hatake-lab-MBP.local
```

鍵生成が完了すると、２つのファイル(`id` と `id.pub`)が生成されていることが確認できます。
セキュリティキーを使わない場合と全く同じですね。

## 2.2 サーバの用意と公開鍵の登録
あとは、サーバ側の設定を行います。
もちろん、サーバ側も OpenSSHv8.2 以上であることが必要です。ありがたいことに Ubuntu20.04 はこれを満たします。
今回は Docker を用いて簡易的にサーバを用意することにしました。
id.pub の中身をいずれかの手段を使って `/root/.ssh/authorized_keys` に登録しておいてください。

```dockerfile
FROM ubuntu:20.04

RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
EXPOSE 22

RUN mkdir -m 700 /root/.ssh
RUN echo "<あなたの作った id.pub の中身>" > /root/.ssh/authorized_keys
RUN chmod 600 /root/.ssh/authorized_keys

CMD ["/usr/sbin/sshd", "-D"]
```

## 2.3 ログイン
ssh でログインできることを確認します。

```zsh
% docker build -t ssh-server .
% docker run -d -p 22222:22 ssh-server
b6167570477cde7fe127c049cda503dbcdec1acf6eaeee37f558c0f473737997
% ssh -p 22222 -i id root@localhost
The authenticity of host '[localhost]:22222 ([::1]:22222)' can't be established.
ECDSA key fingerprint is SHA256:tq7LVj8y7HrbJRjXBFbElVlIPL0Ytzv2RBaTRLw4IAI.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[localhost]:22222' (ECDSA) to the list of known hosts.
Confirm user presence for key ECDSA-SK 
<yubikey をタップ>
...<省略>
root@b6167570477c:~#
```
Yubikey をタップするとログインに成功しました！
こうして、普段と同じように Yubikey のようなセキュリティキーを使った ssh ログインができることを確認しました。

## 2.4 Clean up
docker container の停止・削除及び作成した docker image の削除を行います。
また、 ローカル端末の .ssh/known_hosts に [localhost]:22222 が追加されてしまったのでそれを削除しておきます。

```zsh
% docker stop b6
% docker rm b6
% docker rmi ssh-server
% ssh-keygen -R '[localhost]:22222'
```

# 3. セキュリティキーの本領を発揮させていく
前章ではセキュリティキーを使った ssh 鍵生成からログインまで一通り試してみました。
しかし、前章で説明しただけでは普通に鍵生成した場合とあまりかわらず、セキュリティキーを使うとどんないいことがあるのか見えていません。

そこで、次節からは次のことを行います。

1. 生成した鍵ペアに書かれている情報
    - 秘密鍵がセキュリティキーに格納されていると言っていましたが、前章では秘密鍵ファイル `id` が生成されていました。そこには秘密にしておくべき情報が見える形で格納されていないことを確認します。
1. Attestation の検証
    - 登録先のサーバが、ユーザの言い分を信頼する以外の方法でその公開鍵が確かにセキュリティキー上で作成されたことを確認する仕組みです。この仕組みを使うことでサーバは公開鍵の素性を検証することができます。

## 3.1 鍵ペアのフォーマット 
前章では、秘密鍵 `id` と公開鍵 `id.pub` の鍵ペアが生成されファイルに保存されていました。
この節では生成された鍵のフォーマットを元にどんな情報がそのファイルたちに格納されているか確認していきます。

そして、秘密鍵のファイルに秘密にすべき情報が誰でも見れる状態で格納されていないことを確認します。

### 3.1.1 公開鍵のフォーマット

```zsh
% cat id.pub
sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBPKQ2Dk0PLFigJneqkS44pgvqr/cJA6d9zap1/dVIAnuntPHLyHkxFkfP3QAyUnlJtceUjHmBQypa+cr06E5+YYAAAAEc3NoOg== hatake@hatake-lab-MBP.local
```
`ssh-keygen` で作成した公開鍵は OpenSSH 独自のフォーマットとなっています。 
[OpenSSH の sshd が要求する鍵フォーマット](https://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT)に合わせているようです。
SSH の公開鍵のフォーマットは [RFC4716](https://datatracker.ietf.org/doc/html/rfc4716)に定義されていて、変換は `ssh-keygen -e` で行えます。

```zsh
% ssh-keygen -e -m rfc4716 -f id.pub
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "256-bit ECDSA-SK, converted by hatake@hatake-lab-MBP.local f"
AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAA
BBBPKQ2Dk0PLFigJneqkS44pgvqr/cJA6d9zap1/dVIAnuntPHLyHkxFkfP3QAyUnlJtce
UjHmBQypa+cr06E5+YYAAAAEc3NoOg==
---- END SSH2 PUBLIC KEY ----
```
x509 の PEM みたいな感じですが、ダッシュが４つしかなかったり空白があったりと[少し異なっています](https://datatracker.ietf.org/doc/html/rfc4716#section-3.5)。
公開鍵の本体は、 Comment 行を除いたダッシュで囲まれた部分で、 [base64 でエンコードされています](https://datatracker.ietf.org/doc/html/rfc4716#section-3.4)。
試しに、公開鍵の本体だけを取り出して base64 decode してみます。 

(注: [Comment 行は `\` で改行できてしまう](https://datatracker.ietf.org/doc/html/rfc4716#section-3.3)ので以下のコマンドでは適切に抽出できない場合があります）

```zsh
% ssh-keygen -e -m rfc4716 -f id.pub | sed -e '/^----/d' -e '/^Comment/d' | openssl enc -base64 -d | xxd
00000000: 0000 0022 736b 2d65 6364 7361 2d73 6861  ..."sk-ecdsa-sha
00000010: 322d 6e69 7374 7032 3536 406f 7065 6e73  2-nistp256@opens
00000020: 7368 2e63 6f6d 0000 0008 6e69 7374 7032  sh.com....nistp2
00000030: 3536 0000 0041 04f2 90d8 3934 3cb1 6280  56...A....94<.b.
00000040: 99de aa44 b8e2 982f aabf dc24 0e9d f736  ...D.../...$...6
00000050: a9d7 f755 2009 ee9e d3c7 2f21 e4c4 591f  ...U ...../!..Y.
00000060: 3f74 00c9 49e5 26d7 1e52 31e6 050c a96b  ?t..I.&..R1....k
00000070: e72b d3a1 39f9 8600 0000 0473 7368 3a    .+..9......ssh:
```
早速中身を確認していきましょう。
SSH は　ASN.1 DER とはまた違った[独自のフォーマット](https://datatracker.ietf.org/doc/html/rfc4251#section-5) があります。
例えば string は先頭４byte で文字列の長さを表します。 上の例では、先頭の `00　00　00　22` で 34bytes の文字列(`sk-ecdsa-...@openssh.com`)が続きます。　そして次の `0x00000008` で 8bytes の文字列 (`nistp256`) が続いています。

ecdsa-sk の[公開鍵フォーマット](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f#L64) に基づいてパースしてみると次のような情報が入っています。

- public key format identifier: `sk-ecdsa-sha2-nistp256@openssh.com`
- curve name: `nistp256`
- Q_x: `f290d839343cb1628099deaa44b8e2982faabfdc240e9df736a9d7f7552009ee`
- Q_y: `9ed3c72f21e4c4591f3f7400c949e526d71e5231e6050ca96be72bd3a139f986`
- application: `ssh:`

ecdsa の公開鍵と比較してみるとほぼ同じです。
これは、セキュリティキーを使った場合でも公開鍵として公開する情報が変わらないためです。

```zsh
% ssh-keygen -t ecdsa -f id
% ssh-keygen -e -m rfc4716 -f id.pub | sed -e '/^----/d' -e '/^Comment/d' | openssl enc -base64 -d | xxd
00000000: 0000 0013 6563 6473 612d 7368 6132 2d6e  ....ecdsa-sha2-n
00000010: 6973 7470 3235 3600 0000 086e 6973 7470  istp256....nistp
00000020: 3235 3600 0000 4104 fc84 6f1a 9259 5cca  256...A...o..Y\.
00000030: 354b caaa 720f 1ca2 bf87 9a72 1411 5960  5K..r......r..Y`
00000040: 338a 956f a77b 5bb0 f786 69b9 bf3e cd5e  3..o.{[...i..>.^
00000050: cf5b b054 de44 f0e0 2de7 721f 9c38 09b1  .[.T.D..-.r..8..
00000060: 454b 045a bc6f 80b7                      EK.Z.o..
```

- public key format identifier: `ecdsa-sha2-nistp256`
- curve name: `nistp256`
- Q_x: `fc846f1a92595cca354bcaaa720f1ca2bf879a7214115960338a956fa77b5bb0`
- Q_y: `f78669b9bf3ecd5ecf5bb054de44f0e02de7721f9c3809b1454b045abc6f80b7`


ちなみに公開鍵のハッシュは `ssh-keygen -l` で取ることができますが、実体は次の通りに公開鍵のハッシュをとっています。
公開鍵の本体に当たる部分を base64 decode して、 sha256 でハッシュをとり、 base64 encode しているものと同じであることがわかります。

```zsh
% ssh-keygen -l -f id.pub
256 SHA256:mEkKAVsin2lzeqI6DyLwwjfH7kxv/KcnVeudhXaIjfw hatake@hatake-lab-MBP.local (ECDSA-SK)
% ssh-keygen -e -m rfc4716 -f id.pub | sed -e '/^----/d' -e '/^Comment/d' | openssl enc -base64 -d | openssl dgst -sha256 -binary | openssl enc -base64
mEkKAVsin2lzeqI6DyLwwjfH7kxv/KcnVeudhXaIjfw=
```


### 3.1.2 秘密鍵のフォーマット
秘密鍵は[OpenSSH 独自のフォーマット](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key#L8) で定められています。

```zsh
% cat id | sed -e '/^----/d' | openssl enc -base64 -d | xxd
00000000: 6f70 656e 7373 682d 6b65 792d 7631 0000  openssh-key-v1..
00000010: 0000 046e 6f6e 6500 0000 046e 6f6e 6500  ...none....none.
00000020: 0000 0000 0000 0100 0000 7f00 0000 2273  .............."s
00000030: 6b2d 6563 6473 612d 7368 6132 2d6e 6973  k-ecdsa-sha2-nis
00000040: 7470 3235 3640 6f70 656e 7373 682e 636f  tp256@openssh.co
00000050: 6d00 0000 086e 6973 7470 3235 3600 0000  m....nistp256...
00000060: 4104 f290 d839 343c b162 8099 deaa 44b8  A....94<.b....D.
00000070: e298 2faa bfdc 240e 9df7 36a9 d7f7 5520  ../...$...6...U
00000080: 09ee 9ed3 c72f 21e4 c459 1f3f 7400 c949  ...../!..Y.?t..I
00000090: e526 d71e 5231 e605 0ca9 6be7 2bd3 a139  .&..R1....k.+..9
000000a0: f986 0000 0004 7373 683a 0000 00f0 ca84  ......ssh:......
000000b0: e497 ca84 e497 0000 0022 736b 2d65 6364  ........."sk-ecd
000000c0: 7361 2d73 6861 322d 6e69 7374 7032 3536  sa-sha2-nistp256
000000d0: 406f 7065 6e73 7368 2e63 6f6d 0000 0008  @openssh.com....
000000e0: 6e69 7374 7032 3536 0000 0041 04f2 90d8  nistp256...A....
000000f0: 3934 3cb1 6280 99de aa44 b8e2 982f aabf  94<.b....D.../..
00000100: dc24 0e9d f736 a9d7 f755 2009 ee9e d3c7  .$...6...U .....
00000110: 2f21 e4c4 591f 3f74 00c9 49e5 26d7 1e52  /!..Y.?t..I.&..R
00000120: 31e6 050c a96b e72b d3a1 39f9 8600 0000  1....k.+..9.....
00000130: 0473 7368 3a01 0000 0040 e7bf c685 0651  .ssh:....@.....Q
00000140: 75b1 afd6 039f b82f 6b45 6e72 c1a2 cfbf  u....../kEnr....
00000150: 15ee d8f4 3dc7 220c 4523 66f7 b3bc 7ef3  ....=.".E#f...~.
00000160: c1ce 757f de94 9ded f330 7aa0 5711 2b3f  ..u......0z.W.+?
00000170: 77ad 7c87 dc9f 222a 8c7b 0000 0000 0000  w.|..."*.{......
00000180: 001b 6861 7461 6b65 4068 6174 616b 652d  ..hatake@hatake-
00000190: 6c61 622d 4d42 502e 6c6f 6361 6c01       lab-MBP.local.
```

パースしてみると次の情報が入っていることがわかります。

- AUTH_MAGIC: `openssh-key-v1`
- ciphername, kdfname: `none`
- kdfoptions: `<空文字>`
- number of keys N: `1`
- publickey1: 長さが 0x7f の公開鍵
- encrypted, padded list of private keys: 長さ 0xf0 の残り

今回は、秘密鍵ファイルに対して暗号化を行なっていないので、 ciphername & kdfname が `none` =(0x`0000 046e 6f6e 65`) で指定されており、 kdfoptions が空文字になっています。
そして、 number of keys N が `00 0000 01` となっているのでこの秘密鍵ファイルには鍵が１つだけ入っていることがわかります。
公開鍵が埋め込まれており、確認すると id.pub と全く同じ情報が入っていることがわかります。

```zsh 
% diff <(cat id | sed -e '/^----/d' | openssl enc -base64 -d | dd bs=1 skip=43 count=$((16#7f))) <(ssh-keygen -e -m rfc4716 -f id.pub | sed -e '/^----/d' -e '/^Comment/d' | openssl enc -base64 -d)
```

encrypted, padded list of private keys　はフォーマットが [PROTOCOL.key#L30](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key#L30) に書いてあります。 
さらに、今回利用している ecdsa-sk の鍵フォーマットは [PROTOCOL.u2f#L71](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f#L71) に書いてあります。これに基づいて秘密鍵をパースしてみると

- `sk-ecdsa-sha2-nistp256@openssh.com`
- curve name: `nistp256`
- Q_x: `f290d839343cb1628099deaa44b8e2982faabfdc240e9df736a9d7f7552009ee`
- Q_y: `9ed3c72f21e4c4591f3f7400c949e526d71e5231e6050ca96be72bd3a139f986`
- application: `ssh:`
- flags: `01`
- key_handle 64bytes: `e7bfc685065175b1afd6039fb82f6b456e72c1a2cfbf15eed8f43dc7220c452366f7b3bc7ef3c1ce757fde949dedf3307aa057112b3f77ad7c87dc9f222a8c7b`

上５つは公開鍵と同じ情報ですね(同じ情報がありすぎでは...)。 
flags については [sk-api.h](https://github.com/openssh/openssh-portable/blob/master/sk-api.h#L26) に書いてあるように、 openssh 独自のフラグのようです。今回は UP (User Presense) のみ要求していることになります。

key_handle は FIDO U2F で使われる言葉です。
僕が FIDO U2F に関して十分に理解できていないのですが、公開鍵の識別子と考えて良いと思います。
この識別子から yubikey は対応する秘密鍵を理解できます。

ここで注目しておきたいのは、秘密鍵のファイルと言いながら、秘密にしておかないといけない情報がないということです。
比較として `ssh-keygen -t ecdsa` をしたときの秘密鍵を見てみます。
秘密鍵を PEM に変換して、 openssl を使って中身を見てみると確かに秘密鍵が存在しています。

```zsh
% ssh-keygen -t ecdsa -f id
% ssh-keygen -e -f id -m pem -p
% openssl ec -in id -text -noout
read EC key
Private-Key: (256 bit)
priv:
    e0:94:ca:a8:b1:c8:08:c0:c5:e6:b5:46:67:f8:cc:
    b1:e9:25:0c:40:9a:5b:3a:51:e9:e2:3b:41:eb:38:
    42:a4
pub:
    04:fc:84:6f:1a:92:59:5c:ca:35:4b:ca:aa:72:0f:
    1c:a2:bf:87:9a:72:14:11:59:60:33:8a:95:6f:a7:
    7b:5b:b0:f7:86:69:b9:bf:3e:cd:5e:cf:5b:b0:54:
    de:44:f0:e0:2d:e7:72:1f:9c:38:09:b1:45:4b:04:
    5a:bc:6f:80:b7
ASN1 OID: prime256v1
NIST CURVE: P-256
```

### 3.1.3 Yubikey は秘密鍵をどこに保存するか？
先ほど見た、key_handle についてさらに深掘りしてみます。
yubikey のような小さな端末は限られたストレージなので、 yubikey は端末に秘密鍵を格納していません。
代わりに、 yubikey の端末ごとにユニークな master key を使って秘密鍵を暗号化(AES256-CCM)して、外部に保存します。
秘密鍵の暗号化に使う鍵はその yubikey でしか利用できないため、たとえ外部に保存していたとしても(暗号アルゴリズムが安全であると言えれば)安全であるといえます。
暗号化された秘密鍵はメタデータ含めて 64bytes となっており、先述の key_handle と同じサイズです。
参考: https://developers.yubico.com/U2F/Protocol_details/Key_generation.html

## 3.2 Attestation の検証
yubikey には生成した鍵が確かに yubikey 内部で生成され、外部に漏洩しないことを Attestation という形で公開鍵を受け取ったサーバが検証できる仕組みがあります。
`ssh-keygen -O write-attestation` を使って yubikey が生成した attestation を受け取ることができます。

challenge と呼ばれるランダムバイナリを用意してから、 attestation 付きの鍵生成を行います。
セキュリティキーはこのチャレンジが含まれたデータに対して署名を行い、リプレイ攻撃を防ぎます。
指定しない場合は、 ssh-keygen がよしなにランダム生成します。

```zsh
% dd if=/dev/random bs=1 count=32 of=challenge
% ssh-keygen -t ecdsa-sk -f id -O challenge=challenge -O write-attestation=attestation
Generating public/private ecdsa-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter PIN for authenticator:
You may need to touch your authenticator (again) to authorize key generation.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id
Your public key has been saved in id.pub
The key fingerprint is:
SHA256:zzKdlZrv5ZqRj1EO1ldgxSUrj2sojbQOhB1E9BSKi10 hatake@hatake-lab-MBP.local
Your FIDO attestation certificate has been saved in attestation
```
attestation の検証を行いたいのですが、下記の通り検証は自分で頑張る必要があります。

> OpenSSH treats the attestation certificate and enrollment signatures as opaque objects and does no interpretation of them itself.
> 訳) OpenSSH は attestation 証明書と署名を opaque objects (不透明なオブジェクト)として扱い、それらを解釈しません。
> https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f#L173

検証の流れは次の通りです。

1. Attestation Object をパースする
1. 署名を検証する

### 3.2.1 Attestation object のパース
https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f#L151 にしたがってパースをしていきます。

```zsh
# string		"ssh-sk-attest-v01"
% START=0; LEN=$((16#$(dd if=attestation bs=1 skip=$START count=4 | xxd -p))); START=$(($START + 4))
% dd if=attestation bs=1 skip=$START count=$LEN
# string		attestation certificate
% START=$(($START + $LEN)); LEN=$((16#$(dd if=attestation bs=1 skip=$START count=4 | xxd -p))); START=$(($START + 4))
% dd if=attestation bs=1 skip=$START count=$LEN of=attestation.crt
# string		enrollment signature
% START=$(($START + $LEN)); LEN=$((16#$(dd if=attestation bs=1 skip=$START count=4 | xxd -p))); START=$(($START + 4))
% dd if=attestation bs=1 skip=$START count=$LEN of=signature
# string		authenticator data (CBOR encoded)
% START=$(($START + $LEN)); LEN=$((16#$(dd if=attestation bs=1 skip=$START count=4 | xxd -p))); START=$(($START + 4))
% dd if=attestation bs=1 skip=$START count=$LEN of=authData
```
### 3.2.2 署名を検証する
試行錯誤した結果、 WebAuthn Packed Attestation Statement Format に従って yubikey は attestation を生成しているようです。
ここに記述されている Verification Procedure に基づいて検証を行なっていきます。

#### 署名対象のデータを用意する
WebAuthn Packed Attestation Statement は `authenticatorData | ClientDataHash` を署名対象のデータとしています。

 ssh-keygen については[コード](https://github.com/openssh/openssh-portable/blob/V_8_4/sk-usbhid.c#L723)を読む限り入力として与えたチャレンジをそのまま clientDataHash として扱っているようです。
従って、 clientDataHash を次のように用意します。

```zsh
% cp challenge clientDataHash # challenge は ssh-keygen -O challenge で指定したもの
```

(注意：[openssh の master ブランチ](https://github.com/openssh/openssh-portable/commit/59a194825f12fff8a7f75d91bf751ea17645711b#diff-7015d136e2c1d9deb413f5689032b83abc25be0c8ea8a340665d09b62ef4599c)ではここ最近、 チャレンジに対して sha256 したものを clientDataHash として扱うように変更が加えられています。 変更の理由として、`ssh-keygen -O challenge` 与えるランダム列が 32bytes でないとエラーになってしまう現状を変えたいようです。　実際、[WebAuthn は challenge の長さ](https://www.w3.org/TR/webauthn/#sctn-cryptographic-challenges) について最低 16bytes と言っているので、この変更は必要な気がします。)

そして、 authenticatorData は前述の attestation パースで得ています。
しかし、これは CBOR エンコードされているため少し変更が必要です。 
`xxd -p authData | tr -d "\n"` したものを [cbor.me](http://cbor.me/?bytes=58c4e30610e8a162115960fe1ec223e6529c9f4b6e80200dcb5e5c321c8af1e2b1bf4500000072cb69481e8ff7403993ec0a2729a154a80040d74d83656407604f92774caadf8c758833c68e8d4e1af312ee0388b76bfc29b2246fcd243063abd04c0f936e76e074794d824f82eebdfa8dc24b21ac5af4880fa5010203262001215820ec7c646640ce2d20b4ba568f9380354ac685b7f3d316c8a702a37c5f026698aa225820ca0d20d16ccb48552aae338bfc6b9e1e36f131a7bb5bccad19b3d6f29a79ae97) で見てみると、 authenticatorData の実体は先頭2byteを除いた残りであることがわかります。今回はこれを authenticatorData として扱います。
従って、 authenticatorData を次のように用意します。

```zsh
% dd if=authData bs=1 skip=2 of=authenticatorData
```

署名検証までもう少しです。　
先ほど作った二つをつなぎ合わせて、署名対象となるデータを用意します。

```zsh
% cat authenticatorData > tbsData
% cat clientDataHash >> tbsData
```

#### 署名検証に使う鍵を用意する
次は署名検証に使う鍵ですが、これは attestation certificate に含まれています。取り出しておきます。

```zsh
% openssl x509 -in attestation.crt -inform DER -noout -pubkey -out attestation.crt.pubkey
```

#### 署名を検証する
検証を行います。

```zsh
% openssl dgst -sha256 -verify attestation.crt.pubkey -signature signature tbsData
Verified OK
```

検証に成功です！
ここまでで、 signature が確かに attestation certificate に対応する秘密鍵を使って署名されたことが検証できました。 
しかし、これだけで検証は終わりではありません。

#### attestation certificate の requirements 検証
まずは、 attestation certifiacte がこの用途で使われる証明書であるか検証します。 [証明書の requirements](https://www.w3.org/TR/webauthn/#sctn-packed-attestation-cert-requirements) にマッチしているか確認してみます。

- version 3 であること。 (x509 では 0x2 が version 3 を表します)
- subject field も OK
- 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) が存在しているので、 AAGUID が yubikey 5c nano となじか確認 -> OK
    - [Yubikey の AAGUID](https://support.yubico.com/hc/en-us/articles/360016648959-YubiKey-Hardware-FIDO2-AAGUIDs) によると、マッチしていることが確認できます。
    - certificate は AAGUID が OCTET STRING でラップされているので `04 10` 以降が 16 (=0x10) bytes の AAGUID を表します。
- Basic Constraints が `false` になっていること

```zsh
% openssl x509 -in attestation.crt -inform DER -noout -certopt no_pubkey,no_sigdump,ext_dump -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 413943488 (0x18ac46c0)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Yubico U2F Root CA Serial 457200631
        Validity
            Not Before: Aug  1 00:00:00 2014 GMT
            Not After : Sep  4 00:00:00 2050 GMT
        Subject: C = SE, O = Yubico AB, OU = Authenticator Attestation, CN = Yubico U2F EE Serial 413943488
        X509v3 extensions:
            1.3.6.1.4.1.41482.2:
                0000 - 31 2e 33 2e 36 2e 31 2e-34 2e 31 2e 34   1.3.6.1.4.1.4
                000d - 31 34 38 32 2e 31 2e 37-                 1482.1.7

            1.3.6.1.4.1.45724.2.1.1:
                0000 - 03 02 05 20                              ...

            1.3.6.1.4.1.45724.1.1.4:
                0000 - 04 10 cb 69 48 1e 8f f7-40 39 93 ec 0a   ...iH...@9...
                000d - 27 29 a1 54 a8                           ').T.

            X509v3 Basic Constraints: critical
                CA:FALSE
```

#### AAGUID が一致していることの検証
次に、 authenticatorData に含まれる aaguid と certificate の aaguid が一緒か確認します。

#### attestation certificate のチェーン検証
attestation certificate が Yubico 社の Root CA が署名した証明書であるかどうかを検証します。 RootCA は `https://developers.yubico.com/U2F/yubico-u2f-ca-certs.txt` にあります。 
検証には、 [`openssl verify`](https://www.openssl.org/docs/man1.1.1/man1/openssl-verify.html) を用いました。 
正しく検証できたということが出力されていますね。

```zsh
% wget https://developers.yubico.com/U2F/yubico-u2f-ca-certs.txt
% openssl x509 -in attestation.crt -inform DER | openssl verify -CAfile yubico-u2f-ca-certs.txt
stdin: OK
```

これで、 attestation が Yubico 社を起点とする Trust chain で検証できました。
提示された attestation が yubikey で作られたと強く信頼できます。

#### 生成した公開鍵と attestation の対象とする公開鍵が同じものか検証する
WebAuhthn であれば attestation に含まれる公開鍵をそのまま登録するので検証は以上となりますが、 `ssh-keygen` では別に公開鍵が生成されているため attestation がその公開鍵を対象にしたものであるか追加で検証する必要があります。

authenticatorData をパースして、 credentialPublicKey を抽出してみます。
ssh-keygen の attestation output から CBOR encode された authenticatorData を取得しているのでそれをパースしていきます。

```zsh
% dd if=authData bs=1 skip=121 of=authData.credPubKey
% xxd -p authData.credPubKey | tr -d "\n"
a5010203262001215820ec7c646640ce2d20b4ba568f9380354ac685b7f3d316c8a702a37c5f026698aa225820ca0d20d16ccb48552aae338bfc6b9e1e36f131a7bb5bccad19b3d6f29a79ae97
```

これを cbor.me でデコードすると、次が得られます。

```
{
  1(kty): 2(EC2), 3(alg): -7(ES256), -1(crv): 1(P-256), 
  -2(x coordinate): h'EC7C646640CE2D20B4BA568F9380354AC685B7F3D316C8A702A37C5F026698AA', 
  -3(y coordinate): h'CA0D20D16CCB48552AAE338BFC6B9E1E36F131A7BB5BCCAD19B3D6F29A79AE97'
}
```

これを公開鍵と比較しましょう。 
この 2バイト目から 32 bytes が x coordicate, 次の 32bytes が y coordinate となっているため、同じであることがわかります。

```zsh
% cat id.pub | cut -d " " -f2 | base64 -d | xxd -s 54
00000036: 04ec 7c64 6640 ce2d 20b4 ba56 8f93 8035  ..|df@.- ..V...5
00000046: 4ac6 85b7 f3d3 16c8 a702 a37c 5f02 6698  J..........|_.f.
00000056: aaca 0d20 d16c cb48 552a ae33 8bfc 6b9e  ... .l.HU*.3..k.
00000066: 1e36 f131 a7bb 5bcc ad19 b3d6 f29a 79ae  .6.1..[.......y.
00000076: 9700 0000 0473 7368 3a                   .....ssh:
``` 
こうして、 attestation の内容と public key の内容が一致していることが検証できました。

長かったですが、これで attestation を正しく検証ができ、登録されるサーバは公開鍵の素性を検証することができます。

# 4. まとめ
OpenSSHv8.2 から yubikey のようなセキュリティキーを使ったSSH鍵生成からログインまでできることを確認しました。
また、生成された鍵の中身を見ていくことで、セキュリティキーを使った鍵生成では秘密鍵ファイルに秘密にすべき情報が見れる形で保存されていないことを確認しました。
さらに、Attestation 機能を使うことで受け取った公開鍵が確かにセキュリティキーで生成されたことを検証できることを確認しました。

今回は紹介しませんでしたが、 ssh-keygen では[さまざまなオプション](https://man.openbsd.org/ssh-keygen#O)が利用可能です。軽く紹介しておきます。

- `-O no-touch-required`: 秘密鍵を利用する際に、ユーザがセキュリティキーをタッチ (user presense)することを要求しないオプションです。
- `-O resident`: 秘密鍵をサーバに暗号化して保存するのではなく、セキュリティキー自身に保存しておく resident key を生成するオプションです。セキュリティキーによっては対応してません。
  - `-O application=`: domain-specific な resident key を生成できます。デフォルトは `ssh:` です。
  - `-O user=`: resident key でユーザ名をしておくことができます。

しかし、見てきたように attestation の検証をやっていくのは正直しんどいのではないかと思います。 ssh のコマンド体系で検証できるようになるといいですね。
なお、 yubikey 5 series では ssh-keygen -t ecdsa-sk 以外にも  [PGP](https://developers.yubico.com/PGP/SSH_authentication/) や [PIV](https://developers.yubico.com/PIV/Guides/SSH_with_PIV_and_PKCS11.html) など別の方法を使って ssh ログインできます。それぞれメリットデメリットがあるので場合に応じて使っていくと面白いと思います。