ふと思ったのですが、YubiKeyってどういう原理で認証しているのでしょうか。

2023年にYubiKey 5C NFCを使い始めて、その後USB-Aのモデルも買い足しました。当時は1Passwordのアカウントを守るために導入し、持ち歩く用と家に置いておく予備を登録する、という使い方を考えていました。

USBに挿して、必要なときに触れる。スマートフォンならNFCでかざす。使う側の操作はかなり少ないのですが、その裏で何をやっているのかは、ちゃんと整理したことがありませんでした。

あの金色の部分は指紋を見ているのか。そもそも電池がないのに、どうやって動いているのか。

今回はYubicoの公式資料と認証の仕様を読みながら、この辺りを調べてみました。手元のキーを分解したり、暗号処理を実測したりした記事ではありません。普段のログイン操作を、仕組みの側から見直した記録です。

YubiKeyの対応方式

YubiKeyはハードウェアの名前で、内部では複数の認証方式を扱えます。そのときに何を使ったかで、やり取りする情報も安全性の性質も変わります。

私が買ったYubiKey 5シリーズには、FIDO2、FIDO U2F、OATH、Yubico OTP、PIV、OpenPGPといった機能があります。全部が同じ暗号処理をしているわけではありません。公式の機能一覧を見ると、小さな本体の中に用途の違うアプリケーションが入っている、と考える方が近そうです。

代表的な違いを並べると、こうなります。

方式何を使って確かめるか利用時に外へ出るもの
FIDO2 / U2F公開鍵と対になる秘密鍵で署名する認証要求に対する署名など
OATH-TOTP / HOTPサーバーと共有した秘密からコードを計算する6桁や8桁などの認証コード
Yubico OTP共通鍵で暗号化した、カウンターなどを含むデータ長い文字列のワンタイムパスワード
PIV / OpenPGP保管した秘密鍵を使って暗号処理をする署名や復号結果など、操作に応じた応答

ブラウザに「触れてください」と言われるときと、認証アプリで6桁の数字を表示するときは別の仕組みです。ここを混ぜると、サーバーに秘密を渡すのか、という問いの答えまで変わります。

なお、Security Keyシリーズのように、FIDOに機能を絞った製品もあります。

まずは、Webのセキュリティキーやパスキーで使うFIDO2を中心に見ていきます。

公開鍵と秘密鍵

FIDO系の認証で中心になるのは、公開鍵暗号を使った電子署名です。

公開鍵と秘密鍵は、対になる二つの鍵です。秘密鍵を使ってデータに署名し、サービスに渡した公開鍵を使ってその署名が正しいかを検証します。

電子署名は、相手だけに内容を読ませる暗号化とは目的が違います。確認したいのは、対象のデータについて、対応する秘密鍵を使える側が署名したかどうかです。

YubiKeyをFIDOの認証器として使う場合、サービスには公開鍵を登録し、秘密鍵を使う処理はキーの内部で行います。サービスはログインのたびに署名を確認することで、登録した鍵を今使える相手なのかを確かめます。YubicoのFIDO2開発者向けガイドでは、この構成と各参加者の役割が説明されています。

パスワードとの大きな違いは、ログインの際に、繰り返し使える秘密そのものをサービスへ渡さなくてよいことです。

公開鍵だけを見て同じように署名することはできません。公開鍵から秘密鍵を求めるのが現実的な計算量では困難になるよう、暗号方式と鍵の大きさが選ばれています。

メールアドレスなどの漏えいや、サーバー自体の侵害は別の問題です。ここで減らせるのは、保存していた公開鍵の漏えいが、そのまま本人として署名できる能力の漏えいになる危険です。

キーの登録

ログインの前には、キーの登録が必要です。サービスの設定画面で「セキュリティキーを追加する」といった操作をする部分ですね。

登録では、サービスからブラウザへ、ランダムな値やサービスの識別情報、アカウントに関する情報、利用できる暗号方式などが渡されます。ブラウザやOSはそれを認証器へ伝え、キーが認証用の鍵を生成します。返ってきた公開鍵やcredential IDなどを、サービスがそのアカウントに関連付けて保存します。登録の流れは、Yubicoの資料にも載っています。

credential IDは、使う認証資格情報を識別する値です。同じ物理キーで複数のサイトに登録できるのは、このような資格情報をサイトごと、登録ごとに扱っているからです。

このときサービス側がしているのは、「このアカウントには、この公開鍵で検証できる認証を許可する」という登録です。YubiKeyの製品名だけを見て本人扱いするわけではありません。

同じモデルのキーをもう一つ買ってきても、登録していなければログインできません。

登録する時点で、アカウントの持ち主も確認する必要があります。認証器は、誰のアカウントなのかを最初から知っているわけではありません。サービスが本人を確認してから、今後使う鍵を結び付けます。だから、鍵の追加を許す操作も大事ですね。

ログイン時のやり取り

登録が終わると、ログイン時にはサービスが新しいランダムな値を用意します。これはchallenge、チャレンジと呼ばれます。

「この値を含むデータに、登録した鍵で署名してください」という問いを出して、正しい署名が返ってくるか確かめるわけです。毎回違う問いを出すので、以前の正解をそのまま持ってきても、今回の問いへの答えにはなりません。Yubicoの認証処理の説明には、チャレンジと返される情報が整理されています。

たとえば、昨日のログインでは「A」という値に対する署名が正しかったとします。今日サービスが出した値が「B」なら、昨日の「A」への署名を送っても、今日の認証には使えません。署名を検証するときには、鍵が合うことに加えて、今回発行したチャレンジが応答に含まれていることも確認します。

新しい問いに答えられることが、その時点で鍵を使える証拠になります。コピーした過去の通信だけでは、新しい応答を作れません。

サービスは十分に予測しにくいチャレンジを生成し、どの認証操作に発行したのかを管理し、使用済みや期限切れの応答を受け付けないようにします。

操作の流れをかなり省略すると、こうなります。

sequenceDiagram
    participant S as サービス
    participant B as ブラウザ・OS
    participant Y as YubiKey
    participant U as 利用者
    S->>B: 今回のチャレンジと認証条件
    B->>B: 呼び出し元と認証先を検査
    B->>Y: 認証先のIDと署名対象の情報
    Y-->>U: 操作を要求
    U->>Y: タッチ・必要ならPIN等で確認
    Y->>Y: 秘密鍵で署名
    Y-->>B: 署名と認証器の情報
    B-->>S: 認証応答
    S->>S: 公開鍵で署名と各条件を検証

署名するデータ

ここまで「チャレンジに署名する」と書きましたが、実際のWebAuthnの認証応答はもう少し情報を持っています。

概念的な署名対象は、次の形になります。

authenticatorData || SHA-256(clientDataJSON)

||はデータを連結するという意味です。clientDataJSONにはチャレンジ、呼び出し元のorigin、操作の種類などが入り、authenticatorDataにはRP IDのハッシュや、利用者の操作・確認に関するフラグなどが入ります。正確な形式と検証手順はWebAuthn仕様で定義されています。

つまり「このランダムな値に答えた」だけでなく、「どの認証先について、どんな確認を行ったか」も署名に結び付いています。後からフラグだけを書き換えれば、署名対象が変わるので検証は通りません。

ブラウザが持つJSONの情報は、ハッシュによって固定長の値として認証器へ渡します。JSONそのものも応答に含まれ、サービスがその内容と署名を確認できます。

YubiKeyには、ブラウザのアドレスバーを眺める機能はありません。ブラウザが作った情報を受け取って暗号処理をしています。そのため、情報を正しく作るブラウザと、返ってきた情報を正しく検証するサービスも、この仕組みの一部です。

フィッシングへの対策

ワンタイムパスワードと比べてもFIDO認証がフィッシングに強い理由は、認証先を制限する仕組みにあります。

数字の認証コードは、人間が入力欄へ持っていけます。入力先が本物かどうかを、コード自体は判定しません。それに対してFIDOの資格情報は、登録したサービスの識別情報に結び付けて扱われます。U2Fのプロトコル概要にも、この認証先との結び付きが説明されています。

WebAuthnでは、実際にAPIを呼んだページのoriginをブラウザが確認し、要求されたRP IDが許されるものかを検査します。RP IDは認証先を表す識別子で、通常はドメイン名です。認証器もRP IDに対応する資格情報を使います。

ここでoriginとRP IDは同じ文字列とは限りません。originにはスキームやポートも含まれますが、RP IDはドメイン名を扱います。たとえばサービスがexample.comをRP IDにする設計なら、その配下のログイン用サブドメインで共有することがあります。細かな許可関係はブラウザが仕様に従って判断します。

見た目をそっくりにした別ドメインのサイトが、本物向けの鍵を使おうとしても、ブラウザがその呼び出しを許可できるか確認します。認証先を変えれば、本物に登録した資格情報と合わなくなります。

認証の手続きにログイン先の検査を組み込むことで、人間による偽サイトの見分けだけに頼らずに済みます。ただし、正常なブラウザやOS、正しいサービス側の検証が前提です。

タッチと指紋認証

普通のYubiKey 5シリーズで触れる部分は、指紋認証のセンサーではありません。

あの操作で確認するのは、利用者がその場にいて、キーに触れたことです。FIDOではUser Presence、略してUPと呼ばれます。挿さっているキーに対して、ソフトウェアから勝手に認証要求を出し続けるだけでは完了しないよう、利用者の操作を要求します。

一方、PINや生体認証で利用者を確認することはUser Verification、UVです。この二つは公式資料でも区別されています。タッチだけでは、触った人が誰なのかまでは分かりません。

指紋を使うのは、YubiKey Bioのような対応製品です。

また、タッチしたからといって、今の認証要求の内容を人間が全部理解した証拠にもなりません。本体にログイン先の名前を表示する画面はないので、どの要求に応じているかはブラウザやOSの表示を見ます。操作の確認と、内容の理解は分けて考える必要があります。

PINの役割

FIDO2のPINとWebサイトのパスワードは、確認する相手が違います。

Webサイトのパスワードは、そのサービスに対して自分を認証するためのものです。FIDO2のPINは、手元の認証器を使う人を確認するためのものです。サービスのサーバーへPINを送って、サーバー側で一致を調べる仕組みではありません。

ブラウザやOSの入力画面で受け取ったPINを使い、認証器との間で利用者確認を行います。サービス側には、確認に成功したことを示すUVの情報と署名が渡ります。PINは認証器側で管理される、という点が、先ほどの開発者ガイドでも説明されています。

PINによる確認が必須の認証なら、拾った人がキーを持っているだけでは使えません。物理的に持っていることと、PINを知っていることを組み合わせられます。

ただし、すべての利用でPINが必須とは限りません。パスワードに加えてタッチだけを求める使い方もあります。サービスの要求、資格情報の保護設定、認証器の機能によって条件が変わります。「PINを設定したから、すべての認証方式がそのPINで守られる」と考えるのも危ないですね。

YubiKeyのFIDO2にはPINの試行回数制限もあります。FIDO機能の資料では、誤入力を繰り返すとFIDO2がロックされ、復旧にはリセットが必要になることが説明されています。PINを忘れたまま、思い付く候補を延々と試すための仕組みにはなっていません。

そのリセットは、元の認証資格情報を復元する操作ではありません。以前の鍵でログインできる状態は戻らないため、予備の認証手段が必要になります。

FIDO2と関連する規格

FIDO2をWebで使う構成では、Webサイトとブラウザの間にWebAuthn、ブラウザやOSと外付けの認証器の間にCTAPがあります。CTAPの公式解説を見ると、二つの区間を分けて考えると分かりやすいです。

Webサイト
    │ WebAuthn API
ブラウザ・OS
    │ CTAP
YubiKey

WebAuthnは、サイトから認証資格情報の登録や利用を要求するためのAPIです。JavaScriptでいうとnavigator.credentials.create()やnavigator.credentials.get()を使います。サイト側がUSBの通信内容を直接組み立てているわけではありません。

CTAPは、クライアントと認証器が会話するためのプロトコルです。「資格情報を作ってください」「この認証要求への応答をください」といったやり取りを、対応した接続方法で運びます。USBやNFCは、その会話を運ぶ経路になります。

U2Fは、それ以前から使われているFIDOの方式です。主にパスワードに追加する第二の要素として使われ、公開鍵とチャレンジへの署名、認証先との結び付きという基本を持っています。FIDO2では、PINなどによる利用者確認や、後で触れるdiscoverable credentialなどを使えるようになり、パスワードなしのログインにも対応します。

FIDO2に対応したキーを持っていても、サイト側がパスワードなしの認証を採用しているとは限りません。登録方式とログインの条件は、相手側の運用によります。

資格情報の保存方法と容量

資格情報の保存方法によって、本体の容量を使うかどうかも変わります。

FIDO2には、discoverable credentialとnon-discoverable credentialがあります。前者は認証器側で認証先に対応する資格情報を見つけられる方式で、後者ではサービス側からcredential IDを渡して、使う資格情報を指定します。Yubicoの資格情報の説明に、この違いが載っています。

discoverableな場合は、認証器がアカウントに関する情報も保持しているため、サイトから「このサービスのアカウントで認証したい」と要求された際に、候補を探せます。ユーザー名を先に入力しなくても、利用するアカウントを選ぶログインにつながります。

non-discoverableな場合は、サービスが先にアカウントを特定し、そのアカウントに登録したcredential IDを認証器へ渡す構成になります。認証器の中に、全登録先を検索できる一覧を持つ必要はありません。

YubicoのU2Fの鍵生成についての説明では、鍵の情報をキーだけが復元できるよう保護し、key handleとして外部に保持する方式が説明されています。次の認証では、そのhandleをキーへ戻し、内部で必要な鍵を復元して使います。

ここは「秘密鍵が外に出ない」と言うときに補足が要る部分でした。秘密鍵を利用可能な形で渡さないことと、保護済みのデータが一切外へ出ないことは別です。handleをコピーしても、それだけでは署名できず、復元には元のキーが必要になります。

このU2Fの説明を全方式の内部実装だと決め付けることはできませんが、登録のたびに本体の鍵一覧へ一件ずつ追加する、と考える必要はないことが分かります。

一方、discoverable credentialを保存する容量には上限があります。サービス提供者向けの資料では、YubiKey 5のファームウェア5.0〜5.6.xで25件、5.7以降で100件と説明されています。製品名だけで容量を決め付けず、ファームウェアと資格情報の種類を見る必要があります。手元の個体のバージョンは、今回確認していません。

パスキーとの関係

最近よく見る「パスキー」も、YubiKeyとは名前の指す範囲が違います。

パスキーはFIDOの認証資格情報を使う、パスワードに代わる認証の呼び方です。保存先はスマートフォンやパソコン、パスワード管理アプリの場合もあれば、YubiKeyのようなセキュリティキーの場合もあります。FIDO Allianceの説明では、同期するパスキーと、デバイスに結び付いたパスキーを区別しています。

同期するパスキーは、対応したプロバイダーの仕組みを使って、複数の端末で利用できるようにします。新しいスマートフォンでも使いやすい反面、同期先のアカウントや、その復旧方法も管理の対象になります。

YubiKeyに作るパスキーは、物理キーに結び付いたものです。パソコンを買い替えても、そのキーを接続して使えます。けれども、同じアカウントで新しいキーを買ったからといって、古いキーの資格情報が自動で降ってくるわけではありません。

一つのYubiKeyを複数のパソコンで使うのは、同じ認証器を別の端末につなぐ行為です。各パソコンへ秘密鍵を配るわけではありません。逆に、私が持つUSB-CとUSB-Aのキーは、別々の認証器として登録する必要があります。

TOTPとHOTP

Yubico Authenticatorで数字を表示する使い方では、OATH-TOTPやHOTPを使います。この場合は、認証器とサービスが同じ秘密を持ちます。

TOTPは時間を、HOTPはカウンターを変化する入力として使います。その入力と共有した秘密からHMACを計算し、数字として入力しやすい長さにします。HOTPの仕組みはRFC 4226で定義されています。

TOTPをかなり省略して書くと、こうなります。

時間の区切り番号 = floor((現在のUnix時刻 - 基準時刻) / 更新間隔)
認証コード = 共有した秘密と時間の区切り番号から計算

更新間隔が30秒なら、同じ区切りの中では同じ入力を使い、次の区切りになると入力が変わります。サービスも同じ秘密と時間の情報からコードを計算できるので、入力された数字が合うか確認できます。RFC 6238が、この時間ベースの方式を説明しています。

サービス側も秘密を持つ点が、FIDO認証との違いです。

YubiKeyを使う利点は、コード生成に必要な秘密をキーに保管し、その内部で処理できることです。別のパソコンで認証アプリを開いても、同じキーを接続すればコードを出せます。OATHの公式解説にも、この保管場所の違いが説明されています。

ただし、生成した数字はアプリに返され、人間がサイトに入力する情報です。秘密の保管をハードウェアに移しても、その数字の入力先をFIDOのように制限する仕組みが自動で付くわけではありません。YubiKeyを使っていても、認証方式によってフィッシングへの強さは異なります。

登録時に読み取るQRコードも、ただの飾りではありません。多くの場合、コードを生成する秘密と設定が入っています。写真に撮って公開してよい情報ではない、というのも、この原理から分かります。

Yubico OTP

YubiKeyを挿して触ったら、入力欄へ長い文字列が打ち込まれた。こちらは、設定に応じてYubico OTPなどの機能で起きる動作です。

Yubico OTPでは、識別情報、カウンター、タイマー、乱数などを含むデータをAESの共通鍵で暗号化し、文字列として出力します。標準的な構成では44文字になります。Yubico OTPの資料には、各フィールドの意味と生成の流れが載っています。

検証する側は対応する共通鍵で中身を確かめ、カウンターなどを使って、以前受け付けたものをもう一度受け付けないようにします。先ほどのFIDO認証のように、サービスから受け取った新しいチャレンジへ公開鍵方式で署名する流れとは違います。

また、この方式のタイマーは、時計の現在日時をそのまま表すものではありません。電源が入った間の時間経過を扱う情報です。「OTPだから全部30秒おきに変わる6桁の数字」と思うと、ここで話が合わなくなります。

USBではキーボードとして文字を送る機能があるため、専用の入力機器のように動作できます。同じOTPアプリケーションのスロットには、固定パスワードやHOTP、チャレンジレスポンスを設定する用途もあります。スロットの説明を見ると、短いタッチと長いタッチに割り当てる機能まで別々です。

固定パスワードを打ち込む設定なら、出てくる文字列は当然ワンタイムではありません。便利な入力手段であることと、認証方式が持つ性質を、分けて考えないといけませんね。

電源と時計

YubiKeyは電池を内蔵せず、接続先から電力を受けて動作します。

USBで使うときは、接続先から電力を受けます。NFC対応のモデルは、リーダー側の電磁界から動作に必要な電力を得ます。使っていない間も本体が起動し続ける必要はありません。YubiKey 5C NFCの製品仕様でも、電池を必要としないことが示されています。

秘密の情報や設定は不揮発性の記憶領域に保持するので、電源が切れたら毎回登録内容を忘れる、ということにはなりません。必要なときだけ起動して、入力を受け取り、暗号処理をして、結果を返す使い方です。

YubiKeyでTOTPを生成する場合、接続した端末側の時刻の情報を使います。本体が電池で時計を動かし続け、30秒ごとに表示を書き換えているわけではありません。実際、Yubico Authenticatorのトラブルシューティングでも、TOTPが拒否される場合に端末の時計を確認するよう案内されています。

FIDOの認証なら、そもそも現在時刻からコードを作る必要はありません。サービスが渡したチャレンジに応答します。電池なしでワンタイムの認証ができるのは、毎回変わる入力を外から受け取れるからでもあります。

キーに電池が不要でも、接続先は動作している必要があります。本体にネットワーク接続が不要なことと、Webサービスへのログインに通信が必要なことも、別ですね。

PIVとOpenPGP

Webのログイン以外の機能として、PIVとOpenPGPもあります。

PIVはスマートカードを使う認証などの規格で、YubiKeyでは鍵生成や署名などの処理を行えます。PIVの概要で、アプリケーションから命令を送り、結果を受け取る構成を確認できます。OpenPGPも、署名、復号、認証用の鍵を扱う用途があります。

これらは、本体内で鍵を生成する場合に加え、外部で作った秘密鍵を取り込む使い方もあります。OpenPGPのAPIにもインポートの操作があります。外部で作った鍵なら、取り込む前のコピーが別の場所に残っている可能性があるため、バックアップの考え方も変わります。

また、これらの署名がFIDO認証と同じドメイン制限を持つわけではありません。どんなデータへの操作を許すかは、利用する方式とソフトウェア次第です。秘密を保管する装置という共通点はあっても、認証の文脈まで同じにはなりません。

予備キーの登録

2023年の記事では、持ち歩くキーと家に置くキーを登録しておく、と書きました。FIDO認証でこの運用をするなら、二つのキーはそれぞれ独立した認証手段になります。

メインのキーで作った秘密鍵を読み出し、予備へコピーする手順ではありません。サービス側に二つの公開鍵を登録して、「どちらの鍵でもこのアカウントを認証できる」という状態にします。キーをなくしたら、予備でログインして、なくした方の登録を削除できます。

FIDO Allianceの企業向け資料でも、物理キーの喪失と復旧を考え、予備の資格情報を用意する扱いが説明されています。

買っただけの予備は、サービスからまだ認識されません。メインをなくした後で登録しようとしても、登録画面へ入るための認証が必要になってしまいます。

TOTPの予備は、また違います。同じアカウントのコードを複数のキーで出したいなら、同じ共有秘密をそれぞれに登録する方法があります。Yubico Authenticatorの予備キーに関する説明では、この設定方法が案内されています。FIDOのように、サービスへ別々の公開鍵を追加する話とは分ける必要があります。

秘密を取り出せない性質は、紛失後に都合よく取り戻すこともできなくします。予備は、その認証方式を使い続けるための設計ですね。

ログイン後のセッションとアカウント復旧

しかし、ログイン後のサービスは、多くの場合セッションを使います。一度認証した後の操作すべてについて、毎回YubiKeyに触れるわけではありません。ブラウザや端末、セッションの管理は引き続き必要です。サービスが重要な操作の前に再認証を要求するなら、そこで改めてキーの出番があります。

アカウントの復旧経路も同様です。鍵を使えなくなったときに戻れる方法が必要な一方、その経路から本人確認を省略して戻れてしまえば、強い鍵を用意した意味が薄れます。認証と復旧を一緒に考える理由は、ここにあります。