このガイドについて
Chrome Verified Access API を使用すると、VPN やイントラネット ページなどのネットワーク サービスは、クライアントが本物であり、企業のポリシーに準拠していることを暗号によって検証できます。ほとんどの大企業には、WPA2 EAP-TLS ネットワーク、VPN の上位層アクセス、相互 TLS イントラネット ページに、企業が管理するデバイスのみを許可する必要があります。既存の多くのソリューションでは、侵害された可能性のある同じクライアントに対するヒューリスティック チェックが使用されています。これは、デバイスの正当なステータスを証明するために依存する信号が偽造された可能性があるという課題を示します。このガイドでは、デバイスの ID のハードウェア格納型暗号による保証を行い、デバイスの状態が未変更で、起動時にポリシーに準拠していること(確認付きアクセス)について説明します。
主な対象 | 企業の IT ドメイン管理者 |
技術コンポーネント | ChromeOS、Google Verified Access API |
確認済みアクセスの前提条件
確認済みアクセス プロセスを実装する前に、次の設定を完了します。
API を有効にする
Google API Console プロジェクトを設定し、API を有効にします。
- Google API Console でプロジェクトを作成するか、既存のプロジェクトを使用します。
- [有効な API とサービス] ページに移動します。
- Chrome Verified Access API を有効にします。
- Google Cloud API ドキュメントに沿ってアプリケーションの API キーを作成します。
サービス アカウントを作成する
ネットワーク サービスが Chrome Verified Access API にアクセスしてチャレンジ レスポンスを検証するには、サービス アカウントとサービス アカウント キーを作成します(新しい Cloud プロジェクトを作成する必要はなく、同じものを使用できます)。
サービス アカウント キーを作成すると、サービス アカウントの秘密鍵ファイルがダウンロードされます。これは秘密鍵の唯一のコピーであるため、安全な場所に保管してください。
管理対象の Chromebook デバイスを登録する
確認済みアクセス用の Chrome 拡張機能を使用して、適切に管理された Chromebook デバイスをセットアップする必要があります。
- Chromebook デバイスは、企業または教育機関の管理用に登録されている必要があります。
- このデバイスのユーザーは、同じドメインの登録ユーザーである必要があります。
- 確認済みアクセス用の Chrome 拡張機能をデバイスにインストールする必要があります。
- 確認済みアクセスを有効にし、Chrome 拡張機能を許可リストに登録して、ネットワーク サービスを表すサービス アカウントに API へのアクセス権を付与するポリシーが構成されています(Google 管理コンソールのヘルプ ドキュメントをご覧ください)。
ユーザーとデバイスを確認する
デベロッパーは、確認済みアクセスをユーザーまたはデバイスの確認に使用するか、両方を使用してセキュリティを強化できます。
デバイスの確認 - デバイスの確認によって、Chrome デバイスが管理対象ドメインに登録されていること、ドメイン管理者が指定した確認付きブートモードのデバイス ポリシーに準拠していることが保証されます。ネットワーク サービスにデバイス ID を表示する権限が付与されている場合(Google 管理コンソールのヘルプ ドキュメントを参照)は、監査、トラッキング、Directory API の呼び出しに使用できるデバイス ID も受け取ります。
ユーザー確認 - ユーザー確認が成功した場合、ユーザー確認により、ログインしている Chrome ユーザーが管理対象ユーザーであること、登録済みデバイスを使用していること、ドメイン管理者が指定した確認付きブートモードのユーザー ポリシーに準拠していることが保証されます。追加のユーザーデータを受信する権限がネットワーク サービスに付与されると、ユーザーが発行した証明書署名リクエストも取得されます(CSR(署名済み公開鍵とチャレンジ(SPKAC)、keygen 形式とも呼ばれます)の形式)。
ユーザーとデバイスを確認する方法
チャレンジを取得する - デバイスの Chrome 拡張機能が Verified Access API に接続して、チャレンジを取得します。課題は、1 分間有効な不透明なデータ構造(Google によって署名された blob)です。つまり、古いチャレンジを使用すると、チャレンジ レスポンスの検証(ステップ 3)が失敗します。
最も単純なユースケースでは、拡張機能が生成したボタンをクリックしてこのフローを開始します(これは Google 提供のサンプル拡張機能でも行われます)。
var apiKey = 'YOUR_API_KEY_HERE'; var challengeUrlString = 'https://verifiedaccess.googleapis.com/v2/challenge:generate?key=' + apiKey; // Request challenge from URL var xmlhttp = new XMLHttpRequest(); xmlhttp.open('POST', challengeUrlString, true); xmlhttp.send(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4) { var challenge = xmlhttp.responseText; console.log('challenge: ' + challenge); // v2 of the API returns an encoded challenge so no further challenge processing is needed } };
チャレンジをエンコードするヘルパーコード - v1 の API を使用している場合は、チャレンジをエンコードする必要があります。
// This can be replaced by using a third-party library such as // [https://github.com/dcodeIO/ProtoBuf.js/wiki](https://github.com/dcodeIO/ProtoBuf.js/wiki) /** * encodeChallenge convert JSON challenge into base64 encoded byte array * @param {string} challenge JSON encoded challenge protobuf * @return {string} base64 encoded challenge protobuf */ var encodeChallenge = function(challenge) { var jsonChallenge = JSON.parse(challenge); var challengeData = jsonChallenge.challenge.data; var challengeSignature = jsonChallenge.challenge.signature; var protobufBinary = protoEncodeChallenge( window.atob(challengeData), window.atob(challengeSignature)); return window.btoa(protobufBinary); }; /** * protoEncodeChallenge produce binary encoding of the challenge protobuf * @param {string} dataBinary binary data field * @param {string} signatureBinary binary signature field * @return {string} binary encoded challenge protobuf */ var protoEncodeChallenge = function(dataBinary, signatureBinary) { var protoEncoded = ''; // See https://developers.google.com/protocol-buffers/docs/encoding // for encoding details. // 0x0A (00001 010, field number 1, wire type 2 [length-delimited]) protoEncoded += '\u000A'; // encode length of the data protoEncoded += varintEncode(dataBinary.length); // add data protoEncoded += dataBinary; // 0x12 (00010 010, field number 2, wire type 2 [length-delimited]) protoEncoded += '\u0012'; // encode length of the signature protoEncoded += varintEncode(signatureBinary.length); // add signature protoEncoded += signatureBinary; return protoEncoded; }; /** * varintEncode produce binary encoding of the integer number * @param {number} number integer number * @return {string} binary varint-encoded number */ var varintEncode = function(number) { // This only works correctly for numbers 0 through 16383 (0x3FFF) if (number <= 127) { return String.fromCharCode(number); } else { return String.fromCharCode(128 + (number & 0x7f), number >>> 7); } };
チャレンジ レスポンスを生成する - Chrome 拡張機能は、ステップ 1 で受け取ったチャレンジを使用して Chrome API enterprise.platformKeys を呼び出します。これにより、署名付きの暗号化されたチャレンジ レスポンスが生成されます。これは拡張機能がネットワーク サービスに送信するアクセス リクエストに含めます。
このステップでは、拡張機能とネットワーク サービスが通信に使用するプロトコルは定義しません。どちらのエンティティも外部のデベロッパーによって実装され、相互通信方法は規定されていません。たとえば、HTTP POST または特別な HTTP ヘッダーを使用して、(URL エンコードされた)チャレンジ レスポンスをクエリ文字列パラメータとして送信します。
チャレンジ レスポンスを生成するサンプルコードを次に示します。
チャレンジのレスポンスを生成する
// Generate challenge response var encodedChallenge; // obtained by generate challenge API call try { if (isDeviceVerification) { // isDeviceVerification set by external logic chrome.enterprise.platformKeys.challengeKey( { scope: 'MACHINE', challenge: decodestr2ab(encodedChallenge), }, ChallengeCallback); } else { chrome.enterprise.platformKeys.challengeKey( { scope: 'USER', challenge: decodestr2ab(encodedChallenge), registerKey: { 'RSA' }, // can also specify 'ECDSA' }, ChallengeCallback); } } catch (error) { console.log('ERROR: ' + error); }
チャレンジ コールバック関数
var ChallengeCallback = function(response) { if (chrome.runtime.lastError) { console.log(chrome.runtime.lastError.message); } else { var responseAsString = ab2base64str(response); console.log('resp: ' + responseAsString); ... // send on to network service }; }
ArrayBuffer 変換のヘルパーコード
/** * ab2base64str convert an ArrayBuffer to base64 string * @param {ArrayBuffer} buf ArrayBuffer instance * @return {string} base64 encoded string representation * of the ArrayBuffer */ var ab2base64str = function(buf) { var binary = ''; var bytes = new Uint8Array(buf); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); } /** * decodestr2ab convert a base64 encoded string to ArrayBuffer * @param {string} str string instance * @return {ArrayBuffer} ArrayBuffer representation of the string */ var decodestr2ab = function(str) { var binary_string = window.atob(str); var len = binary_string.length; var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } return bytes.buffer; }
チャレンジ レスポンスを検証する - デバイスからチャレンジ レスポンス(既存の認証プロトコルの拡張など)を受信すると、ネットワーク サービスは Verified Access API を呼び出して、デバイスの ID とポリシーの状態を検証する必要があります(下記のサンプルコードを参照)。なりすましに対処するには、通信先のクライアントをネットワーク サービスで識別し、クライアントに想定される ID をリクエストに含めることをおすすめします。
- デバイスを確認する場合は、想定されるデバイス ドメインを指定する必要があります。ネットワーク サービスは特定のドメインのリソースを保護するため、多くの場合、この値はハードコードされている可能性があります。事前に不明な場合は、ユーザー ID から推測できます。
- ユーザー確認の場合、想定されるユーザーのメールアドレスを指定する必要があります。Google は、ネットワーク サービスがユーザーを認識していることを想定しています(通常はユーザーのログインが必要です)。
Google API が呼び出されると、次のようなチェックが行われます。
- チャレンジのレスポンスが ChromeOS によって生成され、転送中に変更されていないことを確認する
- デバイスまたはユーザーが企業で管理されていることを確認します。
- デバイス/ユーザーの ID が想定された ID と一致することを確認します(後者が指定されている場合)。
- 回答対象のチャレンジが最新(1 分以内)であることを確認します。
- デバイスまたはユーザーが、ドメイン管理者が指定したポリシーを遵守していることを確認します。
- 呼び出し元(ネットワーク サービス)に、API を呼び出す権限が付与されていることを確認します。
- 追加のデバイスやユーザーデータを取得する権限が呼び出し元に付与されている場合は、レスポンスにデバイス ID またはユーザーの証明書署名リクエスト(CSR)を含めます。
この例は gRPC ライブラリを使用しています。
import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.chrome.verifiedaccess.v2.VerifiedAccessGrpc; import com.google.chrome.verifiedaccess.v2.VerifyChallengeResponseRequest; import com.google.chrome.verifiedaccess.v2.VerifyChallengeResponseResult; import com.google.protobuf.ByteString; import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; import io.grpc.auth.ClientAuthInterceptor; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyChannelBuilder; import java.io.File; import java.io.FileInputStream; import java.util.Arrays; import java.util.concurrent.Executors; // https://cloud.google.com/storage/docs/authentication#generating-a-private-key private final String clientSecretFile = "PATH_TO_GENERATED_JSON_SECRET_FILE"; private ManagedChannel channel; private VerifiedAccessGrpc.VerifiedAccessBlockingStub client; void setup() { channel = NettyChannelBuilder.forAddress("verifiedaccess.googleapis.com", 443) .sslContext(GrpcSslContexts.forClient().ciphers(null).build()) .build(); List<ClientInterceptor> interceptors = Lists.newArrayList(); // Attach a credential for my service account and scope it for the API. GoogleCredentials credentials = ServiceAccountCredentials.class.cast( GoogleCredentials.fromStream( new FileInputStream(new File(clientSecretFile)))); credentials = credentials.createScoped( Arrays.<String>asList("https://www.googleapis.com/auth/verifiedaccess")); interceptors.add( new ClientAuthInterceptor(credentials, Executors.newSingleThreadExecutor())); // Create a stub bound to the channel with the interceptors applied client = VerifiedAccessGrpc.newBlockingStub( ClientInterceptors.intercept(channel, interceptors)); } /** * Invokes the synchronous RPC call that verifies the device response. * Returns the result protobuf as a string. * * @param signedData base64 encoded signedData blob (this is a response from device) * @param expectedIdentity expected identity (domain name or user email) * @return the verification result protobuf as string */ public String verifyChallengeResponse(String signedData, String expectedIdentity) throws IOException, io.grpc.StatusRuntimeException { VerifyChallengeResponseResult result = client.verifyChallengeResponse(newVerificationRequest(signedData, expectedIdentity)); // will throw StatusRuntimeException on error. return result.toString(); } private VerifyChallengeResponseRequest newVerificationRequest( String signedData, String expectedIdentity) throws IOException { return VerifyChallengeResponseRequest.newBuilder() .setChallengeResponse( ByteString.copyFrom(BaseEncoding.base64().decode(signedData))) .setExpectedIdentity(expectedIdentity == null ? "" : expectedIdentity) .build(); }
アクセスを許可する - このステップはネットワーク サービスに固有のものです。これは推奨の実装であり、推奨ではありません。考えられる対応は次のとおりです。
- セッション Cookie の作成
- ユーザーまたはデバイスの証明書を発行する。ユーザー検証が成功し、ネットワーク サービスに(Google 管理コンソールのポリシーを介して)追加のユーザーデータへのアクセス権が付与されていると仮定すると、ユーザーが署名した CSR がネットワーク サービスに送信されます。この CSR を使用して、認証局から実際の証明書を取得できます。MicrosoftⓇ CA と統合する場合、ネットワーク サービスは仲介者として機能し、ICertRequest インターフェースを使用できます。
確認済みアクセスでのクライアント証明書の使用
大規模な組織では、確認済みアクセスの恩恵を受けるネットワーク サービスが複数存在することがあります(VPN サーバー、Wi-Fi アクセス ポイント、ファイアウォール、複数のイントラネット サイト)。ただし、これらの各ネットワーク サービスで(上記のセクションの)ステップ 2 ~ 4 のロジックを構築するのは現実的ではない場合があります。多くの場合、これらのネットワーク サービスの多くは、認証の一部としてクライアント証明書を必要とする機能をすでに備えています(EAP-TLS や相互 TLS イントラネット ページなど)。したがって、これらのクライアント証明書を発行する企業認証局が手順 2 ~ 4 を実施し、クライアント証明書の発行をチャレンジ レスポンス検証で条件とできる場合、証明書の所有は、クライアントが本物であり、企業ポリシーに準拠していることを証明できます。その後、各 Wi-Fi アクセス ポイントや VPN サーバーなどが、手順 2 ~ 4 を行う代わりに、このクライアント証明書を確認できます。
つまり、企業デバイスにクライアント証明書を発行する CA が、図 1 のネットワーク サービスの役割を担っています。Verified Access API を呼び出し、チャレンジ レスポンスの検証に合格した場合にのみ、クライアントに証明書を提供します。クライアントに証明書を提供するのは、図 1 の「ステップ 4 - アクセスを許可する」と同じです。
クライアント証明書を Chromebook に安全に取得する方法については、こちらの記事をご覧ください。この段落で説明されている設計に従う場合、Verified Access Extension とクライアント証明書オンボーディング拡張機能を 1 つに統合できます。詳しくは、クライアント証明書オンボーディング拡張機能を作成する方法をご覧ください。