テックブログ
電子カルテの二要素認証に、ICカードでも指静脈でもなくパスキーを選んだ理由
目次
電子カルテや記録のシステムで、二要素認証をどう満たすかを考える時期に来ています。厚生労働省の「医療情報システムの安全管理に関するガイドライン」第7.0版では、令和9年(2027年)4月1日時点で稼働しているシステムを新しく入れる・更新するときに、二要素認証の採用(またはこれに相当する対応)が遵守事項になります。医療機関のあいだで「二要素認証の義務化」と呼ばれているものです(システム運用編 14章の遵守事項⑤、解説は14.1.1)。
手段としてよく比べられるのは、ICカードや、指静脈認証などの生体認証と、パスワードとの組み合わせです。MENTRAでは、スタッフのログインにパスキーを採用しました。スマホに一度登録すれば、次からは顔か指紋だけで入れます。この記事では、なぜその手段を選んだのか、そして顔や指紋だけのログインがなぜ二要素になるのかを、実装のコードと図でご説明します。
先に、言葉の整理
以降で使う言葉を先にまとめます。
| 言葉 | どういうものか |
|---|---|
| 二要素認証 | 「知っていること(パスワード)」「持っているもの(カードや端末)」「本人の体(顔・指紋など)」のうち、種類の違う2つを組み合わせて本人を確かめること |
| パスキー | パスワードの代わりに、端末の中の鍵で本人を確かめるログインの方式。国際的な規格(FIDO2/WebAuthn)にもとづき、iPhone・Android の標準機能で使える |
| 秘密鍵と公開鍵 | 対になる2つの鍵。秘密鍵で作った署名は、対の公開鍵で正しいかを確かめられる。公開鍵からは秘密鍵を作れない |
| 問い(チャレンジ) | サーバがログインのたびに渡す、毎回違う値。端末はこれに署名して返す。使い回しの応答を見分けるためのもの |
| 署名 | 秘密鍵で問いから作る値。秘密鍵を持つ端末でなければ作れない |
| 登録先の名前(RP ID) | 鍵を登録したサイトの名前。鍵はこの名前のサイトでしか使えない |
二要素認証は、何で満たせるのか
候補になる手段を、医療の現場での使い方の目線で並べると次のようになります。
| 手段 | 確かめる要素 | 要素の数 | 追加の機器 | 訪問先・車内で |
|---|---|---|---|---|
| パスワードの自動入力 | 知っていること | 1 | 不要 | 使える |
| パスワード+ICカード | 知っていること+持っているもの | 2 | カードと読み取り機 | 読み取り機が要る(パソコンで使う場合) |
| パスワード+指静脈 | 知っていること+本人の体 | 2 | 専用の読み取り機 | 読み取り機が要る |
| パスキー(顔・指紋) | 持っているもの+本人の体 | 2 | 不要(スマホの標準機能) | 使える |
パスワードを端末に覚えさせて自動で入れる方法は手軽ですが、確かめているのはパスワード1つのままです。ICカードや指静脈は院内の据え置き端末では確かな手段ですが、端末ごとに読み取り機が要ります。
今回の出発点は、訪問看護の現場からいただいた「Face ID や指紋でログインできないか」「IDとパスワードを2段階で入れないとダメか」というご要望でした。スタッフの方は、訪問と訪問のあいだの車内で、1件ずつ短い時間で書き物を済ませます。読み取り機を持ち歩けず、毎回パスワードを打つのも重い場面です。追加の機器なしで二要素になり、操作はむしろ軽くなるという条件を満たすのが、パスキーでした。
なぜ、顔や指紋だけで二要素になるのか
パスキーの要点は、ログインに使う秘密鍵がサーバに渡らず、本人の端末の中でだけ使われることです(同じ Apple ID や Google アカウントで同期している本人の端末を含みます)。サーバに預けるのは対になる公開鍵だけで、ログインのたびに、スマホは顔か指紋(またはスマホのパスコード)で本人を確かめてから秘密鍵で署名を作ります。
スマホを拾った人は顔や指紋(または本人しか知らないスマホのパスコード)を通れないので鍵を使えず、パスワードを盗み見た人はスマホを持っていないので署名を作れません。これが「1回の操作で二要素」の中身です。さらに鍵は登録先の名前(RP ID)に結び付いているので、よく似た偽のサイトに誘導されても、そこでは鍵が使えません。パスワードのように入力して盗まれる手順がそもそもありません。
サーバがスマホに渡すログインの条件は、次のように組み立てています。顔・指紋による本人確認を必須にしているのが要です。
export async function buildAuthenticationOptions(rpId: string, challenge?: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
return await generateAuthenticationOptions({
rpID: rpId,
...(challenge ? { challenge } : {}),
// 空=端末が自分の鍵を選ぶ(ID を打たずにログインできる)
allowCredentials: [],
userVerification: "required",
timeout: WEBAUTHN_TIMEOUT_MS,
});
}allowCredentials を空にしているので、IDを打たなくても、スマホが自分の持っている鍵を選んで差し出します。現場で求められた「ボタン1つで入る」は、この1行で実現しています。
ログインは、どんな流れで進むのか
ボタンを押してから入れるまでの流れです。上の段がスマホ、下の段がサーバで起きていることです。
①を先に済ませておく理由
iPhone のブラウザは、画面を押した直後に呼ばないと顔・指紋の確認を出さない作りです(WebKit の公式ブログ)。私たちの実機では、押してからサーバに問いを取りに行く作りにすると、何も表示されないまま失敗する場面がありました。利用者から見ると、押しても反応しないボタンになります。
そこで、ログイン画面を開いた時点で問いを受け取っておき、押されたら待たずに顔・指紋の確認を呼び出す作りにしました。画面のコードでは、押してから確認を呼ぶまでのあいだに、待ちを1つも挟んでいません。
// 問いは1回きり。使ったら捨てる(成功しなければ下で取り直す)
preparedRef.current = null;
setBusyBoth(true);
// 🔴 ここから startAuthentication までの間に await を挟まない(iOS のタップ直後の制限)
const ceremony = lib.startAuthentication({ optionsJSON: prepared.options });受け取った問いには期限があるので、別のアプリから戻ってきたときや時間が経ったときは、裏で取り直します。押した時点で問いが古ければ、黙って失敗させずに「もう一度押してください」と表示します。
⑤ サーバは何を確かめているか
サーバ側では、署名が正しいかに加えて、どのサイトに向けた応答かと本人確認を済ませた応答かを確かめます。どれか1つでも違えば通しません。
const result = await verifyAuthenticationResponse({
response: params.response as AuthenticationResponseJSON,
expectedChallenge: params.expectedChallenge,
expectedOrigin: params.expectedOrigins,
expectedRPID: params.expectedRpId,
requireUserVerification: true,
credential: {
id: params.credentialId,
publicKey: new Uint8Array(params.publicKey),
counter: 0,
transports: toTransports(params.transports),
},
});requireUserVerification: true は、スマホが顔か指紋で本人を確かめたうえで作った署名であることを求める指定です。持っているだけでは通らない、という二要素のもう一方を、サーバの側でも確かめています。
外から止められない作りにするには
ログイン画面は、誰でも開けます。ということは、問いを渡す窓口も誰でも呼べるということです。ここに落とし穴があります。
素直に作ると、渡した問いをサーバの表に貯めておき、返ってきた応答と照らし合わせます。ところがこれだと、外から窓口を大量に呼ぶだけで表が膨らみます。表に上限を設けて断れば、そのあいだ全員の顔・指紋ログインが止まります。古いものから捨てれば、スタッフの方が受け取った問いまで捨てられます。どちらも、外の誰かが現場のログインを止められてしまう作りです。
そこで、問いを表に貯めないことにしました。問いそのものに期限とサーバの署名を入れて渡し、戻ってきたときに署名と期限を確かめます。
問いを作る側のコードです。期限・乱数・署名を「.」でつないだ文字列を、そのまま問いとして渡します。
/** 署名つきの challenge(文字列)を作る。WebAuthn の options にそのまま渡す */
export async function issueDirectChallenge(secret: string, nowMs: number, nonce?: Uint8Array): Promise<string> {
const exp = Math.floor(nowMs / 1000) + DIRECT_CHALLENGE_TTL_SEC;
const n = nonce ?? crypto.getRandomValues(new Uint8Array(16));
const body = `${VERSION}.${exp}.${bytesToBase64Url(n)}`;
const sig = await mac(await hmacKey(secret), body);
return `${body}.${bytesToBase64Url(sig)}`;
}署名と期限を確かめるだけでは、同じ応答を2回送られたときに見分けられません。そこで、署名の検証まで通った問いだけを「使用済み」として記録し、同じ問いの2回目は断ります。記録する順番が要で、署名を確かめる前に書くと、偽の応答を送るだけで表が増えてしまいます。
// ⑥' direct だけ: 署名まで通った challenge を「使った」と記録する(同じ応答の再送はここで止まる)。
// 🔴 署名の検証より前に書かない(外から偽の応答を送るだけで表が増えるため)
const { error: usedError } = await db.from(USED_CHALLENGE_TABLE).insert({
challenge_hash: directChallenge.hash,
expires_at: directChallenge.expiresAt.toISOString(),
});
if (usedError) {
if ((usedError as { code?: string }).code === "23505") {
return await fail("challenge_missing_or_expired", { ...keyFields, detail: "direct_replayed" });
}
return await fail("internal_error", { ...keyFields, detail: "used_insert" }, safeDbError(usedError, "insert used challenge"));
}記録するのは問いそのものではなく、問いから計算した要約の値です。期限の過ぎた記録はその都度掃除するので、表に残るのは直近の成功の分だけになります。
二要素にしたうえで、なぜログインを1回にまとめたのか
もう1つのご要望が「2段階で入れないとダメか」でした。これまでのログインは、事業所として入る1回目と、スタッフ個人として入る2回目に分かれていました。本番の記録を数えると、開き直すたびに2回の入力を繰り返している様子が見えました。
| 1人が1日にログインし直した回数 | 人数 |
|---|---|
| 1回 | 3人 |
| 3回 | 2人 |
| 7回 | 1人 |
| 合計16回 | 6人 |
二要素の確かさは個人のログインで担保できます。そこで、個人の確認が通ったときにだけ、サーバが事業所の入口も一緒に開ける形にし、利用者から見たログインを1回にしました。事業所の入口のための秘密を端末に置かず、個人の確認を通らなければ開かない順番にしています。
顔・指紋の登録の案内は、スマホにだけ出しています。決める前に、エラーの記録に残っていた端末の種類を数えました(件数が少ないので目安です)。
| 端末とブラウザ | 件数 |
|---|---|
| iPhone の Safari | 5 |
| Windows の Chrome | 3 |
| iPhone の Chrome | 1 |
| Mac の Chrome | 1 |
訪問先で使うのはスマホで、パソコンは帰社後の据え置きです。パソコンには登録の案内を出さず、これまでどおりIDとパスワードで入れるようにしました。顔・指紋の登録はお一人ずつの任意で、IDとパスワードのログインも残しています。
出す前に、何を確かめたか
認証は、壊れたときに気づきにくい場所です。守りを1つ足すごとに、わざと壊して検査が赤くなるかまで確かめました。
| 確かめたこと | 数 | 結果 |
|---|---|---|
| サーバ側の自動テスト(全体) | 864件 | すべて合格 |
| 守りをわざと壊して、テストが落ちるか(ログインの判定) | 9通り | すべて検知 |
| 同上(問いの期限と署名) | 2通り | すべて検知 |
| iPhone の実機での通し(登録・顔でのログイン・解除・案内) | 5場面 | すべて合格 |
令和9年の更新を控えて二要素認証の手段を比べている管理者の方、訪問の合間に何度もアプリを開き直すスタッフの方の両方に、追加の機器なしで応えられる形を、MENTRAはすでに現場で動かしています。アカウントと認証の考え方はセキュリティに、使える場面はプロダクトアップデートにまとめました。ガイドラインの全体像は3省2ガイドラインの解説をご覧ください。院内の運用に合わせたログインの形も、ご相談いただけます。
よくあるご質問
顔や指紋だけのパスキーが、なぜ多要素認証(二要素認証)になるのですか
パスキーでは、ログインに使う秘密鍵が本人の端末の中にだけあってサーバには渡らず、その鍵を使うたびに顔か指紋(またはスマホのパスコード)での本人確認を求めます。サーバは「その鍵で作った署名か」を確かめるので、スマホを持っていること(所持)と、本人の顔や指紋であること(生体)の2つがそろわないと通れません。操作は1回でも、確かめている要素は2つです。
顔や指紋の情報は、サーバに送られますか
送られません。顔や指紋はスマホの中で照合され、照合に通ったときにだけスマホが鍵で署名を作ります。サーバに届くのはその署名と、登録したときに受け取った公開鍵を照らし合わせるための情報だけです。
スマホをなくしたらどうなりますか
鍵は本人の端末の中にあり、使うには本人の顔か指紋(またはスマホのパスコード)が要るため、拾った人がそのまま入ることはできません。院内の管理者が管理画面からその人の鍵を解除でき、IDとパスワードでのログインはそのまま使えます。
