Web Authentication API 紹介 (Windows Hello を使った Edge 開発)


Windows Hello を使った開発

こんにちは。
しばらく投稿が滞ってすみません。(長く、休暇をいただきました。)

Chrome で実装されている Credential Management API や、Microsoft Edge (Preview Build) に実装されている Web Authentication API (本投稿で紹介します) など、パスワードのみに依存しない、統一的で、ユーザーの負荷を軽減し、かつセキュアな認証に対するいくつかの試みが、W3C による標準化の動きと共に提案されています。

今回は、FIDO 2.0 に準拠した W3C の Web Authentication API について、実際の Microsoft Edge におけるプログラミングを見ながら細かく紹介します。

もう何度も本ブログで紹介してきたように、Microsoft では、Microsoft Passport と呼ばれるフレームワークを Windows 10 で導入しています。このフレームワークは、多要素認証 (Device と紐づいた認証) などのパスワードのみに依存しないセキュアな認証方式を、生体認証 (biometrics authentication) と組み合わせることで、より直観的に (入力の手間などユーザーの負荷を軽減して) 実現する仕組みで、FIDO に準拠し、既存技術の拡張の形で実装することで、将来的な相互運用も考慮した設計となっています。

以前投稿した「Windows Hello を使った App 開発」では、Windows の API (UWP の KeyCredentialManager API) を使って、このプログラムからの活用例を紹介しましたが、今回紹介する Web Authentication API は、この処理と同様の概念を Web アプリケーションで実現できる API です。

なお、BUILD 2016 のセッションの中でも紹介していますが、現在 (2016/05)、この Web Authentication API には下記の制限があります。

  • まだ早期ドラフトの実装です (名前空間等、MS-Prefix 実装です)。現在は Microsoft Edge のみで動作します。
  • USB, Bluetooth (BT) などによる External Authenticator (外部 Device による認証) は将来対応予定で、現在は TPM などの Embedded Authenticator (利用 Device に付随した認証) に限定しています。今後、FIDO Device がサポートされれば、この API を使って、wearable 端末も含むポータブルなデバイスを使った認証も可能になるでしょう。 (マイクロソフトの公式な投稿ではありませんが、こちらの記事 なども参照してみてください。。。)

 

概念 (おさらい)

まず、「Windows Hello を使った App 開発」でも紹介したように、Microsoft Passport による大まかな流れを理解しておきましょう。(何度も紹介していますが、復習のため改めて記載します。)

Privatre Key (秘密鍵), Public Key (公開鍵) の key pair を用いて、Privatre Key を Device 側、Public Key を接続する Service 側に保持して、相互に認証をおこないます。
まず最初に、PIN や Windows Hello による生体 (biometrics) 認証などの Device 側の認証 (ネットワーク上に情報が流れない閉じた認証) によって、key pair の作成と、Device 側への private key の登録、public key の取得 (および、この取得した public key の Service 側への登録) をおこないます。以降の認証では、同様に Device 側の認証 (PIN, Windows Hello などによる認証) を使って、格納されている private key を取り出して challenge data (nonce など) にデジタル署名 (Digital Signature) をおこない、Application 側 (Service 側) では、この生成された署名が正しいかどうかを public key を使って検証します。(Azure AD とも、このようにして相互に連携します。)

このため、Web Authentication API で使用する (主な) 関数は 2 つだけです。
まず、makeCredential 関数は、初回の key pair の作成、private key の (Device への) 登録、public key の取得をおこなう関数です。(この際、上述の通り、PIN の入力や、Windows Hello による生体認証がおこなわれます。) Application では、通常、利用開始の最初の 1 回だけ、この関数を呼び出すことになるでしょう。
getAssertion 関数は、登録された private key を用いた challenge data のデジタル署名 (Digital Signature) を生成します。(この際も同様に、PIN の入力や、Windows Hello による生体認証がおこなわれます。) Application における以降の認証では、この getAssertion を毎回呼び出して、Digital Signature を生成します。

 

プログラミング (Programming)

W3C で定義されている API の名前空間は webauthn.makeCredential、webauthn.getAssertion ですが、上述の通り、現在はドラフト仕様に基づく ms-prefix の実装であるため、現在 (2016/05 時点) の Edge の実装では、明示的に msCredentials.makeCredential、msCredentials.getAssertion を使用します。

下記は、これらの API を使った簡単な実装例です。

<!DOCTYPE html>
<html>
<head>
<title>Web Authentication API Test</title>
<script language="javascript">
function make() {
  var accountInfo = {
    rpDisplayName: 'Contoso', // Name of relying party
    userDisplayName: 'Tsuyoshi Matsuzaki' // Name of user account
  };
  var cryptoParameters = [
    {
      type: 'FIDO_2_0',
      algorithm: 'RSASSA-PKCS1-v1_5'
    }
  ];
  
  msCredentials.makeCredential(accountInfo, cryptoParameters)
  .then(function (result) {
    // for debugging
    document.getElementById('credID').value = result.id;
    document.getElementById('publicKey').value = JSON.stringify(result.publicKey);
    //alert(result.algorithm)
    //alert(result.attestation)
  }).catch(function (err) {
    alert('err: ' + err.message);
  });
}

function sign() {
  var credID = document.getElementById('credID').value;
  var filters = {
    accept:[
      {
        type: 'FIDO_2_0',
        id: credID
      }
    ]
  };
  msCredentials.getAssertion('challenge value', filters)
  .then(function(result) {
    //for debugging
    document.getElementById('signature').value = result.signature.signature;    
    document.getElementById('authnrdata').value = result.signature.authnrData;    
    document.getElementById('clientdata').value = result.signature.clientData;    
  }).catch(function (err) {
    alert('err: ' + err.message);
  });
}
</script>
</head>
<body>
  <button onclick="make()">Make</button>
  <button onclick="sign()">Sign</button>
  <div>
    Credential ID:<input type="text" size="120" id="credID"><br>
    Public Key:<input type="text" size="120" id="publicKey"><br>
    Base64 Encoded Signature:<input type="text" size="120" id="signature"><br>
    Base64 Encoded AuthnrData:<input type="text" size="120" id="authnrdata"><br>
    Base64 Encoded ClientData:<input type="text" size="120" id="clientdata"><br>
  </div>
</body>
</html>

この Web Application では、[Make] ボタンを押すと、makeCredential 関数が呼ばれて、下図の通り PIN 入力や生体認証 (Windows Hello) が促され、Device 側の認証に成功すると、key pair が作成され、Key Container (Device) に private key が登録されて、下図の通り public key (Json の値) が返ってきます。

返される publicKey は、下記のフォーマットの JSON Web Key と呼ばれる key で、今回、このサンプル プログラムでは、受け取った public key をテキストボックスに書き込んでいますが (上図参照)、通常は、この public key を Application 側 (Service 側) に保持 (記憶) しておいて、今後の認証 (getAssertion の処理) に備えます。

{
  "kty": "RSA",
  "alg": "RS256",
  "ext": false,
  "n": "xCqz2wEsl-3...",
  "e": "AQAB"
}

補足 : makeCredential で返される id (上記サンプルコードの中の credID) は、key identifier です。(「Windows Hello を使った App 開発」で紹介した KeyCredentialManager API の RequestCreateAsync, OpenAsync で渡した引数と同じ位置づけの id です。)
この key identifier は、今後、makeCredential に引数として渡して、あらかじめ指定可能になる予定ですが、現時点では、まだ事前の指定は不可能です。(なお、同じ key identifier を使って何度も makeCredential を呼ぶと、key が上書きされます。)
この key identifier は、一度忘れたら取り出す方法 (API など) はないので注意してください。一度作成したら、アプリ側で必ずおぼえておいてください

[Sign] ボタンを押すと、getAssertion 関数が実行されるため、この場合も、再度、PIN 入力や生体認証 (Windows Hello) が促されます。認証に成功すると、上記の getAssertion 関数の引数に指定した challenge data (今回の場合、「challengae value」の文字列) に対する Digital Signature (デジタル署名) を生成して、この署名の値を返します。(上記サンプル コードの authnrData, clientData については後述します。)

このあと見ていきますが、Application 側では、この受け取った Digital Signature (デジタル署名) が正しいかどうかを、あらかじめ保持 (記憶) しておいた public key を使って検証 (verify) します。(少しコツがあるので、後述)

 

Polyfill を使ったプログラミング

MS-Prefix (独自の名前空間) ではなく、W3C 標準の API を使ってプログラミングしたい場合は、webauthn.js の polyfill が提供されています。
この polyfill を使うと、下記の通り、webauthn.makeCredential、webauthn.getAssertion などを使った標準的なプログラミングが可能です。(返り値の取得方法など、細かな内容も上記と異なっていますので比較してみてください。)

<!DOCTYPE html>
<html>
<head>
<title>Web Authentication polyfill test</title>
<script src="webauthn.js"></script>
<script language="javascript">
function make() {
  var accountInfo = {
    rpDisplayName: 'Contoso', // Name of relying party
    userDisplayName: 'Tsuyoshi Matsuzaki' // Name of user account
  };
  var cryptoParameters = [
    {
      type: 'ScopedCred',  // also 'FIDO_2_0' is okay !
      algorithm: 'RSASSA-PKCS1-v1_5'
    }
  ];
  webauthn.makeCredential(accountInfo, cryptoParameters)
  .then(function (result) {
    document.getElementById('credID').value = result.credential.id;
    document.getElementById('publicKey').value = JSON.stringify(result.publicKey);
  }).catch(function (err) {
      alert('err: ' + err.message);
  });
}

function sign() {
  webauthn.getAssertion('challenge value')
  .then(function(result) {
    document.getElementById('signature').value = result.signature;    
    document.getElementById('authnrdata').value = result.authenticatorData;    
    document.getElementById('clientdata').value = result.clientData;    
  }).catch(function (err) {
    alert('err: ' + err.message);
  });
}
</script>
</head>
<body>
  <button onclick="make()">Make</button>
  <button onclick="sign()">Sign</button>
  <div>
    Credential ID:<input type="text" size="120" id="credID"><br>
    Public Key:<input type="text" size="120" id="publicKey"><br>
    Base64 Encoded Signature:<input type="text" size="120" id="signature"><br>
    Base64 Encoded AuthnrData:<input type="text" size="120" id="authnrdata"><br>
    Base64 Encoded ClientData:<input type="text" size="120" id="clientdata"><br>
  </div>
</body>
</html>

なお、このサンプルでは、上記の通り、Credential Id (前述の key identifier) を指定せずに getAsserion を呼び出せていますが、polyfill 内部で Credential Id を indexedDB に保持しています。(最終的にブラウザーに実装された場合は、ここはブラウザー側の組み込み機能として実装されることになるでしょう。)

 

FIDO 2 準拠な Signature 検証 (Verify)

上記で取得した Digital Signature (デジタル署名) は、「Azure AD を使った Service (API) 開発 (access token の verify)」で紹介したように、RSA に基づく標準的な方法 (例えば、PHP の場合は、openssl_verify 関数を使うなど) で検証できます。
ただし、いくつか FIDO 2.0 の署名の扱いに関するお作法 (注意点) があるのでコードと一緒に紹介しておきます。(この仕様の詳細については「W3C : Web API for accessing FIDO 2.0 credentials」を参照してください。)

まず、この取得されるデジタル署名 (Digital Signature) は、上記の challenge data (getAssertion の第一引数) に対する直接的な署名ではありません。
上記の getAssertion の結果返ってくる authnrData (authenticatorData) と clientData の Hash の文字列結合を challenge としたデジタル署名 (Digital Signature) となっています。なお、この clientData は、getAssertion で渡した challenge data (第一引数) を使った下記フォーマットの JSON データを Base64 Url エンコードした文字列 (Base64 エンコードをおこない、+ を -、/ を _ に変換して、= を削除した文字列) ですので、意味的には getAssertion で指定した challenge data と同義です。(Json 化されただけと思ってください。)

{ "challenge" : "challenge value" }

このため、Digital Signature の検証は、例えば、PHP で記述した場合は、下記の通りとなります。(下記の $sig_enc が、検証対象の Digital Signature です。)

なお、上述の通り、今回使用する public key は、JSON 形式の modulus (上記 jwt の “n”) と exponent (上記 jwt の “e”) の指定された RSA 公開鍵のため、PEM エンコードを使って下記の通り PHP (Openssl) で扱える public key に変換してから、openssl_verify 関数で検証しています。(この他に、Crypt_RSA を使う方法などもあるでしょう。)

<?php
function verify_signature() {
  // input value (see previous)
  $challenge = 'challenge value';
  $sig_enc = 'nF7SxLHfOd...';
  $n = 'xCqz2wEsl-3...';
  $e = 'AQAB';
  $authnrdata_enc = 'AQAAAAA';
  $clientdata_enc = 'ew0KCSJjaG...';
  
  // return 1, if signature is valid
  // return 0, if signature is invalid
  $res = 0;
  
  // get signature
  $sig = base64_url_decode($sig_enc);
  
  // get signed target input (nonce)
  $authnrdata = base64_url_decode($authnrdata_enc);
  $clientdata = base64_url_decode($clientdata_enc);
  $signeddata = $authnrdata . hash('sha256', $clientdata, true);

  // get public key
  $modulus = strtr($n, '-_', '+/'); // "=" erased base64 encode
  $exponent = strtr($e, '-_', '+/'); // "=" erased base64 encode
  $cert_data = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA' . $modulus . 'ID' . $exponent;
  $cert_txt = '-----BEGIN PUBLIC KEY-----' . "\r\n"
    . wordwrap($cert_data, 64, "\r\n", true) . "\r\n"
    . '-----END PUBLIC KEY-----';
  $pkey_obj = openssl_pkey_get_public($cert_txt);
  $pkey_arr = openssl_pkey_get_details($pkey_obj);
  $pkey_txt = $pkey_arr['key'];
  $res = $pkey_txt;

  // verify signature
  $res = openssl_verify($signeddata, $sig, $pkey_txt, OPENSSL_ALGO_SHA256);
  // if error occured, please check as following
  // $test = openssl_error_string();

  return $res;
}

// Helper functions
function base64_url_decode($arg) {
  $res = $arg;
  $res = strtr($res, '-_', '+/');
  switch (strlen($res) % 4) {
    case 0:
      break;
    case 2:
      $res .= "==";
      break;
    case 3:
      $res .= "=";
      break;
    default:
      break;
  }
  $res = base64_decode($res);
  return $res;
}
?>

 

Origin (Domain) による Isolation

Windows Hello を使った App 開発」で紹介したように、UWP の KeyCredentialManager API では、User ごと、App ごとに使用する key が分離されていました。(例えば、App をまたがって key の共有は不可能でした。)
これと同様、この Web Authentication API では、User ごと、Domain (Origin) ごとに分離されます。

例えば、Web サイト A で作成した key を、同じ key identifier を指定して Web サイト B から使おうとしても、セキュリティ上の観点から不可能です。(もしこれができてしまうと、別のサイトから乗っ取られてしまいますね。)

つまり、KeyCredentialManager のときと同様、この API も、Azure AD や Office 365 など、別のサービスの認証に使うことはできないので注意が必要です。あくまでも、皆さん自身の Web Application の認証に使うための API と考えてください。

 

※ 参考情報

[Windows Blog] A world without passwords: Windows Hello in Microsoft Edge
https://blogs.windows.com/msedgedev/2016/04/12/a-world-without-passwords-windows-hello-in-microsoft-edge/

 

Comments (0)

Skip to main content