FIDO2によるChromeとMacBook Proのtouch idを利用したパスワードレスログインの実装

# はじめに
ChromeとMacBook Proを利用したFIDO2によるパスワードレスなログインの実装について説明します。

ここではFIDO2が何かについては言及しませんのであらかじめご了承ください。
とりあえずFIDO2（Web Authentication）で遊びたい方、FIDO2について知りたい方、FIDO2のフローやパラメーターについて知りたい方、FIDO2の実装について知りたい方向けのリンクをそれぞれいくつか貼っておくのでよければご覧ください。

**とりあえずFIDO2（Web Authentication）で遊びたい方**
https://webauthn.me/
https://webauthn.org/
https://demo.yubico.com/webauthn-technical/registration

**FIDO2（Web Authentication）が何か知りたい方**
https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
https://gihyo.jp/dev/column/newyear/2019/webauthn
https://speakerdeck.com/ynojima/webauthn-in-a-nutshell-ntt-tech-conf-number-3-ja

**FIDO2（Web Authentication）の詳しいフローやパラメーターについて知りたい方**
https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn/
https://medium.com/@herrjemand/introduction-to-webauthn-api-5fd1fb46c285

**実装について知りたい方**
https://slides.com/fidoalliance/jan-2018-fido-seminar-webauthn-tutorial#/
https://www.slideshare.net/techblogyahoo/fido-124019677

# 実装
## 検証環境
Chrome 72.0.3626.119
MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)

## Attestationの検証
ChromeとMacBook Proのtouch idを利用したWeb Authenticationの場合、最低でも以下のどちらかのattestation formatに対応している必要があります。

* "none" attestation
* SELF "packed" attestation

これはChromeが返すattestation formatがSELF "packed" attestationであるためです。
"packed"attestationについては以下の記事に詳細に書かれておりますので、こちらをご覧いただくと良いでしょう。
https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn-attestation-packed/
https://medium.com/@herrjemand/verifying-fido2-packed-attestation-a067a9b2facd
一応上記のattestationについて説明をしていきます。

### "none" attestation
こちらはRPがattestaion情報を受け取ることを希望しないときのフォーマットになります。
具体的にはnavigator.credentials.create()を呼び出す際、以下のようにattestationというオプションに対してnoneを指定することでattestation format"none"のレスポンスが返ってきます。

```javascript

    {
        "status": "ok",
        "errorMessage": "",
        "rp": {
            "name": "Example Corporation"
        },
        "user": {
            "id": "S3932ee31vKEC0JtJMIQ",
            "name": "johndoe@example.com",
            "displayName": "John Doe"
        },

        "challenge": "uhUjPNlZfvn7onwuhNdsLPkkE5Fv-lUN",
        "pubKeyCredParams": [
            {
                "type": "public-key",
                "alg": -7
            }
        ],
        "timeout": 10000,
        "excludeCredentials": [
            {
                "type": "public-key",
                "id": "opQf1WmYAa5aupUKJIQp"
            }
        ],
        "authenticatorSelection": {
            "residentKey": false,
            "authenticatorAttachment": "cross-platform",
            "userVerification": "preferred"
        },
        "attestation": "none"
    }
```
ちなみに、attestationに対しては、none、indirect、directを指定することができます。詳しくは以下のリンクをご覧ください。
https://w3c.github.io/webauthn/#enumdef-attestationconveyancepreference
上記のようにattestationにnoneを指定した場合、attestationObjectのfmtがnoneで返ってくるのでそれをチェックして、Attestationの検証をスキップします。

```javascript

{ fmt: 'none',
  attStmt: {},
  authData:
   <Buffer 49 96 0d e5 88 0e 8c 68 74 34 17 0f 64 76 60 5b 8f e4 ae b9 a2 86 32 c7 99 5c f3 ba 83 1d 97 63 45 5c 7d 1c de ad ce 00 02 35 bc c6 0a 64 8b 0b 25 f1 ... 150 more bytes> 
}
```


### SELF "packed" attestation
こちらは、packed attestationの中でも、attestation statementから公開鍵を取得できないようなフォーマットになります。
具体的なFULL "packed" attestation と SELF "packed" attestationのattestationObjectは以下のようになります。

**FULL "packed" attestation**

```javascript

{ fmt: 'packed',
  attStmt:
   { alg: -7,
     sig:
      <Buffer 30 46 02 21 00 be 80 1a 66 cb 2b 11 d3 ba c6 94 10 17 05 56 ee 72 e1 75 ff 0d ec c1 46 17 96 7d 11 10 5f 1d 5c 02 21 00 fe e4 f7 09 64 97 4d f5 bd 3d ... 22 more bytes>,
     x5c:
      [ <Buffer 30 82 02 be 30 82 01 a6 a0 03 02 01 02 02 04 74 86 fd c2 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 30 2e 31 2c 30 2a 06 03 55 04 03 13 23 59 75 62 ... 656 more bytes> ] },
  authData:
   <Buffer 49 96 0d e5 88 0e 8c 68 74 34 17 0f 64 76 60 5b 8f e4 ae b9 a2 86 32 c7 99 5c f3 ba 83 1d 97 63 41 00 00 01 58 f8 a0 11 f3 8c 0a 4d 15 80 06 17 11 1f ... 146 more bytes> 
}
```
**SELF "packed" attestation**

```javascript
{ fmt: 'packed',
  attStmt:
   { alg: -7,
     sig:
      <Buffer 30 46 02 21 00 f3 c8 07 b7 2d ce 45 7d 45 94 30 44 89 2c 85 36 c8 e1 1e 20 64 a2 6b 4d cc 74 ab 86 16 cd 53 c3 02 21 00 c1 77 e6 45 96 c9 ff 55 12 1c ... 22 more bytes> },
  authData:
   <Buffer 49 96 0d e5 88 0e 8c 68 74 34 17 0f 64 76 60 5b 8f e4 ae b9 a2 86 32 c7 99 5c f3 ba 83 1d 97 63 45 5c 7d 17 d1 ad ce 00 02 35 bc c6 0a 64 8b 0b 25 f1 ... 150 more bytes> 
}

```
それぞれのattestationObjectの中身を見ると、x5cの有無がよくわかるかと思います。
x5c(X.509 certificate chain)は証明書です。つまり、FULL "packed" attestationではx5c（証明書）から公開鍵を取り出し、署名を検証することができます。
一方、SELF "packed" attestationには、x5c（証明書）がないため、別の方法で検証をする必要があります。

#### SELF Attestationの検証
##### AuthenticatorDataから公開鍵を取得
以下、AuthenticatorDataの中身になります。AuthenticatorDataのCOSEPublicKeyから公開鍵を取得します。

```javascript

{
　rpIdHash:
   <Buffer 49 96 0d e5 88 0e 8c 68 74 34 17 0f 64 76 60 5b 8f e4 ae b9 a2 86 32 c7 99 5c f3 ba 83 1d 97 63>,
  flagsBuf: <Buffer 45>,
  flags: { up: true, uv: true, at: true, ed: false, flagsInt: 69 },
  counter: 1551704367,
  counterBuf: <Buffer 5c 7d 21 2f>,
  aaguid: <Buffer ad ce 00 02 35 bc c6 0a 64 8b 0b 25 f1 f0 55 03>,
  credID:
   <Buffer 00 17 89 f2 d9 ab 21 64 9a a3 1c 2a 70 7e 2e 3f 7f d8 1d 22 78 47 75 2c 57 d7 36 fe 81 40 e5 14 ee 2b 6a 6b 6c fb a5 37 c9 83 67 79 1f f0 cf fe 65 5f ... 18 more bytes>,
  COSEPublicKey:
   <Buffer a5 01 02 03 26 20 01 21 58 20 da 96 c4 1b 2c 77 32 f3 0c 2d 10 7a d7 d6 55 f6 e4 58 eb d4 db f4 1a 2b 36 77 4f d3 67 e7 c8 1f 22 58 20 59 ac c1 55 c0 ... 27 more bytes> 
}
```
##### 署名用データの作成
署名用のデータはauthDataとclientDataJSONをハッシュ値を連結したものになります。以下、作成イメージです。

```javascript

Buffer.concat([attestationStruct.authData, clientDataHashBuf]);
```
##### 取得した公開鍵、alg、sig、署名用データで検証
1で取得した公開鍵、attStmtのalg、sig、2で作成した署名用のデータを用いて検証します。
（以下のattStmtのalg、sigを利用します）

```javascript
{ fmt: 'packed',
  attStmt:
   { alg: -7,
     sig:
      <Buffer 30 46 02 21 00 f3 c8 07 b7 2d ce 45 7d 45 94 30 44 89 2c 85 36 c8 e1 1e 20 64 a2 6b 4d cc 74 ab 86 16 cd 53 c3 02 21 00 c1 77 e6 45 96 c9 ff 55 12 1c ... 22 more bytes> },
  authData:
   <Buffer 49 96 0d e5 88 0e 8c 68 74 34 17 0f 64 76 60 5b 8f e4 ae b9 a2 86 32 c7 99 5c f3 ba 83 1d 97 63 45 5c 7d 17 d1 ad ce 00 02 35 bc c6 0a 64 8b 0b 25 f1 ... 150 more bytes> 
}

```

## Assertionの検証
Assertionの検証時は他の認証器を利用する場合と特別違うことをする必要はありません。
署名の検証の処理については以下のようになります。

```javascript

let verifyAuthenticatorAssertionResponse = (webAuthnResponse, authenticators, userVerification) => {
    let authr = findAuthr(webAuthnResponse.id, authenticators);

    ...

    let authenticatorData = base64url.toBuffer(webAuthnResponse.response.authenticatorData)
    let response = { 'verified': false }
    let authrDataStruct = parseGetAssertAuthData(authenticatorData)
    let clientDataHash = hash('sha256', base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
    let signatureBase = Buffer.concat([authenticatorData, clientDataHash])
    let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey))
    let signature = base64url.toBuffer(webAuthnResponse.response.signature)
    response.verified = verifySignature(signature, signatureBase, publicKey)

    ...

    return response

}
```
大まかな処理としては、以下の4つになります。

##### 登録時に保存しておいた公開鍵を取得
navigator.credentials.get()のレスポンスとして以下のような値が送られてきます。
以下のオブジェクト内のidに紐づく公開鍵を取り出します。

```javascript

    {
        "id":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA",
        "rawId":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA",
        "response":{
            "authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAAA",
            "signature":"MEYCIQCv7EqsBRtf2E4o_BjzZfBwNpP8fLjd5y6TUOLWt5l9DQIhANiYig9newAJZYTzG1i5lwP-YQk9uXFnnDaHnr2yCKXL",
            "userHandle":"",
            "clientDataJSON":"eyJjaGFsbGVuZ2UiOiJ4ZGowQ0JmWDY5MnFzQVRweTBrTmM4NTMzSmR2ZExVcHFZUDh3RFRYX1pFIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9"
        },
        "type":"public-key"
    }
```

##### signatureの取得
navigator.credentials.get()のレスポンスに含まれるresponseというオブジェクトの中にsignatureが入っているので取り出します。

```javascript

{
 authenticatorData:
   'SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMd ...',
  signature:
   'MEYCIQDHQ8Fk3yFRuNQmMYRNArThKIXG-To514Of ...',
  userHandle: 'dsMGBXJO8wtWGMPf0t0XIa38NJWkT ...',
  clientDataJSON:
   'eyJjaGFsbGVuZ2UiOiJLeUVWa3N1Rm1IUUlnUEpx ... ' 
}
```

##### authenticatorDataとclientDataJSONのハッシュ値を結合して署名用データの作成
以下、データ作成例です。

```javascript

Buffer.concat([authenticatorData, clientDataHash])
```
##### 署名の検証
上記で用意したデータを用いて署名の検証を行います。

# おわりに
うまく実装できると以下のように動作します。
![ezgif-2-15c7cf25b423.gif](https://qiita-image-store.s3.amazonaws.com/0/213529/c7894d74-27e6-a596-9300-a451fb0abfc2.gif)

自分は長い間Assertionのシグネチャの検証が失敗しており、またその理由がわからずに悩んでいました。
しかし先日ついに、署名用のデータの生成方法を変えることでうまくいくようになりました。
具体的には、

```javascript

Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);
```
から以下のように変えることでうまくいきました。

```javascript

Buffer.concat([attestationStruct.authData, clientDataHashBuf]);
```
どうやらデータが不足していたようです。

# 参考
https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API
https://gihyo.jp/dev/column/newyear/2019/webauthn
https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn/
https://medium.com/@herrjemand/introduction-to-webauthn-api-5fd1fb46c285
https://w3c.github.io/webauthn/#enumdef-attestationconveyancepreference
https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn-attestation-packed/
https://medium.com/@herrjemand/verifying-fido2-packed-attestation-a067a9b2facd
https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html