WebAuthn

Web Authentication APIの略称。Webブラウザとサーバー間でパスワードレス認証を実現するW3C標準のJavaScript API。FIDO2の一部を構成する。

概要

WebAuthnは、Webアプリケーションが公開鍵暗号を用いた強力な認証を実装できるようにするブラウザAPI。パスワードに依存せず、フィッシング耐性のある認証を実現する。

アーキテクチャ

Webサービス(Relying Party)
  ↕ HTTPS
Webブラウザ
  ├─ WebAuthn API(JavaScript)
  └─ 認証器マネージャー
       ↕ CTAP2(USB/NFC/BLE)
認証器(Authenticator)
  ├─ プラットフォーム認証器(内蔵)
  └─ 外部認証器(セキュリティキー等)

主要な概念

Relying Party(RP: 依拠当事者)

認証を受けるWebサービス。ドメイン名で識別される。

const rpID = "example.com";
const rpName = "Example Service";

Authenticator(認証器)

秘密鍵を生成・保管し、署名を行うデバイス。

Credential(クレデンシャル)

公開鍵ペアと関連メタデータ。

{
  id: "credential-id",        // クレデンシャルID
  type: "public-key",         // 種別
  publicKey: ArrayBuffer,     // 公開鍵
  authenticatorData: {...}    // 認証器情報
}

認証フロー

1. 登録(Registration / Attestation)

新しいクレデンシャルを作成し、公開鍵をサーバーに登録する。

// サーバーからチャレンジを取得
const options = {
  challenge: Uint8Array.from("random-challenge", c => c.charCodeAt(0)),
  rp: {
    name: "Example Corp",
    id: "example.com"
  },
  user: {
    id: Uint8Array.from("user-id-123", c => c.charCodeAt(0)),
    name: "user@example.com",
    displayName: "User Name"
  },
  pubKeyCredParams: [
    { type: "public-key", alg: -7 },  // ES256
    { type: "public-key", alg: -257 } // RS256
  ],
  authenticatorSelection: {
    authenticatorAttachment: "platform", // or "cross-platform"
    userVerification: "required"         // 生体認証/PIN必須
  },
  timeout: 60000,
  attestation: "direct" // or "none", "indirect"
};

// 認証器で公開鍵ペアを生成
const credential = await navigator.credentials.create({
  publicKey: options
});

// 公開鍵をサーバーに送信して登録
await fetch('/webauthn/register', {
  method: 'POST',
  body: JSON.stringify({
    id: credential.id,
    rawId: Array.from(new Uint8Array(credential.rawId)),
    response: {
      clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)),
      attestationObject: Array.from(new Uint8Array(credential.response.attestationObject))
    },
    type: credential.type
  })
});

2. 認証(Authentication / Assertion)

既存のクレデンシャルを使ってログインする。

// サーバーからチャレンジを取得
const options = {
  challenge: Uint8Array.from("random-challenge", c => c.charCodeAt(0)),
  rpId: "example.com",
  allowCredentials: [
    {
      type: "public-key",
      id: Uint8Array.from(credentialId, c => c.charCodeAt(0))
    }
  ],
  userVerification: "required",
  timeout: 60000
};

// 認証器で署名
const assertion = await navigator.credentials.get({
  publicKey: options
});

// 署名をサーバーに送信して検証
await fetch('/webauthn/authenticate', {
  method: 'POST',
  body: JSON.stringify({
    id: assertion.id,
    rawId: Array.from(new Uint8Array(assertion.rawId)),
    response: {
      clientDataJSON: Array.from(new Uint8Array(assertion.response.clientDataJSON)),
      authenticatorData: Array.from(new Uint8Array(assertion.response.authenticatorData)),
      signature: Array.from(new Uint8Array(assertion.response.signature)),
      userHandle: assertion.response.userHandle ? Array.from(new Uint8Array(assertion.response.userHandle)) : null
    },
    type: assertion.type
  })
});

セキュリティ特性

フィッシング耐性

ドメインごとに異なる公開鍵ペアを生成するため、フィッシングサイトでは利用できない。

example.com用の鍵ペア → example.comでのみ有効
evil-example.com用の鍵ペア → 別の鍵(使い回し不可)

リプレイ攻撃耐性

チャレンジは毎回ランダムに生成され、署名に含まれるため、リプレイ攻撃が不可能。

チャレンジ1 → 署名1(チャレンジ1に対してのみ有効)
チャレンジ2 → 署名2(署名1は無効)

中間者攻撃耐性

TLS通信を前提とし、オリジン情報が署名に含まれる。

clientDataJSON
├─ type: "webauthn.get"
├─ challenge: "..."
├─ origin: "https://example.com"  ← オリジン検証
└─ crossOrigin: false

ブラウザサポート

対応ブラウザ

ブラウザ バージョン プラットフォーム認証器
Chrome 67+ Windows Hello, Touch ID
Firefox 60+ Windows Hello
Safari 13+ Touch ID, Face ID
Edge 18+ Windows Hello

機能検出

if (window.PublicKeyCredential) {
  // WebAuthnサポートあり

  // プラットフォーム認証器の有無を確認
  const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();

  if (available) {
    // Touch ID、Windows Hello等が利用可能
  }
}

Attestation(構成証明)

認証器が本物であることを証明する仕組み。

Attestation Statement Format

Attestation Conveyance

attestation: "none"      // 構成証明不要(プライバシー重視)
attestation: "indirect"  // 匿名化された構成証明
attestation: "direct"    // 完全な構成証明(デバイス識別可)

User Verification(ユーザー検証)

認証器がユーザー本人であることを確認する仕組み。

User Verification Methods

User Verification Policy

userVerification: "required"     // 必須
userVerification: "preferred"    // 推奨(できれば)
userVerification: "discouraged"  // 不要

Resident Key(Discoverable Credential)

ユーザーIDをサーバーに問い合わせずに認証できるクレデンシャル。

Non-Resident Key(従来)

1. ユーザーがメールアドレスを入力
2. サーバーがクレデンシャルIDを返却
3. 認証器がそのIDで署名

Resident Key

1. ユーザーが何も入力せず認証開始
2. 認証器内に保存されたクレデンシャルを選択
3. 認証完了(パスワードレス+ユーザー名レス)
authenticatorSelection: {
  residentKey: "required",     // Resident Key必須
  requireResidentKey: true     // 後方互換用
}

eKYCとの統合可能性

現状

犯収法のワ・ヘ・ホ方式は、WebAuthnを直接利用していない。

統合シナリオ

低リスク取引:

WebAuthn(プラットフォーム認証器)
  → 簡便なログイン

中リスク取引:

WebAuthn + 顔認証
  → 生体認証の二重チェック

高リスク取引:

JPKI電子署名([[マイナンバーカード]])
  → 法的効力のある意思表示

サーバー側実装

ライブラリ例

Python:

from webauthn import generate_registration_options, verify_registration_response

options = generate_registration_options(
    rp_id="example.com",
    rp_name="Example Corp",
    user_id="user-123",
    user_name="user@example.com"
)

verification = verify_registration_response(
    credential=credential_from_client,
    expected_challenge=challenge,
    expected_origin="https://example.com",
    expected_rp_id="example.com"
)

Node.js:

const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');

const options = await generateRegistrationOptions({
  rpName: 'Example Corp',
  rpID: 'example.com',
  userID: 'user-123',
  userName: 'user@example.com',
});

const verification = await verifyRegistrationResponse({
  response: credentialFromClient,
  expectedChallenge: challenge,
  expectedOrigin: 'https://example.com',
  expectedRPID: 'example.com',
});

関連