OpenAM で WebAuthn(FIDO2)Credentialの保存先にLDAPを使う

# OpenAM で WebAuthn(FIDO2)Credentialの保存先にLDAPを使う

弊社濱野がブルガリアで開催された[LDAPcon2019](https://ldapcon.org/2019/wp-content/events/GDJAZW.htm)にて[LDAPのFIDO2用スキーマの提案発表](https://www.osstech.co.jp/~hamano/posts/fido2-ldap/)を行いました。
彼がW3Cの仕様を読んで作ったFIDO2 RPサーバーサンプル実装と、LDAPスキーマ設計内容、並行して開発していたOpenAMのWebAuthn認証モジュール実装とユースケース、の3方向から検討重ねつつベスト解と思われる物を導き出しています。

OpenAMはユーザーデーターストアにLDAPを利用しており、WebAuthn認証モジュールを使うときに、バックエンドのLDAPに何をどう保存するのか？という観点で書いています。

## RPサーバーは、なにを保存しておくべきか？ 
以下にある値は最低限保存しないと認証できません。

- **CredentialID(認証デバイス内での鍵の識別ID)**
- **PublicKey (認証デバイスで生成された公開鍵)**
- **SignCount (認証回数カウンター値)**

### ではuser.idは？

認証デバイス登録時にRPサーバーから指定する **user.id** がやっかいです。
W3Cでは以下のように書いてあります。

[user.id](https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-id)

> The **user handle** of the user account entity. To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this id member, not the displayName nor name members. See Section 6.1 of [RFC8266].

ユーザーアカウントの **user handle** で、認証と認可はこのidを用いるベシよと。ただし、displayNameやnameとは分けるベシ。

では **user handle** とは？同じようにW3Cでは以下のよう定義されています。

[14.9. User Handle](https://www.w3.org/TR/webauthn/#sctn-user-handle-privacy)

> Since the user handle is not considered personally identifying information in §14.8 Privacy of personally identifying information Stored in Authenticators, the Relying Party SHOULD NOT include personally identifying information, e.g., e-mail addresses or usernames, in the user handle. This includes hash values of personally identifying information, unless the hash function is salted with salt values private to the Relying Party, since hashing does not prevent probing for guessable input values. It is RECOMMENDED to let the user handle be 64 random bytes, and store this value in the user’s account.


RPサーバーはユーザー名や、メールアドレスなど、IDを特定できる情報を **user handle** に含んではイカン、salt無しのhashも駄目、まぁsalt付きhashでRPサーバー外で類推不可能なら良いけど、64バイトのランダムな値で、(RPサーバー内の)ユーザー属性として保存することをお勧めするよ。

要はRPサーバー内で、ユーザーを特定できるランダムな値と理解できます。

この場合、ユニークな値となるように考慮して、ユーザー属性に **user.id** 値を保存するか、別にマッピングテーブルを持つ必要があります。

LDAPユーザーエントリーに新設計で値を保存するとなると、スキーマを拡張し、ユーザーのオブジェクトクラスを追加する必要があります。＜伏線です。

最終的に以下4つは必須で保存する必要があるという結論です。

- **CredentialID(認証デバイス内での鍵の識別ID)**
- **PublicKey (認証デバイスで生成された公開鍵)**
- **SignCount (認証回数カウンター値)**
- **user.id (userHandle値)**

## どう保存するか？
保存する値は決まったので、どうスキーマに落とし込むか？

ユーザーは複数の認証デバイスを並行して運用できないと意味がありませんので、複数デバイスを登録することが前提で考えます。

W3Cの仕様にも準拠した運用に耐えるのは当然のこととして、LDAPでの検索パフォーマンスの事も考慮しないといけません。

複数の登録情報（公開鍵）をユーザーの属性にマルチバリューのエントリーとしてダラダラとぶら下げる方法もありますが、検索性とユーザーが不明な状態で始まる **ResidentKey** のフローもあるため、Credentialsというouにまとめて格納することにします。

ユーザー属性にuser.idと複数の鍵を保存するの図
![usertree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/386f7b3e-5fc9-1a8f-280b-d0922ec92150.png)


ou=Credentialsに複数の鍵を保存するの図（エントリー内のfido2UserID(user.id)は所有者へのポインタ）
![credtree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/2beb8132-b79f-8e33-080b-264ba89c6502.png)


### CredentialID
CredentaiIDは登録された公開鍵と1対1となる鍵の識別子とも言うべき値です。
バイナリ値ですが、Base64Urlエンコードし、文字列として格納します。
dnになるためユニーク性はLDAP側で保証してくれます。

### PublicKey
認証デバイスから送られてくる鍵はCOSEフォーマットです。サイズが小さくて済むので、この形式のままバイナリとして保存します。

### SignCount
認証デバイスで署名（認証）した回数の数値（long)なのでそのまま数値として保存します。

### user.id
濱野の案のキモはコレ
ランダムな64byteのバイナリ値を生成して、いちいちユニークか確認して、さらにユーザーのuser.id保存属性と、Credentialの持ち主のuser.idを保存する属性にトランザクションを考慮して書くのは厳しい。

図=同一トランザクション書き込み
![2userid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/b948b472-5dae-0d8c-7263-a8105f149252.png)


そこでLDAPのユーザー属性を見るとentryUUIDというバイナリ値が見えます。

図=entryUUID
![entryUUID.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/310d4610-75c7-4402-f718-81b7cbf2bedc.png)


LDAP内でエントリーを一意に表す値です。
これをuser.idとして使うのがLDAP的にはベスト解なのではないか？
ユーザーエントリーには値を追加する必要が無くなり、トランザクションを考慮して2つのエントリーへ値を追加する必要もなくなりました。
さらに **user.id** 値を保存する為にユーザーにオブジェクトクラスを追加しなくて済みます。<伏線回収

図=完成形
![2entry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/7a0f3c11-8a5c-6d5d-d18e-11f4363f27f2.png)

## スキーマはどのように構成になったか？

4つの値以外に、鍵のAAGUIDを保存する属性と、紛失した鍵を削除したりする場合にCredentialIDやバイナリ値では何だかワケが分からないので、人間が判別できる名前を付けるための属性を追加して以下のようになりました。

Credential情報にユーザー名を入れちゃうのは無しの方向です。
ナシの図
![nashi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/b19c2178-6f86-7567-af49-e3a15d9582d2.png)

```
# LDAP Schema for FIDO2 Credential

attributetype ( 1.3.6.1.4.1.34468.2.56.1.1
        NAME 'fido2CredentialID'
        EQUALITY caseExactIA5Match
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
        SINGLE-VALUE )

attributetype ( 1.3.6.1.4.1.34468.2.56.1.2
        NAME 'fido2RawID'
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.5
        SINGLE-VALUE )

attributetype ( 1.3.6.1.4.1.34468.2.56.1.3
        NAME 'fido2PublicKey'
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.5
        SINGLE-VALUE )

attributetype ( 1.3.6.1.4.1.34468.2.56.1.4
        NAME 'fido2SignCount'
        EQUALITY integerMatch
        ORDERING integerOrderingMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
        SINGLE-VALUE )

attributetype ( 1.3.6.1.4.1.34468.2.56.1.5
        NAME 'fido2UserID'
        EQUALITY uuidMatch
        ORDERING uuidOrderingMatch
        SYNTAX 1.3.6.1.1.16.1
        SINGLE-VALUE )

attributetype ( 1.3.6.1.4.1.34468.2.56.1.6
        NAME 'fido2AAGUID'
        EQUALITY octetStringMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
        SINGLE-VALUE )

attributetype ( 1.3.6.1.4.1.34468.2.56.1.7
        NAME 'fido2CredentialName'
        EQUALITY caseExactMatch
        ORDERING caseExactOrderingMatch
        SUBSTR caseExactSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
        SINGLE-VALUE )

objectclass ( 1.3.6.1.4.1.34468.2.56.2.1
	NAME 'fido2Credential'
	DESC 'objectClass for FIDO2 Credential'
	SUP top STRUCTURAL
	MUST ( fido2CredentialID $ fido2PublicKey $
	       fido2SignCount $ fido2UserID )
	MAY ( fido2RawID $ fido2AAGUID $ fido2CredentialName ))
```
もうすぐリリース予定の次のOpenAMではこのスキーマが標準で入ってます。
なので特にスキーマ拡張無しでWebAuthnが使えます。

実際に実装としてテストしている今時点では問題は無いですが、
完全に確定というわけではなく、指摘や修正案など受け付けておりますので、何かあれば是非。