本文へ移動
🔐

パスキーはなぜ偽サイトに強い?パスワードとの違いと弱点を図解

31分
もくじ

ログインの方法は、パスワードだけではなくなった。スマートフォンに届く番号を打ち込む方法があり、指紋や顔で端末のロックを解くだけで済むパスキーもある。どれも、ログインしようとしている人が本人かどうかを確かめる仕組みだ。ところが、それぞれが何を確かめていて、どこが弱いのかは、名前を見ても分からない。

この違いは、証券口座で実際の被害になった。金融庁が2026年10月9日に更新した被害の表によると、2025年1月から2026年9月までに、証券会社のインターネット取引で、不正アクセスが19,252件、不正取引が10,642件あった(各社の報告に基づく暫定の値)。日本証券業協会は、2025年10月15日に改正したガイドラインで、ログインや出金のときに「フィッシングに耐性のある多要素認証」を必須にするよう証券会社に求め、その例にパスキーを挙げた。

フィッシングは、本物そっくりの偽サイトに誘い込み、パスワードなどを打ち込ませて盗む手口だ。多要素認証は、パスワードのような「知っているもの」、スマートフォンのような「持っているもの」、指紋のような「体の特徴」のうち、種類の違う2つ以上を組み合わせて本人を確かめることだ。日本証券業協会のガイドラインも、多要素認証を、知識、所持、生体の情報のうち2つ以上の組み合わせと定めている。

パスキーは偽サイトに強いが、ログインの後まで守るわけではない。

ログインの方式は、何を確かめて、何を相手に渡すかで違う

3つの方式は、本人だと確かめる材料と、ログインのときに相手に渡すものが違う。パスワードとワンタイムパスワードは、利用者が画面に打ち込んだ値を、そのまま相手に渡す。だから、偽サイトに打ち込めば、その値は偽サイトの手に入り、本物のサイトで使われる。パスキーは、秘密鍵をサーバーに渡さず、サイトの名前とサーバーからの乱数を含むデータへの署名だけを返す。偽サイトで作られた署名は、本物のサイトの検査を通らない。

ただし、パスキーが守るのはログインの場面である。パスワードでもログインできる道が残っていたり、ログインの後にサイトから渡される印を盗まれたりすれば、そこから入られる。

登場するのは、利用者、利用者のブラウザーやアプリ、本物のサイトのサーバー、そして偽サイトのサーバーだ。

インターネット 偽サイトに誘い込まれると 盗んだ値で、本人のふりをする 利用者パスワードを覚えている 利用者のブラウザーやアプリ認証アプリ、パスキーが入っている 本物のサイトのサーバー本人かどうかを確かめる 偽サイトのサーバー
パスワードワンタイムパスワードパスキー
本人だと確かめる材料本人が覚えている文字列(知っているもの)番号を受け取れる、または作れるスマートフォン(持っているもの)秘密鍵が入った端末(持っているもの)と、端末のロックの解除
サーバーが持つものパスワードから作ったハッシュ値認証アプリなら共有の秘密。ショートメッセージなら送った番号公開鍵と、どの会員の鍵かという登録の情報
ログインのときに相手に渡すものパスワードそのもの画面に出た、または届いた番号サーバーが送った乱数への署名
偽サイトで盗まれて、本物のサイトで使われるか使われる使われる。番号が変わる前に、その場で中継される使われない。偽サイトで作られた署名を、本物のサイトは受け入れない

パスワード: 打ち込んだ秘密を、そのまま照合する

僕が引っかかっていたのは、ログインは公開鍵と秘密鍵で成り立っているのか、という点だった。パスワードのログインでは、照合そのものに公開鍵は出てこない。公開鍵が働くのは、その手前の HTTPS の部分だ。

HTTPS(Hypertext Transfer Protocol Secure)は、ブラウザーと Web サーバーの間の通信を暗号化する決まりである。通信を始める前に、Web サーバーがサーバー証明書を示し、ブラウザーがそれを確かめる。この確認に、公開鍵と秘密鍵を使う署名が使われる(共通鍵と公開鍵の記事の「HTTPS は、2つの方式を組み合わせている」)。その後、パスワードは暗号化された通信で Web サーバーに届き、サイトの中で照合される。照合を受け持つのは、ふつうはバックエンドだ。Web サーバーからバックエンドへどう渡すかは、サイトの作りによって違う。

サーバー証明書 パスワード(暗号化した通信) 照合を頼む 照合の結果 合っていれば、ログインを認める 公開鍵が働くのはここ 利用者がパスワードを打ち込む 保存してあるハッシュ値と照合する ブラウザー Web サーバー バックエンド

Web サーバーは、ブラウザーからの接続を受け付けて、HTML や画像、JavaScript のファイルを返す。バックエンドは、サーバーの側で動いて、求められたデータを用意するプログラムだ。サーバー証明書は、Web サーバーの公開鍵が入ったデータで、サイトの運営者とは別の機関(認証局)が発行する。ハッシュ値は、ハッシュ関数という計算で元の値から作る値で、元の値には戻せない。

データベースには、パスワードそのものではなく、パスワードから作ったソルト付きのハッシュ値を保存する。ハッシュ値を作るには、漏れたハッシュ値から総当たりで元のパスワードを探す攻撃に時間がかかるよう、わざと計算に時間がかかる方式を使う。Web のセキュリティの団体 Open Worldwide Application Security Project(OWASP)の指針は、第一候補に Argon2id を挙げ、使えない場合の候補として scrypt を挙げている。ログインのときは、打ち込まれたパスワードを同じ手順でハッシュ値にして、保存してある値と比べる。この保存の仕方は、共通鍵と公開鍵の記事の「パスワード: サーバーには、元のパスワードを保存しない」で、コードと一緒に確かめた。

つまり、パスワードのログインが成り立つ理由は、「その文字列を知っているのは本人だけのはずだ」という1点にある。照合はただの比較なので、正しい文字列を打ち込んだ人は、本人でなくても通る。偽サイトにパスワードを打ち込めば、偽サイトはそれを本物のサイトへ送るだけで、本人になりすませる。合言葉を知っている相手の顔を、門番は見ない。

ワンタイムパスワード: 短い時間だけ使える番号を足す

ワンタイムパスワードは、短い時間で変わる番号だ。ここでは、パスワードに加えて打ち込ませ、「持っているもの」を確かめる使い方を考える。番号の作り方は2つある。

ショートメッセージ: 届いた番号で、電話番号の持ち主かを確かめる

サーバーが番号を作り、登録された電話番号にショートメッセージで送る。警察庁の資料は、ショートメッセージで利用者の電話番号に認証のコードを知らせ、そのコードで認証する方式を「SMS認証」と呼んでいる(SMS は、ショートメッセージサービス Short Message Service の略)。この方式が確かめているのは、その電話番号に届くメッセージを読めることだ。

米国国立標準技術研究所の認証のガイドライン「SP 800-63B-4」は、電話の回線を使うこの方法を「制限付き」として扱っている。ガイドラインの脅威の表には、携帯電話の会社をだまして、被害者の電話番号に届くメッセージを自分に届けさせる攻撃が載っている。

認証アプリ: 共有の秘密と時刻から、番号を計算する

スマートフォンの認証アプリに表示される番号は、どこかから届くのではなく、アプリが計算している。登録のとき、サーバーは共有の秘密という値を作り、アプリに渡す。その後は、アプリとサーバーが、それぞれ共有の秘密と今の時刻から、同じ手順で番号を計算する。この手順を決めた「RFC 6238」(インターネットの技術の仕様書)は、番号が変わる時間の区切りの既定値を30秒としている。一度ログインに使った番号は、同じ区切りの中でも、もう一度は受け付けないとも定めている。

たとえば、共有の秘密と手順が同じなら、59秒の時点で計算した番号は、スマートフォンでもサーバーでも同じになる。

import { createHmac } from "node:crypto";

// 共有の秘密と時刻から、6桁の番号を作る
function oneTimePassword(secret: Buffer, unixSeconds: number): string {
  // 30秒ごとに1つ増える数を作る
  const step = Math.floor(unixSeconds / 30);
  const counter = Buffer.alloc(8);
  counter.writeBigUInt64BE(BigInt(step));

  // 共有の秘密を鍵にして、その数からハッシュ値を作る
  const hash = createHmac("sha1", secret).update(counter).digest();

  // ハッシュ値の一部を取り出して、6桁の数字にする
  const offset = hash[hash.length - 1] & 0x0f;
  const number = (hash.readUInt32BE(offset) & 0x7fffffff) % 1_000_000;
  return number.toString().padStart(6, "0");
}

// 登録のときに、サーバーが共有の秘密を作り、スマートフォンのアプリに渡す
// ここでは仕様の試験に使われる値を使う
const secret = Buffer.from("12345678901234567890");

// 同じ時刻なら、スマートフォンとサーバーは、通信しなくても同じ番号を計算する
console.log("スマートフォン 59秒:", oneTimePassword(secret, 59));
console.log("サーバー       59秒:", oneTimePassword(secret, 59));

// 30秒の区切りをまたぐと、番号が変わる
console.log("スマートフォン 61秒:", oneTimePassword(secret, 61));
スマートフォン 59秒: 287082
サーバー       59秒: 287082
スマートフォン 61秒: 359152

「287082」は、仕様に試験用として載っている8桁の値「94287082」の、下の6桁と同じだ。

アプリとサーバーは、ログインのときに通信しなくても、同じ番号にたどり着く。だから、番号が合えば、共有の秘密を持つスマートフォンが利用者の手元にあると分かる。裏を返すと、サーバーも共有の秘密を持っている。サーバーから共有の秘密が漏れれば、番号はだれにでも計算できる。

番号は、偽サイトから本物のサイトへ中継される

番号は短い時間で変わるので、盗んでも、後からは使えない。だが、その場で使えば間に合う。米国国立標準技術研究所のガイドラインは、ワンタイムパスワードによる認証はフィッシングに耐性がない、と書いている。利用者が手で打ち込む値は、そのときの通信と結び付いていないので、なりすました側が中継できるからだ。

パスワード 盗んだパスワード 番号を打ち込む画面 そっくりの画面 番号 盗んだ番号(まだ変わっていない) ログインを認める 利用者 偽サイト 本物のサーバー

偽サイトは、受け取った番号を、変わる前に本物のサイトへ渡すだけでよい。30秒は、人には短いが、中継するプログラムには十分に長い。

パスキー: 秘密鍵を出さずに、署名だけを返す

パスキーは、公開鍵と秘密鍵の組を使ってログインする方法だ。パスキーの規格を作っている FIDO(Fast IDentity Online)アライアンスは、パスキーを、公開鍵と秘密鍵の暗号を使う認証の方法と説明している。ブラウザーの側の決まりは、Web の標準を決める団体の W3C(World Wide Web Consortium)が「Web Authentication」という仕様にまとめている。

登録: 端末が鍵の組を作り、公開鍵だけを渡す

パスキーを作る 公開鍵 公開鍵と秘密鍵の組を作る 会員と公開鍵を結び付けて保存する 秘密鍵はサーバーに渡さない 利用者 端末 サーバー

Web Authentication の仕様は、秘密鍵を、端末の持ち主にさえ見せない前提で作られている。秘密鍵は端末の中に置かれ、複数の端末に同期するパスキーなら、同期の仕組みの中にも暗号化して置かれる(後の「パスキーでも防げないこと」で触れる)。サーバーがデータベースに保存するのは、公開鍵と、どの会員の鍵かという登録の情報で、秘密鍵は保存しない。だから、データベースの中身が漏れても、パスキーでログインするための秘密鍵は漏れない。

ログイン: サーバーが送った乱数に、署名して返す

ログインしたい チャレンジ(毎回ちがう乱数) 指紋や顔で、端末のロックを解く 署名 ログインを認める チャレンジとサイトの名前に、秘密鍵で署名する 保存してある公開鍵で、署名を確かめる 利用者 端末 サーバー

チャレンジは、サーバーがログインのたびに作る、推測しにくい値だ。仕様は16バイト以上にすることを勧めている。毎回変わるので、前のログインの署名を盗んで送り直しても通らない。署名は秘密鍵が無いと作れず、公開鍵で確かめられる。この仕組みは、共通鍵と公開鍵の記事の「秘密鍵で署名し、公開鍵で確かめる」で、コードと一緒に試した。

偽サイトでは、本物のサイトが受け入れる署名を作れない

パスキーが偽サイトに強いのは、守りが2つ重なっているからだ。

1つめは、ブラウザーと端末である。仕様は、パスキーを、登録したサイトに属するページからしか使えないようにしていて、この範囲を守らせるのはブラウザーと端末の役目だ。偽サイトのページが本物のサイトのパスキーを使おうとしても、ブラウザーが使わせない。

2つめは、署名の中身である。ブラウザーは、サーバーから届いたチャレンジと、いま開いているページのサイトの名前を、1つのデータにまとめる。サイトの名前は、仕様ではオリジンと呼び、https のような通信の決まり、bank.example のようなドメイン、ポート番号(通信の受け口の番号。ふつうの HTTPS では省略される)の3つで決まる。端末は、このデータを含む値に、秘密鍵で署名する。サーバーは、署名を確かめるときに、中のサイトの名前が自分のものかも調べる。MDN Web Docs の「Web Authentication API」は、偽サイトを作った攻撃者は、署名がサイトの名前によって変わるので、利用者としてログインできないと説明している。

次のコードは、本物の手順を大幅に削った再現で、2つめの守りだけを試す。仮に1つめの守りを越えて、偽サイトが利用者の端末に署名させたとする。

import { generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";

// 登録のとき: 利用者の端末が鍵の組を作り、公開鍵だけをサーバーに渡す
const device = generateKeyPairSync("ed25519");
const savedPublicKey = device.publicKey; // サーバーが保存するのは公開鍵だけ

// ログインのとき: サーバーが、毎回ちがう乱数(チャレンジ)を作って送る
const challenge = randomBytes(16).toString("hex");

// 端末は、チャレンジと「いま開いているサイトの名前」をまとめて、秘密鍵で署名する
// サイトの名前は、利用者ではなくブラウザーが書き入れる
function deviceSigns(challenge: string, siteName: string) {
  const data = JSON.stringify({ challenge, siteName });
  return { data, signature: sign(null, Buffer.from(data), device.privateKey) };
}

// 本物のサーバーは、署名と、中に書かれたサイトの名前とチャレンジを確かめる
function serverChecks(answer: { data: string; signature: Buffer }): string {
  if (!verify(null, Buffer.from(answer.data), savedPublicKey, answer.signature)) {
    return "失敗(署名が合わない)";
  }
  const { challenge: got, siteName } = JSON.parse(answer.data);
  if (siteName !== "https://bank.example") return `失敗(サイトの名前が ${siteName})`;
  if (got !== challenge) return "失敗(チャレンジが違う)";
  return "成功";
}

// 本物のサイトでログインする
console.log("本物のサイト:", serverChecks(deviceSigns(challenge, "https://bank.example")));

// 偽サイトが、本物のチャレンジを利用者の端末に渡し、返ってきた署名を本物へ中継する
const relayed = deviceSigns(challenge, "https://bank-login.example");
console.log("偽サイトの中継:", serverChecks(relayed));

// 偽サイトが、中に書かれたサイトの名前を本物に書き換えてから中継する
const rewritten = {
  data: relayed.data.replace("https://bank-login.example", "https://bank.example"),
  signature: relayed.signature,
};
console.log("名前を書き換えて中継:", serverChecks(rewritten));
本物のサイト: 成功
偽サイトの中継: 失敗(サイトの名前が https://bank-login.example)
名前を書き換えて中継: 失敗(署名が合わない)

偽サイトが中継した署名は、中のサイトの名前が違うので、本物のサーバーに断られる。名前を本物に書き換えると、今度は署名が合わない。秘密鍵を持たない偽サイトには、書き換えた文に合う署名を作れないからだ。

パスワードやワンタイムパスワードとの違いは、ここにある。利用者が偽サイトに誘い込まれても、偽サイトの手に入るのは、偽サイトの名前が入った署名だけだ。それは本物のサイトでは使えない。

パスキーでも防げないこと

パスキーは最強なのか、というのも、僕が整理できていなかった点だ。この記事の3つの方式の中で、偽サイトで盗まれた値を本物のサイトで使えないのはパスキーである。とはいえ、パスキーを使っていても、別のログインの方法、同期の仕組み、ログインの後の状態のどれかが弱ければ、アカウントは守りきれない。

パスワードでもログインできる道が残っている

パスキーを登録しても、パスワードやショートメッセージでログインする方法が残っていることがある。日本証券業協会のガイドラインも、パスキーなどを使える機器を持たない人には、代わりの多要素認証を用意するとしている。

セキュリティの会社の Proofpoint は、2025年8月に、偽サイトが「このブラウザーはパスキーに対応していない」と見せかけて、利用者を別のログインの方法に誘導する手口を示した。誘導された先がパスワードとワンタイムパスワードなら、前の節の中継がそのまま効く。ただし同社は、この手口は実際の攻撃ではまだ見つかっていないとしている。

同期するパスキーは、同期に使うアカウントも守る必要がある

パスキーには、1台の端末に結び付けるものと、同じ人の複数の端末に同期するものがある(FIDO アライアンスの説明)。たとえば Apple は、パスキーを iCloud キーチェーンで同期し、その中身を Apple も知らない鍵で暗号化していると説明している。

同期するパスキーでは、秘密鍵は同期の仕組みの中にも置かれる。米国国立標準技術研究所のガイドラインは、同期できるパスキーを認める条件の1つに、同期の仕組みの中の鍵へのアクセスを多要素認証で守ることを挙げている。利用者の側から見れば、同期に使うアカウント(Apple なら iCloud のアカウント)へのログインも、強い方法で守っておく必要がある。

ログインの後に渡される印を盗まれる

ここでは、ログインの後の状態をセッション Cookie で保つ場合を考える。ログインに成功すると、サーバーはブラウザーに、「この人はもうログインした」と分かる印を渡す。ブラウザーはその印を Cookie(サーバーがブラウザーに預ける小さなデータ)に入れておき、同じサイトへの次からのリクエストに付けて送る。この Cookie をセッション Cookie と呼ぶ。

印そのものを盗まれると、どの方式でログインしたかは関係なくなる。Microsoft は、中継型の偽サイトがパスワードとセッション Cookie を盗み、多要素認証を有効にしていた利用者のアカウントにも、認証を飛ばして入った例を報告している。

つまり、パスキーは最強かという問いの答えは、「ログインの入口では、3つの中でいちばん偽サイトに強い。ただし、入口しか守らない」である。

認証の用語を、学ぶ順番に並べる

もう1つ整理できていなかったのが、JSON Web Token、Cookie、SAML、FIDO、OpenID Connect と、用語が多いことだった。並べてみると、これらは同じ段の話ではない。下の図は、学ぶ順番に並べたものだ。上の段の多くは、前の段の言葉で説明できる。ただし、同じ段に並んだもの同士が、必ず上下の関係にあるわけではない(たとえば SAML は OAuth 2.0 の上に作られたものではない)。

1. 言葉の区別認証、認可 2. 暗号の部品ハッシュ、共通鍵、公開鍵と秘密鍵、署名 3. 通信を守るHTTPS、サーバー証明書 4. 人を確かめる(この記事)パスワード、ワンタイムパスワード、パスキー 5. ログインの後の状態を保つセッション、Cookie、JSON Web Token 6. 別のサービスとつなぐOAuth 2.0、OpenID Connect、SAML
用語段何か
認証、認可1認証は本人かどうかを確かめること、認可はその人に何をさせてよいかを決めること(認証と認可の記事)
ハッシュ、共通鍵、公開鍵と秘密鍵2鍵の種類と、暗号化とハッシュ化の違い(共通鍵と公開鍵の記事)
HTTPS、サーバー証明書3ブラウザーが、相手の Web サーバーが本物かを確かめ、通信を暗号化する
FIDO4FIDO アライアンスは、パスキーの規格を作る団体。FIDO2 はその規格の名前で、ブラウザーの決まり(Web Authentication)と、端末と認証の機器の間の通信の決まりからなる
セッション、Cookie5ログインの後、サーバーはセッションの番号を Cookie に入れてブラウザーに預け、次からのリクエストに付けて送らせる
JSON Web Token5署名を付けたデータの形式で、ログインの後の状態を保つのにも使われる。署名には、共通鍵の方式(HS256 という名前の方式)も、公開鍵の方式(RS256、ES256 など)も使える(クロスドメイン認証の記事)
OAuth 2.06利用者のパスワードを渡さずに、別のアプリに、限られた範囲の操作を許す仕組み。OAuth の公式サイトは、OAuth 2.0 は認証の仕組みではないと説明している
OpenID Connect6OAuth 2.0 の上に、ログインした本人の情報を、署名を付けた ID トークンで渡す仕組みを足したもの
SAML(Security Assertion Markup Language)6本人の情報を XML(Extensible Markup Language。データを <名前> のような印で囲んで書く形式)で書いて渡す決まり。署名付きで受け取る場合、受け取る側は、署名で、正しい相手から書き換えられずに届いたかを確かめる。1回のログインで複数のサービスを使えるシングルサインオンに使われる

最初の問いに戻ると、公開鍵と秘密鍵は2段目の部品だ。3段目の HTTPS、4段目のパスキー、公開鍵の方式を選んだときの5段目の JSON Web Token が、それを使う。パスワードの照合は、それを使わない。

パスキーは偽サイトに強いが、ログインの後まで守るわけではない

パスワードは、本人だけが知っているはずの文字列を、相手にそのまま渡して照合する。ワンタイムパスワードは、手元のスマートフォンで番号を受け取るか計算するかして、それを相手に渡す。どちらも、渡した値は偽サイトの手に入り、本物のサイトで使われる。パスキーは、秘密鍵をサーバーに渡さず、サイトの名前を含めた署名だけを返すので、偽サイトでは本物のサイトが受け入れる署名を作れない。

とはいえ、パスキーが守るのはログインの入口だけだ。パスワードで入れる道、同期に使うアカウント、ログインの後に渡される印は、それぞれ別に守る必要がある。名前だけでは見分けられなかった3つの方式も、ログインのときに何を相手に渡すかを見れば、違いが分かる。

おわり😊

RELATED

つくることで、見える景色がある。