1. 導入
1.1. Web Authenticationとは
Web Authenticationは、W3Cで2019年3月に勧告になった、Webアプリケーションの新しいセキュアな認証方式仕様です。 WebAuthnという略称で表記されることもあります。ローカル認証や公開鍵認証、Origin別の鍵管理を組み合わせることで、フィッシングなど認証プロセスに対する攻撃に対して堅固で便利な認証を実現しています。 主要ブラウザで実装されており、セキュリティ、利便性を重視するユーザーに対して優れた選択肢を提供します。
1.3. WebAuthn4Jとは
WebAuthn4Jは、Web Authentication仕様に基づくサーバーサイド検証を行うためのJavaライブラリです。WebAuthn/Passkeysだけでなく、Apple App Attestや、FIDO CTAP2セキュリティキーを用いた独自アプリケーションのサーバーサイド検証に用いることも可能です。 Web Authentication仕様で定められた全ての構成証明ステートメント(Attestation)をサポートしながら、外部ライブラリへの依存関係は最小限に抑えたポータブルなライブラリです。
1.4. 特徴
1.4.1. サポートする構成証明ステートメントフォーマット
WebAuthn4Jでは、以下の通り全ての構成証明ステートメントフォーマットをサポートしています。
-
Packed attestation
-
FIDO U2F attestation
-
Android Key attestation
-
Android SafetyNet attestation
-
TPM attestation
-
Apple Anonymous attestation
-
None attestation
-
Apple App Attest attestation
1.4.2. 準拠状況
FIDO Allianceが提供するFIDO2 Test Tools で必須のテストケースに加え、オプションのAndroid Key attestationのテストケースを全て合格しています。
| FIDO2 Test ToolsはFIDO2 Transport Binding ProfileのREST API経由でテストを実行する為、WebAuthn4J Spring Securityが 提供するFIDO2 Transport Binding ProfileのREST API実装に対して実行して検証を行っています。 |
1.7. Maven Centralからの取得
Mavenを使用している場合、webauthn4jを依存関係として追加して使用してください:
<properties>
...
<!-- Use the latest version whenever possible. -->
<webauthn4j.version>0.31.7.RELEASE</webauthn4j.version>
...
</properties>
<dependencies>
...
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-core</artifactId>
<version>${webauthn4j.version}</version>
</dependency>
...
</dependencies>
1.9. ライセンス
WebAuthn4jは Apache 2.0 License の オープンソースソフトウェアです。
2. クイックスタート
このクイックスタートでは、まずWebAuthn認証の処理の概要を紹介し、その後、WebAuthn4Jライブラリがカバーする範囲や制限、およびWebAuthn4Jを利用したWebAuthn認証の実装方法を説明します。
2.1. WebAuthn認証の処理概要
2.1.1. 認証のフロー
WebAuthn認証は、端的に言えば、ウェブでの利用にあわせて設計された公開鍵認証です。 予め鍵ペアを作成し、サーバー側に公開鍵、認証デバイス側に秘密鍵を保存しておき、 認証時は認証デバイスで秘密鍵で署名を行い、署名をサーバー側に送信して公開鍵で署名を検証することで認証を行う方式です。
署名対象のデータは、クライアントのデータや、認証デバイスのデータです。クライアントのデータには、予めサーバー側で生成したチャレンジや、表示しているサイトのドメイン(origin)等、サーバー側に関連するデータも含まれます。 署名の検証だけでなく、チャレンジの一致もサーバー側で検証しているため、リプレイ攻撃を防止できます。 他にも様々なクライアントのデータや、認証デバイスのデータが署名対象データに含まれており、サーバー側での検証の対象となっています。 認証時のデータの流れを図にすると以下の通りです。
2.1.2. 登録のフロー
WebAuthnの新しいクレデンシャル登録処理とは、クライアントが認証デバイスに対して新しい鍵ペアの生成を要求し、 認証デバイスからクライアントに返却された公開鍵等をクレデンシャルとしてサーバーに登録する処理です。 実は、新しいクレデンシャル登録時も認証時と類似したフローであり、 まずサーバーから送信されたチャレンジを含むクライアントデータと、認証デバイスのデータに対して 認証デバイスが署名して返却し、これをクライアント経由で受け取ったサーバーは署名検証を実施、 成功した場合、クレデンシャルレコードとして登録する、という流れになっています。 但し、登録時の場合、署名対象の認証デバイスのデータに、生成された新しい鍵ペアの公開鍵が含まれます。 サーバーにはこの公開鍵が登録され、認証時の署名検証で使用されることになります。
図にしますと以下の通りです。
前のセクションで説明した通り、認証時はクレデンシャルの秘密鍵で署名されているので、署名をクレデンシャルの公開鍵で検証する仕組となっていますが、 それでは公開鍵登録時に認証デバイスのデータとクライアントのデータに署名する際に使用される秘密鍵は何なのでしょうか。また、この署名を検証する公開鍵をサーバーはどうやって入手するのでしょうか。 通常、実はこの秘密鍵は認証デバイスのモデル毎に固有の鍵で、認証デバイスにあらかじめ焼きこまれています。 検証に使用する公開鍵に関しては、予め信頼する認証デバイスの公開鍵をサーバーに構成しておくか、 FIDO Metadata Serviceのように、認証デバイスのモデル毎の公開鍵を公開しているレジストリから取得することが可能です。
このように、クレデンシャルの公開鍵登録のメッセージを認証デバイスのモデル固有の秘密鍵で署名することで、 登録しようとしている認証デバイスが特定のモデルであることを証明する"Attestation"(構成証明)という仕組がWebAuthnには備わっており、 このデバイスの構成証明情報を含んだデータ構造は、構成証明ステートメント(Attestation Statement)と呼ばれます。 ただし、ユーザーがどのモデルの認証デバイスを使っているかという構成証明ステートメントは、ユーザートラッキングにも使われうる情報です。 そのため、デフォルトの構成では、認証デバイスが構成証明ステートメントを返却してもクライアントによって破棄され、サーバー側には送信されません。 構成証明ステートメントを送信するようオプションで明示的に指定した場合にのみ、エンドユーザーの同意を得たうえで構成証明ステートメントは開示されます。
2.2. WebAuthn4Jのスコープ
WebAuthn4Jは特定のWebアプリケーションフレームワークに依存しないポータビリティを実現する為、WebAuthnの登録時、認証時のサーバー側の検証に意図的に機能を絞り込んでいます。
そのため、HTTPリクエストからのパラメータの取出、チャレンジのセッションへの保存、 フロントエンド側への返却、 生成された公開鍵などをクレデンシャルレコードとして保存する処理、認証時にクレデンシャルレコードをロードする処理、といった機能は具備していません。 それらの処理は、利用しているフレームワークにあわせて実装する必要があるためです。 ご利用のフレームワークで、それらをケアしてくれるWebAuthn4Jのラッパーライブラリが存在する場合は、そちらを利用すると良いでしょう。 例えば、
-
Quarkus Security WebAuthn
-
Spring Security Passkeys
-
Vert.x Auth WebAuthn4J
といったラッパーライブラリが提供されています。 ラッパーライブラリが存在しない場合は、自前でそれらの処理を実装頂く必要があります。次のセクションで説明していきます。
2.3. WebAuthn4Jを利用した登録処理の実装
2.3.1. WebAuthnの鍵ペアの生成
WebAuthnの鍵ペアの生成において中心となるAPIは、ブラウザの navigator.credentials.create メソッドです。
このAPIを呼び出すことで、WebAuthnの鍵ペアが認証デバイスによって生成され、公開鍵を含むWebAuthnのクレデンシャルが戻り値として返却されます。
navigator.credentials.create メソッドの呼出時には、様々なオプションが指定出来ます。
そのオプションの一つに、 challenge が存在します。前述の通り、チャレンジはリプレイ攻撃を防止するためのパラメータであり、サーバー側で生成した値をパラメータとして指定し、また、同じ値をセッション等に保存しておく必要があります。
登録のフローの図の通り、まずバックエンドサーバーでチャレンジを生成してセッションに保存し、それをクライアントに渡す必要があります。
バックエンドサーバーからフロントエンドへのチャレンジの受け渡し方法はWebAuthn仕様では特に定められていません。
HTMLページに埋め込んでも良いですし、チャレンジを返却するRESTエンドポイントを用意することも可能です。
navigator.credentials.create メソッドのパラメータである、 PublicKeyCredentialCreationOptions 全体を返却するエンドポイントを用意するのも良いアイデアでしょう。
WebAuthnのJava Script APIには、 PublicKeyCredential.parseCreationOptionsFromJSON というメソッドが用意されており、JSONとしてシリアライズされた PublicKeyCredentialCreationOptions をパースすることが可能です。
但し、SafariではSafari 18.4以降でしか PublicKeyCredential.parseCreationOptionsFromJSON が利用できません。代替策については、Safariで未サポートなJSON serialization APIsの代替 を参照してください。
WebAuthn4Jは PublicKeyCredentialCreationOptions を表現するJavaのクラスを提供しており、バックエンドサーバー側でJSONを組み立てる際にご活用頂けます。
PublicKeyCredentialCreationOptions 全体をREST Endpointから取得して navigator.credentials.create を呼出const response = await fetch("/passkeys/attestationOptions") //fetch PublicKeyCredentialCreationOptions as JSON string
const publicKeyCredentialCreationOptionsJSON = await response.json() // convert to JSONObject
const credentialCreationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyCredentialCreationOptionsJSON); // convert to PublicKeyCredentialCreationOptions
const publicKeyCredential = await navigator.credentials.create({ publicKey: credentialCreationOptions}); // create PublicKeyCredential
いずれにせよ、バックエンドサーバー側でチャレンジを生成し、セッションに保存した上で、何らかの方法でフロントエンド側に引き渡した上で、
フロントエンド側のJava Scriptで navigator.credentials.create メソッドを呼び出してWebAuthnクレデンシャルを生成して下さい。
navigator.credentials.create メソッドに指定できるその他のオプションに関しては、 MDN: CredentialsContainer: create() メソッドを参照下さい。
2.3.2. WebAuthnの公開鍵のサーバーへの登録
生成されたWebAuthnクレデンシャルは、何らかの方法でバックエンドサーバー側に送信する必要があります。
バックエンドサーバー側にどのようなフォーマットで送信するかについてもWebAuthn仕様では定義されていません。
但し、WebAuthnクレデンシャルである PublicKeyCredential というJavaScriptの型には、 toJSON というメソッドが用意されており、
こちらと JSON.stringify を利用してシリアライズしたデータを送信するのが一つのベストプラクティスです。
但し、この toJSON メソッドもSafariでは利用できませんが、代替策については、Safariで未サポートなJSON serialization APIsの代替 を参照してください。
PublicKeyCredential の送信const registrationResponseJSON = publicKeyCredential.toJSON(); // convert to JSONObject
await fetch("/register", {
method : 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
'username': document.getElementById('username').value,
'registrationResponseJSON': JSON.stringify(registrationResponseJSON) //convert to string
})
});
バックエンドサーバー側は受け取ったWebAuthnクレデンシャルを検証した上で、公開鍵を含むWebAuthnクレデンシャルレコードを永続化する必要があります。
WebAuthn4Jでは、 PublicKeyCredential のJSON表現を WebAuthnManager#verifyRegistrationResponseJSON というメソッドで直接検証することが可能です。
WebAuthnManager#parseRegistrationResponseJSON は、検証を行わず、PublicKeyCredential のデシリアライズ処理のみを行います。
検証でエラーが発生した場合に、元のパースされたデータにアクセスしたい場合は、
WebAuthnManager#parseRegistrationResponseJSON メソッドを用いてパースしたうえで、 得られた RegistrationData のインスタンスを WebAuthnManager#verify メソッドに渡して検証を実行してください。
PublicKeyCredential のサーバーサイド検証String registrationResponseJSON = "<registrationResponseJSON>"; /* set registrationResponseJSON received from frontend */
RegistrationData registrationData;
try {
registrationData = webAuthnManager.parseRegistrationResponseJSON(registrationResponseJSON);
}
catch (DataConversionException e) {
// If you would like to handle WebAuthn data structure parse error, please catch DataConversionException
throw e;
}
// Server properties
Origin origin = null /* set origin */;
String rpId = null /* set rpId */;
Challenge challenge = null /* set challenge */;
ServerProperty serverProperty = ServerProperty.builder()
.origin(origin)
.rpId(rpId)
.challenge(challenge)
.build();
// expectations
List<PublicKeyCredentialParameters> pubKeyCredParams = null;
boolean userVerificationRequired = false;
boolean userPresenceRequired = true;
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, pubKeyCredParams, userVerificationRequired, userPresenceRequired);
try {
webAuthnManager.verify(registrationData, registrationParameters);
} catch (VerificationException e) {
// If you would like to handle WebAuthn data verification error, please catch VerificationException
throw e;
}
// please persist CredentialRecord object, which will be used in the authentication process.
CredentialRecord credentialRecord =
new CredentialRecordImpl( // You may create your own CredentialRecord implementation to save friendly authenticator name
registrationData.getAttestationObject(),
registrationData.getCollectedClientData(),
registrationData.getClientExtensions(),
registrationData.getTransports()
);
save(credentialRecord); // please persist credentialRecord in your manner
RegistrationParameters は、WebAuthnManager#verifyRegistrationResponseJSON メソッドのもう一つの引数であり、 サーバーの状態や検証条件をまとめたパラメータです。
-
serverPropertyは、サーバーの状態を渡すパラメータです。次の[ServerProperty]を参照して下さい。 -
pubKeyCredParamsにはPublicKeyCredentialCreationOptionsで指定したpubKeyCredParamsと同じ値を指定して下さい。 -
userVerificationRequiredは、認証デバイスでのユーザーの生体認証やPIN確認などでの当人認証を必須とするかのパラメータです。 -
userPresenceRequiredは、認証デバイス側でのユーザーの介在確認を必須とするかのパラメータです。ユーザーによって何らかのジェスチャー入力が行われたことを示すUPフラグを確認します。 このジェスチャーには、生体認証に限らず、静電容量ボタンのタッチ等、当人認証が行われない操作も含まれます。 WebAuthnにおいては、UPフラグは基本的に必須ですのでtrueを指定すべきですが、パスワードからパスキーへの自動アップグレード時のクレデンシャル自動生成時のシナリオに限ってはfalseとなります。
サーバーの状態については、 serverProperty としてまとめています。
ServerProperty のコンストラクタを呼び出す際のパラメータには以下の値を指定して下さい。
-
originには WebAuthn による登録・認証セレモニーの時点でクライアントブラウザ(ユーザーエージェント)が認識していたことを期待する Origin を指定してください。ブラウザは認識した Origin を clientData に書き込み、その内容を含めて署名します。サーバー側では、clientData に記録された Origin が期待値と一致するかを照合することで、想定外のページや別オリジンから不正に起動されたセレモニー(フィッシングやフレーミング誘発)を防ぎます。 -
rpIdには WebAuthn による認証を提供するサイトの rpId を指定して下さい。rpId は資格情報のスコープを指定するパラメータです。 詳しくは WebAuthnの仕様書のrpIdの項 を参照して下さい。 -
challengeには発行した Challenge を指定して下さい。challengeはリプレイ攻撃を防ぐ為のパラメータです。 サーバー側でchallengeとしてランダムなバイト列を生成し、フロントエンド側で WebAuthn JS API を実行する際にパラメータとして指定して署名対象に含め、サーバー側で値の一致を検証することで、リプレイ攻撃からユーザーを防御することが出来ます。 発行した Challenge を検証時まで永続化しておくのは WebAuthn4J ライブラリ呼出側の責務です。セッションなどに格納しておくと良いでしょう。 -
(オプション)
topOrigin(…)/topOrigins(…)/topOriginPredicate(…)を使用することで、許容するtop originを指定することが出来ます。-
WebAuthn は同一オリジン制約とクリックジャッキング対策の観点から、原則としてクロスオリジンを許容しません。
-
ただし、認証ウィジェットを別オリジンのサイトに埋め込む、といったユースケースでは、親ページを限定して許容したい場合があります。その場合は
topOrigin(…)/topOrigins(…)/topOriginPredicate(…)で許容する親ページ(top-level ドキュメントの Origin)を明示してください。どうしても任意の親ページを許可する必要がある場合はanyTopOrigin()を使用できますが、攻撃面が広がるため慎重な運用が必要です。 -
例(特定の親ページのみ許可):
ServerProperty serverProperty = ServerProperty.builder() .origin(new Origin("https://rp.example.com")) .rpId("rp.example.com") .challenge(challenge) .topOrigin(new Origin("https://parent.example.com")) .build(); -
例(任意の親ページを許可):
ServerProperty serverProperty = ServerProperty.builder() .origin(new Origin("https://rp.example.com")) .rpId("rp.example.com") .challenge(challenge) .anyTopOrigin() .build();
-
検証に成功した場合は、返却された値から CredentialRecord インスタンスを作成し、データベース等へアプリケーション側で永続化して下さい。 認証時に使用します。
永続化方法について詳しくは、 CredentialRecordのシリアライズ、デシリアライズ を参照して下さい。
検証に失敗した場合は、 VerificationException のサブクラスの例外が発生します。
2.4. WebAuthn4Jを利用した認証処理の実装
2.4.1. WebAuthnのアサーションの生成
WebAuthnでの認証時において中心となるAPIは、ブラウザの navigator.credentials.get メソッドです。
認証のフローの図の通り、認証処理においても、まずバックエンドサーバー側でチャレンジを生成し、セッションに保存する一方、クライアントにチャレンジを引き渡す必要があります。
navigator.credentials.get メソッドのパラメータにも challenge が存在するためです。
バックエンドサーバーからフロントエンド(クライアント)への認証処理のチャレンジの受け渡し方法もWebAuthn仕様では定められていません。登録処理同様、お好みの方法でチャレンジをフロントエンド側に引き渡して下さい。
navigator.credentials.get メソッドのパラメータである、 PublicKeyCredentialRequestOptions をパースするJava Script APIは、 PublicKeyCredential.parseCreationGetOptionsFromJSON です。 PublicKeyCredential.parseCreationGetOptionsFromJSON がSafariで利用できない問題の代替案はSafariで未サポートなJSON serialization APIsの代替 を参照してください。
navigator.credentials.get メソッドに指定できるその他のオプションに関しては、 MDN: CredentialsContainer: get() メソッドを参照下さい。
PublicKeyCredentialRequestOptions 全体をREST Endpointから取得して navigator.credentials.get を呼出const response = await fetch("/passkeys/assertionOptions");
const publicKeyCredentialRequestOptionsJSON = await response.json();
const credentialGetOptions = PublicKeyCredential.parseRequestOptionsFromJSON(publicKeyCredentialRequestOptionsJSON);
const publicKeyCredential = await navigator.credentials.get({ publicKey: credentialGetOptions});
2.4.2. WebAuthnのアサーションの検証処理、および後処理
navigator.credentials.get メソッドによって生成されたアサーションは、バックエンドサーバー側に送信し、検証する必要があります。登録時同様、toJSON メソッドでシリアライズが可能です。
PublicKeyCredential をサーバーに送信const authenticationResponseJSON = publicKeyCredential.toJSON();
console.debug("authenticationResponseJSON: %s", authenticationResponseJSON);
await fetch("/passkeys/authenticate", {
method : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticationResponseJSON)
});
WebAuthn4Jでは、 PublicKeyCredential のJSON表現を WebAuthnManager#verifyAuthenticationResponseJSON というメソッドで検証することが可能です。
パースし、検証する2段階を踏む場合は、WebAuthnManager#parseAuthenticationResponseJSON メソッドと WebAuthnManager#verify メソッドをご利用下さい。
PublicKeyCredential のサーバーサイド検証String authenticationResponseJSON = "<authenticationResponseJSON>"; /* set authenticationResponseJSON received from frontend */
AuthenticationData authenticationData;
try {
authenticationData = webAuthnManager.parseAuthenticationResponseJSON(authenticationResponseJSON);
} catch (DataConversionException e) {
// If you would like to handle WebAuthn data structure parse error, please catch DataConversionException
throw e;
}
// Server properties
Origin origin = null /* set origin */;
String rpId = null /* set rpId */;
Challenge challenge = null /* set challenge */;
ServerProperty serverProperty = ServerProperty.builder()
.origin(origin)
.rpId(rpId)
.challenge(challenge)
.build();
// expectations
List<byte[]> allowCredentials = null;
boolean userVerificationRequired = true;
boolean userPresenceRequired = true;
CredentialRecord credentialRecord = load(authenticationData.getCredentialId()); // please load authenticator object persisted in the registration process in your manner
AuthenticationParameters authenticationParameters =
new AuthenticationParameters(
serverProperty,
credentialRecord,
allowCredentials,
userVerificationRequired,
userPresenceRequired
);
try {
webAuthnManager.verify(authenticationData, authenticationParameters);
} catch (VerificationException e) {
// If you would like to handle WebAuthn data validation error, please catch ValidationException
throw e;
}
// please update the counter of the authenticator record
updateCounter(
authenticationData.getCredentialId(),
authenticationData.getAuthenticatorData().getSignCount()
);
WebAuthnManager#verifyAuthenticationResponseJSON メソッドのもう一つの引数である AuthenticationParameters は、サーバーの状態や検証条件をまとめたパラメータです。
-
serverPropertyは、サーバーの状態を渡すパラメータです。詳しくは[ServerProperty] を参照して下さい。 -
userVerificationRequiredは認証デバイスでのユーザーの生体認証やPIN確認などでの当人認証を必須とするかのパラメータです。パスワード+認証デバイスの「所持」による多要素認証を行う場合は、パスワードで本人性の確認が出来ている為falseで良いでしょう。 パスワードレス認証として、認証デバイスによる本人性確認+「所持」による多要素認証を行う場合はtrueを指定する必要があります。 -
authenticatorには、登録時に永続化したCredentialRecordを指定してください。
検証に成功した場合は、認証に成功したものと見做すことが出来ますので、永続化された CredentialRecord に紐づけたcounterおよび、uvInitialized、backedUpの値を更新してください。
カウンタは万が一認証デバイスのクローンが 作成されたことを検知するために用意されています。
カウンタについて詳しくは WebAuthnの仕様書のカウンタの項 を参照して下さい。
その後、認証済セッションを作成するなど、ユーザー認証成功時の処理を実施下さい。
検証に失敗した場合は、 VerificationException のサブクラスの例外が発生します。
2.5. Apple App Attestの検証
続いて、Apple App Attestの検証方法について解説します。 Apple App Attestは、WebAuthnに類似したデータ構造を持つため、Verifierの設計も、WebAuthn用のVerifierを踏襲しています。 なお、リスクメトリックの評価には対応していません。
2.5.1. Maven Centralからの取得
Apple App Attestの検証用クラスは、WebAuthn4J本体(webauthn4j-core)とは別の、webauthn4j-appattestというモジュールとして配布されています。 Mavenを使用している場合、以下のようにwebauthn4j-appattestを依存関係として追加してください。
<properties>
...
<!-- Use the latest version whenever possible. -->
<webauthn4j.version>0.31.7.RELEASE</webauthn4j.version>
...
</properties>
<dependencies>
...
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-appattest</artifactId>
<version>${webauthn4j.version}</version>
</dependency>
...
</dependencies>
2.5.2. Apple App Attest構成証明の検証
認証デバイスの登録時に構成証明を検証する際は、DCAttestationRequest を引数に
DeviceCheckManager#verify メソッドを用いて登録リクエストのパース、検証を行ってください。 登録リクエストの検証でエラーが発生した場合に、元のパースされたデータにアクセスしたい場合は、
DeviceCheckManager#parse メソッドを用いて登録リクエストをパースしたうえで、 得られた DCAttestationData のインスタンスを DeviceCheckManager#verify メソッドに渡して実行してください。
DCAttestationRequest のメンバー はiOS上でDevice Check App Attest APIを実行して取得した値となります。 何らかの方法でiOSデバイス側からサーバー側に伝送し、指定してください。
DCAttestationParameters は、DeviceCheckManager#verify メソッドのもう一つの引数であり、 サーバーの状態や検証条件をまとめたパラメータです。 サーバーの状態については、 DCServerProperty としてまとめています。
DCServerProperty のコンストラクタを呼び出す際のパラメータには以下の値を指定して下さい。
-
teamIdentifierにはiOSアプリ開発時のteam identifierを指定してください。 詳しくは Apple Apple Attestのサーバサイド検証手順 を参照して下さい。 -
cfBundleIdentifierにはiOSアプリ開発時のbundle identifierを指定してください。 詳しくは Apple Apple Attestのサーバサイド検証手順 を参照して下さい。 -
challengeには発行したChallengeを指定して下さい。challengeはリプレイ攻撃を防ぐ為のパラメータです。 サーバー側でchallengeとしてランダムなバイト列を生成し、iOS側でApp Attest APIを実行する際に パラメータとして指定して署名対象に含め、サーバー側で値の一致を検証することで、リプレイ攻撃からユーザーを防御することが出来ます。 発行したChallengeを検証時まで永続化しておくのはアプリケーション側の責務です。セッションなどに格納しておくと良いでしょう。
検証に失敗した場合は、 VerificationException のサブクラスの例外が発生します。 検証に成功した場合は、返却された値から DCAppleDevice インスタンスを作成し、データベース等へアプリケーション側で永続化して下さい。 認証時に必要となります。
商用環境か?開発環境か?
Apple App Attestは、開発中用に開発用の構成証明を返却することが可能です。
WebAuthn4Jはデフォルトでは商用の構成証明を受け入れる設定となっており、
開発用の構成証明を利用する場合は、 DCAttestationDataVerifier#setProduction で false を設定する必要があります。
// Client properties
byte[] keyId = null; /* set keyId */
byte[] attestationObject = null; /* set attestationObject */
byte[] challenge = null; /* set challenge */
byte[] clientDataHash = MessageDigestUtil.createSHA256().digest(challenge);
// Server properties
String teamIdentifier = null /* set teamIdentifier */;
String cfBundleIdentifier = null /* set cfBundleIdentifier */;
DCServerProperty dcServerProperty = new DCServerProperty(teamIdentifier, cfBundleIdentifier, new DefaultChallenge(challenge));
DCAttestationRequest dcAttestationRequest = new DCAttestationRequest(keyId, attestationObject, clientDataHash);
DCAttestationParameters dcAttestationParameters = new DCAttestationParameters(dcServerProperty);
DCAttestationData dcAttestationData;
try {
dcAttestationData = deviceCheckManager.parse(dcAttestationRequest);
} catch (DataConversionException e) {
// If you would like to handle Apple App Attest data structure parse error, please catch DataConversionException
throw e;
}
try {
deviceCheckManager.verify(dcAttestationData, dcAttestationParameters);
} catch (ValidationException e) {
// If you would like to handle Apple App Attest data validation error, please catch ValidationException
throw e;
}
// please persist Authenticator object, which will be used in the authentication process.
DCAppleDevice dcAppleDevice =
new DCAppleDeviceImpl( // You may create your own Authenticator implementation to save friendly authenticator name
dcAttestationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData(),
dcAttestationData.getAttestationObject().getAttestationStatement(),
dcAttestationData.getAttestationObject().getAuthenticatorData().getSignCount(),
dcAttestationData.getAttestationObject().getAuthenticatorData().getExtensions()
);
save(dcAppleDevice); // please persist authenticator in your manner
2.5.3. Apple App Attestアサーションの検証
認証時にアサーションを検証する際は、DCAssertionRequest を引数に DeviceCheckManager#verify メソッドを 実行してください。DCAssertionRequest の コンストラクタの引数に指定する、 keyId と assertion 、 clientDataHash は iOS側でApple App Attest APIを実行して取得した値となります。 何らかの方法でフロントエンド側からサーバー側に伝送し、指定してください。
DeviceCheckManager#verify メソッドのもう一つの引数である DCAssertionParameters の コンストラクタの引数に指定する、 serverProperty はサーバー側から取得する値をまとめたパラメータです。
DCAppleDevice には、登録時に永続化した DCAppleDevice を指定してください。
検証に失敗した場合は、 VerificationException のサブクラスの例外が発生します。 検証後は、 DCAppleDevice に紐づけたカウンタの値を更新してください。カウンタは万が一認証デバイスのクローンが 作成された場合を検知するために用意されています。カウンタについて詳しくは
WebAuthnの仕様書のカウンタの項 を参照して下さい。
// Client properties
byte[] keyId = null /* set keyId */;
byte[] assertion = null /* set assertion */;
byte[] clientDataHash = null /* set clientDataHash */;
// Server properties
String teamIdentifier = null /* set teamIdentifier */;
String cfBundleIdentifier = null /* set cfBundleIdentifier */;
byte[] challenge = null;
DCServerProperty dcServerProperty = new DCServerProperty(teamIdentifier, cfBundleIdentifier, new DefaultChallenge(challenge));
DCAppleDevice dcAppleDevice = load(keyId); // please load authenticator object persisted in the attestation process in your manner
DCAssertionRequest dcAssertionRequest =
new DCAssertionRequest(
keyId,
assertion,
clientDataHash
);
DCAssertionParameters dcAssertionParameters =
new DCAssertionParameters(
dcServerProperty,
dcAppleDevice
);
DCAssertionData dcAssertionData;
try {
dcAssertionData = deviceCheckManager.parse(dcAssertionRequest);
} catch (DataConversionException e) {
// If you would like to handle Apple App Attest data structure parse error, please catch DataConversionException
throw e;
}
try {
deviceCheckManager.verify(dcAssertionData, dcAssertionParameters);
} catch (ValidationException e) {
// If you would like to handle Apple App Attest data validation error, please catch ValidationException
throw e;
}
// please update the counter of the authenticator record
updateCounter(
dcAssertionData.getCredentialId(),
dcAssertionData.getAuthenticatorData().getSignCount()
);
3. 設定
WebAuthn4Jを利用する上で中心となるクラスは WebAuthnManager クラスです。
WebAuthnManager は登録リクエスト検証時の構成証明ステートメントの署名と信頼性の検証を、 それぞれ AttestationStatementVerifier と CertPathTrustworthinessVerifier インタフェースの実装に委譲します。
大多数のサイトは厳密な構成証明ステートメントの検証を必要とせず、エンタープライズ用途以外では厳密な構成証明ステートメントの検証は非推奨とされていることから(
WebAuthn仕様書関連個所参照 )、 WebAuthn4Jでは構成証明ステートメントの検証をしないように AttestationStatementVerifier と
CertPathTrustworthinessVerifier を構成した WebAuthnManager のインスタンスを返却する
WebAuthnManager.createNonStrictWebAuthnManager ファクトリメソッドを用意しています。
もし、エンタープライズなユースケースで、認証デバイスの厳密な検証が要件である場合は、
WebAuthnManager クラスのコンストラクタを用いて AttestationStatementVerifier と CertPathTrustworthinessVerifier
の実装をコンストラクタインジェクションして構成して下さい。
3.1. 構成証明ステートメントの検証
構成証明ステートメントの検証は、 AttestationStatementVerifier インタフェースの実装クラスが提供します。 構成証明ステートメント毎に、対応する実装クラスが提供されていますので、必要なVerifierからなるListを
WebAuthnManager クラスのコンストラクタに指定して下さい。 例えば、 packed のみをサポートする場合は、 PackedAttestationStatementVerifier を唯一の要素とするListとし、 例えば、 packed, tpm をサポートする場合は、 PackedAttestationStatementVerifier と TPMAttestationStatementVerifier
からなるListを指定して下さい。
Attestation検証を行わない NoneAttestationStatementVerifier や NullPackedAttestationStatementVerifier と、 他の AttestationStatementVerifier を混在させるのはやめましょう。
検証を行わない NoneAttestationStatementVerifier などを、他の検証を行う AttestationStatementVerifier と混ぜてしまうと、Attestation検証迂回に使用される抜け穴となります。
3.1.1. 構成証明ステートメントの信頼性の検証
構成証明ステートメント自体の信頼性の検証は、証明書パスの検証、自己署名のパターンがありますが、 証明書パスの検証は CertPathTrustworthinessVerifier インタフェースの実装に検証が委譲されます。
WebAuthn4Jは CertPathTrustworthinessVerifier インタフェースの実装として DefaultCertPathTrustworthinessVerifier を提供しています。 DefaultCertPathTrustworthinessVerifier は TrustAnchorRepository インタフェースを通じて取得した TrustAnchor をトラストアンカーとして証明書パスの検証を行うことで構成証明ステートメントの信頼性の検証を行います。
3.1.2. トラストアンカーの解決
前節で TrustAnchorRepository インタフェースがトラストアンカーの取得に使用されると述べましたが、 TrustAnchorRepository インタフェースは AAGUID や attestationCertificateKeyIdentifier に基づいて TrustAnchor を返却するインタフェースです。
webauthn4j-core モジュールでは、 TrustAnchorRepository の実装として、KeyStoreTrustAnchorRepository を提供しています。
KeyStoreTrustAnchorRepository は、Java Key Storeファイルからトラストアンカーを取得します。なお、 KeyStoreTrustAnchorRepository は AAGUID や attestationCertificateKeyIdentifier に応じて異なる TrustAnchorを返却することはせず、Java Key Storeファイルに登録された証明書を全てトラストアンカーとして扱います。
FIDO Metadata Serviceを用いたトラストアンカーの取得
FIDO Metadata Statement関連の機能を提供する webauthn4j-metadata モジュールは実験的な提供段階です。
|
FIDO Allianceでは、FIDO Metadata Serviceという、認証デバイスのメタデータを配信するサービスを提供しています。
webauthn4j-metadata モジュールでは、TrustAnchorRepository の実装として MetadataBLOBBasedTrustAnchorRepository を提供しています。
MetadataBLOBBasedTrustAnchorRepository は FidoMDS3MetadataBLOBAsyncProvider と組み合わせることで
FIDO Metadata Serviceの公開する情報に基づいてトラストアンカーを構築することが可能です。
4. 詳細
4.1. クレデンシャル情報の表現
クレデンシャル情報を表現するインタフェースとして、 CredentialRecord インタフェースが存在します。 登録時に、 RegistrationData クラスが含む値を用いて CredentialRecord インタフェースの インスタンスを作成し、アプリケーションの作法に則り永続化してください。認証時の検証に必要となります。 なお、永続化する際は、検索する際の利便性を考え、credentialIdをキーに永続化すると良いでしょう。
CredentialRecord インタフェースの実装クラスを設計する際は、アプリケーションの要件に合わせて拡張すると良いでしょう。 典型的には、 CredentialRecord をユーザーが識別するための名前フィールドの追加などが考えられるでしょう。
4.2. CredentialRecordのシリアライズ、デシリアライズ
認証デバイスの登録時、CredentialRecord のインスタンスをデータベース等に永続化し、認証時に利用できるようにするのはアプリケーションの責務ですが、
CredentialRecord を構成する各メンバをシリアライズ、デシリアライズする際に使用できるクラスをWebAuthn4Jでは用意しています。 アプリケーションで永続化を実装する際の補助としてご利用ください。
4.2.1. attestedCredentialData
AttestedCredentialDataConverter で AttestedCredentialData を byte[] に変換したり、その逆の変換を行うことが出来ます。 なお、String として保存したい場合、さらに Base64UrlUtil を用いることで byte[] から Base64Url String に変換することが出来ます。
AttestedCredentialDataConverter attestedCredentialDataConverter = new AttestedCredentialDataConverter(objectConverter);
// serialize
byte[] serialized = attestedCredentialDataConverter.convert(attestedCredentialData);
// deserialize
AttestedCredentialData deserialized = attestedCredentialDataConverter.convert(serialized);
4.2.2. attestationStatement
AttestationStatement はインタフェースであり、PackedAttestationStatement や、AndroidKeyAttestationStatement など、フォーマットにあわせて複数の実装があります。
AttestationStatement のCBOR表現は具象型が何であるかについて自己記述性を持たない為、フォーマットは別途、別のフィールドとして永続化する必要があります。 そのため、CBORとしてシリアライズする際は、実際のattestationStatementのフィールドと、フォーマットのフィールドを持つエンベロープクラスを用意し、エンベロープクラスをシリアライズしなければなりません。
エンベロープクラス自体は、WebAuthn4Jライブラリでは提供していないため、以下の例を参考にアプリケーションコード側で実装下さい。
//serialize
AttestationStatementEnvelope envelope = new AttestationStatementEnvelope(attestationStatement);
byte[] serializedEnvelope = objectConverter.getCborMapper().writeValueAsBytes(envelope);
//deserialize
AttestationStatementEnvelope deserializedEnvelope = objectConverter.getCborMapper().readValue(serializedEnvelope, AttestationStatementEnvelope.class);
AttestationStatement deserializedAttestationStatement = deserializedEnvelope.getAttestationStatement();
class AttestationStatementEnvelope{
@JsonProperty("attStmt")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "fmt"
)
private AttestationStatement attestationStatement;
@JsonCreator
public AttestationStatementEnvelope(@JsonProperty("attStmt") AttestationStatement attestationStatement) {
this.attestationStatement = attestationStatement;
}
@JsonProperty("fmt")
public String getFormat() {
return attestationStatement.getFormat();
}
public AttestationStatement getAttestationStatement() {
return attestationStatement;
}
}
4.2.3. transports
JSON文字列として保存したい場合は ObjectConverter が使用できます。
String serializedTransports = objectConverter.getJsonMapper().writeValueAsString(transports);
4.3. DCAppleDeviceのシリアライズ、デシリアライズ
webauthn4j-appattestでは、CredentialRecord インタフェースの代わりに、DCAppleDevice インタフェースを実装したクラスを、構成証明の検証時とアサーションの検証時の間で永続化する必要があります。 概ねCredentialRecordのシリアライズ、デシリアライズで解説した方法でシリアライズ、デシリアライズが可能ですが、一点気を付ける必要がある点として、 webauthn4j-appattest独自のクラス(例えば AppleAppAttestAttestationStatement )のシリアライズ、デシリアライズを行う為に、
ObjectConverter は DeviceCheckCBORModule が登録されたものを使用する必要があります。
DeviceCheckCBORModule が登録された ObjectConverter は DeviceCheckManager#createObjectConverter で得ることが出来ます。
4.4. Safariで未サポートなJSON serialization APIsの代替
クイックスタートでは、 PublicKeyCredentialCreationOptions をパースするAPIとして PublicKeyCredential.parseCreationOptionsFromJSON が、
PublicKeyCredential をシリアライズするAPIとして PublicKeyCredential#toJSON が存在すると紹介しましたが、Safariでは18.4以降でしか利用できません。
代わりとして、GitHubが提供するnpmライブラリ、 github/@webauthn-json が提供する、 pony-fillを利用するのがお勧めです。
PublicKeyCredential.parseCreationOptionsFromJSON の代わりに、 parseCreationOptionsFromJSON が、
naviagator.credentials.create の代わりに create が提供されています。
import {
create,
parseCreationOptionsFromJSON,
} from "@github/webauthn-json/browser-ponyfill";
const response = await fetch("<endpoint path that returns PublicKeyCredentialCreationOptions as JSON>") //fetch PublicKeyCredentialCreationOptions as JSON string
const publicKeyCredentialCreationOptionsJSON = await response.json() // convert to JSONObject
const credentialCreationOptions = parseCreationOptionsFromJSON(publicKeyCredentialCreationOptionsJSON); // convert to PublicKeyCredentialCreationOptions
const publicKeyCredential = await create({ publicKey: credentialCreationOptions}); // create PublicKeyCredential
const registrationResponseJSON = publicKeyCredential.toJSON() // JSON object of publicKeyCredential
const registrationResponseJSONStr = JSON.stringify(registrationResponseJSON) // JSON string representation of publicKeyCredential
このpony-fillの create メソッドを利用して得られた publicKeyCredentialでは、 toJSON メソッドが利用可能です。
PublicKeyCredential.parseRequestOptionsFromJSON の代わりとしては、 parseRequestOptionsFromJSON が、
naviagator.credentials.get の代わりに get が提供されています。
import {
get,
parseRequestOptionsFromJSON,
} from "@github/webauthn-json/browser-ponyfill";
const response = await fetch("<endpoint path that returns PublicKeyCredentialRequestOptions as JSON>");
const publicKeyCredentialRequestOptionsJSON = await response.json();
const credentialGetOptions = parseRequestOptionsFromJSON(publicKeyCredentialRequestOptionsJSON);
const publicKeyCredential = await get({ publicKey: credentialGetOptions});
const authenticationResponseJSON = publicKeyCredential.toJSON()
const authenticationResponseJSONStr = JSON.stringify(authenticationResponseJSON)
4.5. モジュール構成
WebAuthn4Jは、以下のModuleから構成されます。
4.5.4. Metadata-Async: webauthn4j-metadata-async.jar
FIDO Metadata Serviceを用いたTrustAnchorの解決など、追加的な機能の非同期バージョンを提供します。
4.6. Origin検証
WebAuthn の「Origin 検証」とは、登録・認証の各セレモニー時点でクライアント(ブラウザ)が認識していた Origin が、RP が期待する Origin と一致するかをサーバー側で検証することを指します。 これは、クライアントが生成する clientData に内包される origin を根拠に、セレモニーが想定したページから正しく開始されたことを確認し、他オリジンからの不正な呼び出しを防止する目的があります。
また、iframe 配下でのクロスオリジンな操作(例: https://example.com のページに埋め込まれた https://rp.example.net の iframe 内でWebAuthn認証を実行するような操作)について、WebAuthn は原則として許容しません。 これは、同一オリジン原則に基づき、クリックジャッキング等のフレーミング攻撃や別オリジンからの恣意的な認証誘発を抑止するためです。
一方で、認証ウィジェットを別オリジンのサイトに iframe 埋め込みする構成(自社・パートナーサイトなど)のようなユースケースでは、適切に制約したうえでクロスオリジンの実行を許容したい場合があります。 Permissions-Policyディレクティブなどを使用して許可を宣言することで、クロスオリジンでのWebAuthnの使用をクライアントに対して許可することが可能ですが、 サーバーサイドでも適切に検証が必要であり、WebAuthn4J では、ServerProperty のビルダー API により、通常の origin(RP ページ)に対する検証に加えて、topOrigin(親ページ)の許容条件を宣言的に構成できます。 既定ではクロスオリジンは許可されません。クロスオリジンを許容する必要がある場合は、許容したい親ページを明示する topOrigin(…)/topOrigins(…)/topOriginPredicate(…) もしくは任意許可の anyTopOrigin() を設定してください。
4.6.1. origin の検証
-
指定方法
-
origin(…), origins(…), originPredicate(…): RP ページの許容オリジンを指定します。
-
-
既定の挙動
-
指定した条件に一致するオリジンのみを許容します。
-
-
使用例
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.build();
4.6.2. topOrigin の検証
-
目的
-
iframe 配下でのクロスオリジンな WebAuthn 操作において、最上位ドキュメント(親ページ)のオリジンを検証し、想定する埋め込み元のみを許容します。
-
-
指定方法
-
topOrigin(…), topOrigins(…), topOriginPredicate(…): 許可するトップオリジンを限定
-
anyTopOrigin(): すべてのトップオリジンを許可(利便性は高いがセキュリティ境界を広げるため慎重に使用)
-
-
既定の挙動
-
明示的に topOrigin を設定しない場合、クロスオリジンな iframe からの利用は許可されません。
-
クライアントから topOrigin が提供されない場合は、通常の origin 検証のみが行われます。
-
-
使用例(特定のトップオリジンのみ許可)
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.topOrigin(new Origin("https://parent.example.com"))
.build();
-
使用例(任意のトップオリジンを許可)
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.anyTopOrigin()
.build();
-
セキュリティ考慮
-
任意許可(anyTopOrigin)は攻撃面の拡大につながる可能性があります。可能な限り topOrigin/topOrigins/topOriginPredicate により許可範囲を限定してください。
-
-
後方互換
-
旧来の包括的許可フラグは後方互換のために残されていますが、新規の構成では topOriginPredicate(または anyTopOrigin)を使用してください。
-
4.7. カスタムな検証ロジックの実装
WebAuthn4Jでは、カスタムな検証ロジックを実装し、追加することが可能です。 登録時の検証にカスタムロジックを追加する場合は、 CustomRegistrationVerifier を実装してください。 認証時の検証にカスタムロジックを追加する場合は、 CustomAuthenticationVerifier を実装してください。
4.8. カスタムなデータ変換ロジックの実装
WebAuthn4Jでは、JSONやCBORのシリアライズ、デシリアライズ処理にJackson 3ライブラリを使用しています。
カスタムなシリアライザ、デシリアライザを使用するには、それらを登録したJackson 3の JsonMapper 、 CBORMapper をWebAuthn4Jに渡す必要があります。
4.8.1. カスタムなデータ変換ロジックの登録
WebAuthn4Jは、Jackson 3の JsonMapper, CBORMapper のセットを、 ObjectConverter というクラスのインスタンスを通じて受け取ります。
カスタムなシリアライザ、デシリアライザを登録した JsonMapper, CBORMapper を ObjectConverter インスタンス作成時にコンストラクタからインジェクトし、
その ObjectConverter を WebAuthnManager のインスタンス作成時にパラメータとして指定してください。
4.8.2. 外部モジュールからのWebAuthn拡張の追加
WebAuthn4Jでは、webauthn4j-core外部のモジュールから独自のWebAuthn拡張を定義し、coreの変更なしに追加できます。
AuthenticationExtensionsClientOutputs 等の拡張コンテナは、内部的に生データを ObjectNode として保持しており、
各拡張のデシリアライズはJackson Moduleに登録されたデシリアライザを通じて遅延的に行われます。
外部モジュールは、自前のJackson Moduleに拡張のデシリアライザを登録し、そのModuleを含む ObjectConverter をWebAuthn4Jに渡すことで、独自の拡張を追加できます。
Client Extension Output(JSON)の追加例
以下は、架空の拡張 exampleExtension を外部モジュールから追加する例です。
この拡張は以下のように定義されているものとします。
partial dictionary AuthenticationExtensionsClientOutputs {
ExampleExtensionOutput exampleExtension;
};
dictionary ExampleExtensionOutput {
required USVString exampleValue;
boolean exampleFlag;
};
この定義に基づくと、Client Extension Outputは以下のようなJSONとして返されます。
{
"exampleExtension": {
"exampleValue": "hello",
"exampleFlag": true
}
}
以下に、この拡張をWebAuthn4Jで扱えるようにする手順を示します。
1. 拡張のデータクラスを定義する
public class ExampleExtensionClientOutput
implements AuthenticationExtensionClientOutput {
private final String exampleValue;
private final Boolean exampleFlag;
@JsonCreator
public ExampleExtensionClientOutput(
@JsonProperty("exampleValue") String exampleValue,
@JsonProperty("exampleFlag") Boolean exampleFlag) {
this.exampleValue = exampleValue;
this.exampleFlag = exampleFlag;
}
@Override
public @NotNull String getIdentifier() {
return "exampleExtension";
}
public @NotNull String getExampleValue() {
return exampleValue;
}
public @Nullable Boolean getExampleFlag() {
return exampleFlag;
}
@Override
public void validate() {
if (exampleValue == null) {
throw new ConstraintViolationException("exampleValue must not be null");
}
}
}
2. ExtensionClientOutputDeserializer を継承したデシリアライザを実装する
デシリアライザは AuthenticationExtensionsClientOutputs 全体の ObjectNode を受け取り、
自分が担当するキー("exampleExtension")の値を取り出して変換します。
キーが存在しない場合は null を返します。
public class ExampleExtensionClientOutputDeserializer
extends ExtensionClientOutputDeserializer<ExampleExtensionClientOutput> {
public ExampleExtensionClientOutputDeserializer() {
super(ExampleExtensionClientOutput.class);
}
@Override
public @NotNull Class<ExampleExtensionClientOutput> getType() {
return ExampleExtensionClientOutput.class;
}
@Override
public @NotNull Set<String> getKeys() {
return Set.of("exampleExtension");
}
@Override
public ExampleExtensionClientOutput deserialize(
JsonParser p, DeserializationContext ctxt) {
ObjectNode node = (ObjectNode) p.readValueAsTree();
JsonNode value = node.get("exampleExtension");
if (value == null || value.isNull()) return null;
// Construct directly from JSON fields rather than using readTreeAsValue
// with the same class, which would recursively invoke this deserializer.
String exampleValue = value.has("exampleValue") ? value.get("exampleValue").asText() : null;
Boolean exampleFlag = value.has("exampleFlag") ? value.get("exampleFlag").asBoolean() : null;
return new ExampleExtensionClientOutput(exampleValue, exampleFlag);
}
}
3. Jackson Moduleを作成し、デシリアライザを登録する
public class ExampleExtensionModule extends SimpleModule {
public ExampleExtensionModule() {
super("ExampleExtensionModule");
this.addDeserializer(ExampleExtensionClientOutput.class,
new ExampleExtensionClientOutputDeserializer());
}
}
4. このModuleを含む ObjectConverter を作成し、 WebAuthnManager に渡す
ObjectConverter は渡された JsonMapper / CBORMapper に WebAuthnJSONModule / WebAuthnCBORModule を
自動的に追加するため、外部モジュールのみ登録すればよい。
JsonMapper jsonMapper = JsonMapper.builder()
.addModule(new ExampleExtensionModule())
.build();
ObjectConverter objectConverter = new ObjectConverter(jsonMapper, new CBORMapper());
5. getExtension(Class) で型安全に取得する
ExampleExtensionClientOutput example =
clientOutputs.getExtension(ExampleExtensionClientOutput.class);
String value = example.getExampleValue();
その他の拡張ポイント
同様のパターンで、以下の拡張も追加できます。
| 種別 | 基底デシリアライザクラス | 登録先 |
|---|---|---|
Client Extension Input |
|
|
Authenticator Extension Output |
|
|
Authenticator Extension Input |
|
|
4.9. クラス
4.9.2. Converter, WebAuthnModule
データパッケージ配下のクラスはJacksonによってシリアライズ、デシリアライズ可能なように設計されています。 一部のクラスはカスタムなシリアライザ、デシリアライザが必要であり、 converter パッケージ配下に集約されています。 カスタムシリアライザ、デシリアライザは WebAuthnJSONModule と WebAuthnCBORModule というJacksonのModuleにまとめられています。 ObjectConverter は内部で使用するJacksonの JsonMapper, CBORMapper に自動で WebAuthnJSONModule と WebAuthnCBORModule を適用しますが、ObjectConverter の外部で WebAuthn4Jのシリアライザ、デシリアライザを使用したい場合は、Jacksonの JsonMapper, CBORMapper に WebAuthnJSONModule と WebAuthnCBORModule を登録すると良いでしょう。
FIDO Metadata Service (MDS) の型(AAID、AuthenticatorStatus 等)を扱う場合は、WebAuthnMetadataJSONModule が対応するシリアライザ、デシリアライザを提供します。
現在、MetadataBLOBFactory、LocalFilesMetadataStatementsProvider、LocalFilesMetadataStatementsAsyncProvider 等のメタデータエントリーポイントクラスは、WebAuthnMetadataJSONModule が未登録の場合にフォールバックのシリアライザ、デシリアライザを自動登録します(警告ログが出力されます)。
将来のリリースでは WebAuthnMetadataJSONModule の明示的な登録が必須になる予定です。
モジュールの登録には ObjectConverter.rebuildWithJSONModule を使用してください:
ObjectConverter objectConverter = new ObjectConverter()
.rebuildWithJSONModule(new WebAuthnMetadataJSONModule());
4.9.3. TrustAnchorsResolver
TrustAnchorsResolver インタフェースは TrustAnchorCertPathTrustworthinessVerifier で構成証明ステートメントの信頼性の 検証を行う際に信頼するルート証明書のセットを探索するために使用されます。
4.9.4. TrustAnchorsProvider
TrustAnchorsProvider インタフェースは前述の TrustAnchorsResolver インタフェースの実装である TrustAnchorsResolverImpl
がTrustAnchorの読込処理を委譲する先のインタフェースです。実装としてJava Key StoreファイルからTrustAnchorを読み込む
KeyStoreFileTrustAnchorsProvider クラスが提供されている他、WebAuthn4J Spring Securityでは、SpringのResourceから TrustAnchorを読み込む CertFileResourcesTrustAnchorProvider が提供されています。
4.10. WebAuthn以外のFIDO CTAP2セキュリティキーを用いた独自アプリケーションでの利用
FIDO CTAP2セキュリティキーにとって、WebAuthnは一つの応用例でしかなく、セキュアな認証を必要とする独自アプリケーションで セキュリティキーを利用することも可能です。本節では、FIDO CTAP2セキュリティキーを用いた独自アプリケーションにおけるAttestation、Assertion検証でWebAuthn4Jを利用する方法を説明します。
4.10.1. FIDO CTAP2セキュリティキーを用いた独自アプリケーションでの登録、認証のフロー
FIDO CTAP2セキュリティキーを独自アプリケーションで認証に使用する場合、セキュリティキーを登録するために、 アプリからFIDO CTAP2セキュリティキーの authenticatorMakeCredential メソッドを呼び出し、公開鍵やデバイスの構成情報を 含むデータ(構成証明、Attestation)を取得し保存します。 取得されたAttestationは、セキュリティキーがアプリとして受け入れ可能なキーか判定するために検証が必要です。 WebAuthn4Jでは、 CoreRegistrationVerifier クラスを用いることで、取得されたAttestationを検証可能です。
認証時には、同様にアプリからFIDO CTAP2セキュリティキーの authenticatorGetAssertion メソッドを呼び出し、認証時にサーバーに送信される署名を含んだデータ(アサーション、Assertion)を取得します。 取得されたAssertionを検証することで、アプリは認証に用いられたセキュリティキーが、登録時に用いられたセキュリティキーと同一であることを確認し、正当なアクセスか判定することが可能となります。WebAuthn4Jでは、 CoreAuthenticationVerifier クラスを用いることで、取得されたAssertionを検証可能です。
4.10.2. アプリケーション固有のクライアントデータの真正性の担保、検証
上記のフローに従って実装することで、FIDO CTAP2セキュリティキーを用いた安全な認証が実現可能ですが、 FIDO CTAP2セキュリティキーを呼び出す主体(クライアント)と、Attestation、Assertionを検証する主体(サーバー)が分離している場合、クライアントが登録、認証時にアプリケーション固有のクライアントデータを生成し、クライアントデータを追加でサーバーで検証したい場合もあります。クライアントデータ自体はAttestation、Assertionと一緒に送信すれば良いですが、 クライアントデータを中間者攻撃から防御するために、クライアントデータに対して署名を行い、保護する必要があります。
さて、FIDO CTAP2では、登録時に利用する authenticatorMakeCredential メソッドと認証時に利用する authenticatorGetAssertion メソッド 、どちらにも共通するパラメータとして、clientDataHash というパラメータが存在します。セキュリティキーは、受け取った clientDataHash パラメータを署名対象のデータの一部として署名を生成するため、アプリケーションとして署名で保護したいクライアントデータのハッシュを取得し、
clientDataHash にセットすることで、アプリケーション固有のクライアントデータが改竄されていない真正なデータか、サーバー側で検証することが出来ます。
5. FAQ
5.1. PublicKeyCredentialCreationOptions の excludeCredentials に何を指定すべきか
PublicKeyCredentialCreationOptions の excludeCredentials は登録時に除外したい Authenticator を指定するためのパラメータです。
アカウントに既に紐付いている Authenticator を新規登録から除外する際に使用します。
// 除外する Authenticator がない場合
List<PublicKeyCredentialDescriptor> excludeCredentials = null;
// 特定の Authenticator を除外する場合
List<PublicKeyCredentialDescriptor> excludeCredentials = Collections.singletonList(
new PublicKeyCredentialDescriptor(
PublicKeyCredentialType.PUBLIC_KEY,
existingCredentialId,
transports
)
);
-
nullを指定した場合、除外される Authenticator はありません
-
そのユーザーがすでに持っている認証器を excludeCredentials に指定することで、誤って同じ認証器を再登録することを防止できます
5.2. PublicKeyCredentialRequestOptions の allowCredentials に何を指定すべきか
PublicKeyCredentialRequestOptions の allowCredentials は認証時に使用可能な Authenticator を指定するためのパラメータです。 non-discoverable credentialの場合、allowCredentialsにcredentialIdを指定する必要があります。
// 全てのAuthenticatorを許可する場合
List<PublicKeyCredentialDescriptor> allowCredentials = null;
// 特定のAuthenticatorのみを許可する場合
List<PublicKeyCredentialDescriptor> allowCredentials = Collections.singletonList(
new PublicKeyCredentialDescriptor(
PublicKeyCredentialType.PUBLIC_KEY,
credentialId,
transports
)
);