こんにちは。Yahoo! JAPANの浜田です。
普段は主にYahoo! JAPANの認証・ログイン関連機能、特にWebAuthn対応機能のサーバーサイドの開発業務を行っています。
今回はWebAuthn仕様に定義されているattestation formatの1つのpacked
に関して、主にサーバーサイドの動作についてご紹介します。
packedはWebAuthnにおいて標準的なattestation formatであるため、attestationの処理の基本的なところを知るのに良いformatです。
Attestationそのものの解説はこちらの記事でも書かれていますので、併せてご参照ください。
用語集
初めに、本稿で用いる用語を説明します。
用語 | 説明 |
---|---|
認証器 | 様々な認証を行う機能。WebAuthn仕様では認証手段は生体認証に限らず、ボタンを押す、といった操作も許可されており、認証器の実装に依存する。公開鍵暗号方式の鍵ペアの生成、秘密鍵の保存なども行う。Authenticator。 |
サービス事業者 | Relying Partyのこと。WebAuthnの公開鍵登録・認証などを行うサーバーサイドのこと。 |
Authenticator Data | 認証器が生成するデータ。サービス事業者に登録する公開鍵、authenticatorの認証レベル、認証回数などの情報を含む。 |
Attestation Statement | 認証器の妥当性の検証、認証器からのリクエストの改ざん検知などを行うためのデータ。 |
Attestation Format | Attestation statementのフォーマット。複数定義されている。 |
Attestationの概要
Attestationは、認証器、端末の正当性を証明するための記述です。
認証器で、attestation用の秘密鍵・公開鍵のペア、署名作成用のデータ、を元に署名を作成し、attestation formatの仕様に沿って、公開鍵、署名をattestation statementに定義し、サービス事業者に送ります。
サービス事業者では、attestation statement内の公開鍵、署名を取り出し、クライアントからのパラメーターから署名作成用のデータを生成し、それらの値を使って署名検証を行います。
Attestationには執筆時点で6種類のattestation formatがあり、packedは基本的なattestation formatです。
Packedとは
Packedは、WebAuthn仕様に最適化されたattestation formatで、コンパクトながらも拡張性があります。
Packedフォーマットの特徴は、attestationの検証が署名検証のみで行われることです。
そのため、opensslなどが利用できる環境であれば実装できます。
このことから、opensslの新しいアルゴリズムが追加された場合もアルゴリズムへの対応を行えば十分です。
Attestation statement解説
署名作成・検証の前に、packedフォーマットのattestation statementの構造を説明します。packedフォーマットのattestation statementは下記の様な構造です。
全体の構造
$$attStmtType //= (
fmt: "packed",
attStmt: packedStmtFormat
)
packedStmtFormat = {
alg: COSEAlgorithmIdentifier,
sig: bytes,
x5c: [ attestnCert: bytes, * (caCert: bytes) ]
} //
{
alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
sig: bytes,
ecdaaKeyId: bytes
} //
{
alg: COSEAlgorithmIdentifier
sig: bytes,
}
$$attStmtType
内のfmt
キーがフォーマットの識別子になっており、この場合は文字列packed
が指定されています。attStmt
キーは後半のpackedStmtFormat
を指定しています。packedStmtFormat
には3つの構造が記載されていますが、認証器はこの内の1つだけを選択してattestationを送ります。そのため、サービス事業者はどのattestation
statementが送られてきているのかを判断する必要があります。
attestation statementの各項目解説
-
alg
署名作成のアルゴリズムが指定されます。指定には-7
のような数字を使い、これはIANA COSE Algorithms registryに定義されているものが利用されます。ただし、WebAuthn仕様ではこの値がIANA COSE Algorithms registryに登録されているべき(SHOULD)となっているため、例外もあり得ます。 -
sig
作成した署名です。バイナリで指定されます。 -
x5c
attestnCert
とそれ自身の証明書チェーンの配列です。attestnCert
は必須で、必ず配列の最初の要素である必要がありますが、証明書チェーンの指定は任意です。 -
attestnCert
X.509フォーマットのアテステーション証明書です。前述のsig
はこの証明書から取得できる公開鍵で署名検証が可能です。 -
ecdaaKeyId
FIDOで新たに定義されている、ECDAAアルゴリズムの公開鍵です。執筆時点でECDAAアルゴリズムの仕様はドラフト版です。
3パターンの構造
Packedのattestation statementは3つのパターンに分けられます。パターンは署名検証に使う公開鍵の違いに依存します。
- x5c
証明書(attestnCert)から取得できる公開鍵を利用するパターン
$$attStmtType //= (
fmt: "packed",
attStmt: packedStmtFormat
)
packedStmtFormat = {
alg: COSEAlgorithmIdentifier,
sig: bytes,
x5c: [ attestnCert: bytes, * (caCert: bytes) ]
}
- ecdaaKeyId
ECDAAアルゴリズムの公開鍵を利用するパターン
$$attStmtType //= (
fmt: "packed",
attStmt: packedStmtFormat
)
packedStmtFormat = {
alg: alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
sig: bytes,
ecdaaKeyId: bytes
}
- self attestation
上記2パターン以外authenticator data内の公開鍵を利用するパターン
$$attStmtType //= (
fmt: "packed",
attStmt: packedStmtFormat
)
packedStmtFormat = {
alg: COSEAlgorithmIdentifier
sig: bytes,
}
x5c、ecdaaKeyIdはattestation statementから公開鍵を取得できますが、self attestationの場合はauthenticator data内の公開鍵を利用する必要があります。
署名作成・検証について
認証器は前述の3パターンの内のどのattestation statementを利用するかを選択し、適切に値を整形します。サービス事業者はx5c、ecdaaKeyId、self attestationのどのパターンに当てはまるattestation statementなのかを適切にハンドリングし、署名検証を行います。ハンドリングの例は下記です。
- Attestation statementに
x5c
が含まれるかつ、ecdaaKeyId
が含まれない場合、x5c
のattestation statementx5c
の最初の値の証明書から公開鍵を取得- 取得した公開鍵、alg、sig、署名作成用データで署名検証
- 署名検証が成功した場合は処理続行、失敗の場合は処理終了
- Attestation statementに
ecdaaKeyId
が含まれるかつ、x5c
が含まれない場合、ecdaaKeyId
のattestation statementecdaaKeyId
から公開鍵を取得- 取得した公開鍵、alg、sig、署名作成用データで署名検証
- 署名検証が成功した場合は処理続行、失敗の場合は処理終了
- Attestation statementに
x5c
もecdaaKeyId
も含まれない場合、self attestation
のattestation statement- authenticator dataから公開鍵を取得
- 取得した公開鍵、alg、sig、署名作成用データで署名検証
- 署名検証が成功した場合は処理続行、失敗の場合は処理終了
x5c
、ecdaaKeyId
のどちらも含まれる場合はattestation statementとして正しくないので、サービス事業者はエラーとして処理を中断すべきです。
署名作成用データ
署名作成用データは3パターン共通して同じものが利用されます。署名作成用データは下記の要領で生成します。
- Authenticator dataを取得。これを以後
authenticatorData
とする。 - ClientDataのSHA-256のハッシュ値を取得。これを以後
clientDataHash
とする。 authenticatorData
とclientDataHash
を結合。
authenticatorData
にはRPIDのハッシュ、clientDataJSONにはチャレンジ、originを含んでいます。そのため、サービス事業者ごと、サービス事業者とのやりとりごとにユニークな値になり、署名作成用データとして適した値になります。
注意点としては、clientDataHash
の元になるClientDataはJSON形式のため、区切り文字の,(カンマ)ごとに改行や半角スペースが含まれている可能性があります。例として、サービス事業者の以下のような処理を考えます。
- 改行が含まれているClientDataをJSONから配列にデコードし、変数
$clientData
として保持 - 署名作成用データ生成のために、変数
$clientData
を配列からJSONにエンコードし、$clientDataJSON
として保持 $clientDataJSON
のSHA-256のハッシュ値を$clientDataHash
として保持- authenticator dataの変数
$authenticatorData
と$clientDataHash
を結合
この場合、開発言語にもよりますが、2の処理にて改行が消えてしまい、3のハッシュ値は期待している値になりません。ブラウザーの実装によってはClientDataに改行があったりなかったりしますので、サービス事業者ではこうした注意点を包含する署名検証を実装する必要があります。
まとめ
今回はattestation formatの1つであるpackedについてご紹介しました。
Packedは公開鍵暗号方式によるセキュリティーを保ちつつ、比較的コンパクトにまとまっている、WebAuthnにおいて標準的なattestation
formatです。
ただし、署名作成のデータにJSONを含んでいるため、正しく署名検証を行っているつもりでも失敗してしまう、ということがある点にも注意が必要です。
本稿をWebAuthnの実装に役立てていただければ幸いです。
関連記事
- 「安全・安心・便利」FIDO(ファイド)を使った パスワードレスログインとは
(2019年2月20日 Yahoo! JAPANコーポレートブログ) - ヤフーが世界で初めて、「FIDO2」を活用したパスワード不要のウェブサービスを実現!(2019年4月15日 linotice)
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました