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.28.5.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
をパースすることが可能です。
但し、2024/12現在、Safariでは 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 = new ServerProperty(origin, rpId, challenge);
// 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を指定して下さい。WebAuthnでは、ブラウザが認識しているOriginをClientDataに書き込んで署名を行います。WebAuthn4Jは書き込まれたOriginが指定されたOriginと合致するかを検証することで、 フィッシング攻撃を防ぎます。 -
rpId
にはWebAuthnによる認証を提供するサイトのrpIdを指定して下さい。rpIdは資格情報のスコープを指定するパラメータです。 詳しくは WebAuthnの仕様書のrpIdの項 を参照して下さい。 -
challenge
には発行したChallengeを指定して下さい。challenge
はリプレイ攻撃を防ぐ為のパラメータです。 サーバー側でchallenge
としてランダムなバイト列を生成し、フロントエンド側でWebAuthn JS APIを実行する際に パラメータとして指定して署名対象に含め、サーバー側で値の一致を検証することで、リプレイ攻撃からユーザーを防御することが出来ます。 発行したChallengeを検証時まで永続化しておくのはWebAuthn4Jライブラリ呼出側の責務です。セッションなどに格納しておくと良いでしょう。
検証に成功した場合は、返却された値から CredentialRecord
インスタンスを作成し、データベース等へアプリケーション側で永続化して下さい。 認証時に使用します。
永続化方法について詳しくは、 CredentialRecordのシリアライズ、デシリアライズ を参照して下さい。
検証に失敗した場合は、 VerificationException
のサブクラスの例外が発生します。
2.4. WebAuthn4Jを利用した認証処理の実装
2.4.1. WebAuthnのアサーションの生成
WebAuthnでの認証時において中心となるAPIは、ブラウザの navigator.credentials.get
メソッドです。
認証のフローの図の通り、認証処理においても、まずバックエンドサーバー側でチャレンジを生成し、セッションに保存する一方、クライアントにチャレンジを引き渡す必要があります。
navigator.credentials.get
メソッドのパラメータにも challenge
が存在するためです。
バックエンドサーバーからフロントエンド(クライアント)への認証処理のチャレンジの受け渡し方法もWebAuthn仕様では定められていません。登録処理同様、お好みの方法でチャレンジをフロントエンド側に引き渡して下さい。
navigator.credentials.get
メソッドのパラメータである、 PublicKeyCredentialGetOptions
をパースするJava Script APIは、 PublicKeyCredential.parseCreationGetOptionsFromJSON
です。 PublicKeyCredential.parseCreationGetOptionsFromJSON
がSafariで利用できない問題の代替案はSafariで未サポートなJSON serialization APIsの代替 を参照してください。
navigator.credentials.get
メソッドに指定できるその他のオプションに関しては、 MDN: CredentialsContainer: get() メソッドを参照下さい。
PublicKeyCredentialGetOptions
全体を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 = new ServerProperty(origin, rpId, challenge);
// 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.28.5.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.getCborConverter().writeValueAsBytes(envelope);
//deserialize
AttestationStatementEnvelope deserializedEnvelope = objectConverter.getCborConverter().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.getJsonConverter().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
が存在すると紹介しましたが、2024/12現在、Safariでは利用できません。
代わりとして、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. カスタムな検証ロジックの実装
WebAuthn4Jでは、カスタムな検証ロジックを実装し、追加することが可能です。 登録時の検証にカスタムロジックを追加する場合は、 CustomRegistrationVerifier
を実装してください。 認証時の検証にカスタムロジックを追加する場合は、 CustomAuthenticationVerifier
を実装してください。
4.7. カスタムなデータ変換ロジックの実装
WebAuthn4Jでは、JSONやCBORのシリアライズ、デシリアライズ処理にJacksonライブラリを使用しています。 Client ExtensionやAuthenticator Extensionのデータ変換でカスタムな変換を行いたい場合、WebAuthn4Jが内部で使用している Jacksonの ObjectMapper
にカスタムなシリアライザ、デシリアライザを登録することで実現できます。
4.8. クラス
4.8.2. Converter, WebAuthnModule
データパッケージ配下のクラスはJacksonによってシリアライズ、デシリアライズ可能なように設計されています。 一部のクラスはカスタムなシリアライザ、デシリアライザが必要であり、 converter
パッケージ配下に集約されています。 カスタムシリアライザ、デシリアライザは WebAuthnJSONModule
と WebAuthnCBORModule
というJacksonのModuleにまとめられています。 WebAuthn4Jは内部で使用するJacksonの ObjectMapper
に自動で WebAuthnModule
を適用しますが、WebAuthnManager
の外部で WebAuthn4Jのシリアライザ、デシリアライザを使用したい場合は、Jacksonの ObjectMapper
に WebAuthnJSONModule
と WebAuthnCBORModule
を登録すると 良いでしょう。
4.8.3. TrustAnchorsResolver
TrustAnchorsResolver
インタフェースは TrustAnchorCertPathTrustworthinessVerifier
で構成証明ステートメントの信頼性の 検証を行う際に信頼するルート証明書のセットを探索するために使用されます。
4.8.4. TrustAnchorsProvider
TrustAnchorsProvider
インタフェースは前述の TrustAnchorsResolver
インタフェースの実装である TrustAnchorsResolverImpl
がTrustAnchorの読込処理を委譲する先のインタフェースです。実装としてJava Key StoreファイルからTrustAnchorを読み込む
KeyStoreFileTrustAnchorsProvider
クラスが提供されている他、WebAuthn4J Spring Securityでは、SpringのResourceから TrustAnchorを読み込む CertFileResourcesTrustAnchorProvider
が提供されています。
4.9. WebAuthn以外のFIDO CTAP2セキュリティキーを用いた独自アプリケーションでの利用
FIDO CTAP2セキュリティキーにとって、WebAuthnは一つの応用例でしかなく、セキュアな認証を必要とする独自アプリケーションで セキュリティキーを利用することも可能です。本節では、FIDO CTAP2セキュリティキーを用いた独自アプリケーションにおけるAttestation、Assertion検証でWebAuthn4Jを利用する方法を説明します。
4.9.1. FIDO CTAP2セキュリティキーを用いた独自アプリケーションでの登録、認証のフロー
FIDO CTAP2セキュリティキーを独自アプリケーションで認証に使用する場合、セキュリティキーを登録するために、 アプリからFIDO CTAP2セキュリティキーの authenticatorMakeCredential メソッドを呼び出し、公開鍵やデバイスの構成情報を 含むデータ(構成証明、Attestation)を取得し保存します。 取得されたAttestationは、セキュリティキーがアプリとして受け入れ可能なキーか判定するために検証が必要です。 WebAuthn4Jでは、 CoreRegistrationVerifier
クラスを用いることで、取得されたAttestationを検証可能です。
認証時には、同様にアプリからFIDO CTAP2セキュリティキーの authenticatorGetAssertion メソッドを呼び出し、認証時にサーバーに送信される署名を含んだデータ(アサーション、Assertion)を取得します。 取得されたAssertionを検証することで、アプリは認証に用いられたセキュリティキーが、登録時に用いられたセキュリティキーと同一であることを確認し、正当なアクセスか判定することが可能となります。WebAuthn4Jでは、 CoreAuthenticationVerifier
クラスを用いることで、取得されたAssertionを検証可能です。
4.9.2. アプリケーション固有のクライアントデータの真正性の担保、検証
上記のフローに従って実装することで、FIDO CTAP2セキュリティキーを用いた安全な認証が実現可能ですが、 FIDO CTAP2セキュリティキーを呼び出す主体(クライアント)と、Attestation、Assertionを検証する主体(サーバー)が分離している場合、クライアントが登録、認証時にアプリケーション固有のクライアントデータを生成し、クライアントデータを追加でサーバーで検証したい場合もあります。クライアントデータ自体はAttestation、Assertionと一緒に送信すれば良いですが、 クライアントデータを中間者攻撃から防御するために、クライアントデータに対して署名を行い、保護する必要があります。
さて、FIDO CTAP2では、登録時に利用する authenticatorMakeCredential メソッドと認証時に利用する authenticatorGetAssertion メソッド 、どちらにも共通するパラメータとして、clientDataHash
というパラメータが存在します。セキュリティキーは、受け取った clientDataHash
パラメータを署名対象のデータの一部として署名を生成するため、アプリケーションとして署名で保護したいクライアントデータのハッシュを取得し、
clientDataHash
にセットすることで、アプリケーション固有のクライアントデータが改竄されていない真正なデータか、サーバー側で検証することが出来ます。