Chrome 已驗證存取權開發人員指南

關於這份指南

Chrome Verified Access API 可讓網路服務 (例如 VPN) 和內部網路 以加密方式 驗證客戶是否真實可靠 符合公司政策大多數大型企業都要求 只允許企業管理的裝置透過 WPA2 EAP-TLS 網路存取。 ,以及雙向傳輸層安全標準 (TLS) 內部網路的層級較高的存取權許多現有 會仰賴經驗法則檢查, 遭到入侵的漏洞這便是工具中的挑戰 本裝置的合法狀態, 偽造本指南提供硬體支援的加密編譯保證, 裝置識別資訊,以及裝置狀態未經修改且符合政策規定 執行時;稱為「已驗證存取權」

主要目標對象 企業 IT 網域管理員
技術元件 ChromeOS、Google Verified Access API

使用「已驗證存取權」的先決條件

實作「已驗證存取權」程序前,請先完成下列設定。

啟用 API

設定 Google API 控制台專案並啟用 API:

  1. 在以下位置建立或使用現有專案: Google API 控制台
  2. 前往 已啟用的 API 與服務網頁。
  3. 啟用 Chrome Verified Access API
  4. 按照 Google Cloud API 說明文件為應用程式建立 API 金鑰。

建立服務帳戶

為了讓網路服務存取 Chrome Verified Access API 以驗證 「挑戰/回應」 建立服務帳戶和服務帳戶金鑰 (您不需要建立新的 Cloud 專案,可以使用同一個專案)。

建立服務帳戶金鑰後,您應擁有服務帳戶 已下載私密金鑰檔案。這是私密金鑰的唯一副本,因此請 請務必妥善保存

註冊受管理的 Chromebook 裝置

必須使用經過妥善管理的 Chromebook 裝置設定 Chrome 「已驗證存取權」的擴充功能

  1. Chromebook 裝置必須註冊企業或教育管理服務
  2. 裝置的使用者必須是來自相同網域的註冊使用者。
  3. 您必須在裝置上安裝「已驗證存取權」的 Chrome 擴充功能。
  4. 政策設為啟用「已驗證存取權」,請將 Chrome 加入許可清單 擴充功能,並將存取 API 存取權授予代表 網路服務 (請參閱 Google 管理控制台說明文件)。

驗證使用者和裝置

開發人員可以使用「已驗證存取權」向使用者或裝置驗證,或同時使用這兩種做法 ,為帳戶多添一層保障:

  • 裝置驗證:如果成功,裝置驗證會提供 確保 Chrome 裝置已在受管理的網域中註冊,且該裝置 符合網域指定的驗證開機模式裝置政策 管理員。如果網路服務授予查看裝置的權限 身分 (請參閱 Google 管理控制台說明文件),也會收到 裝置 ID 可用於稽核、追蹤或呼叫 Directory API

  • 使用者驗證:如果成功,使用者驗證就能保證 已登入的 Chrome 使用者是受管理的使用者,且正在使用已註冊的裝置。 必須符合網域指定的驗證開機模式使用者政策 管理員。如果網路服務授予接收權限 額外的使用者資料時,也會取得已核發的憑證簽署要求 使用者 (CSR,格式為 Sign-public-key-and-challenge,或 SPKAC), 也稱為 Keygen 格式)。

如何驗證使用者和裝置

  1. 取得驗證問題:裝置上的 Chrome 擴充功能與「已驗證」連線 存取 API 來取得驗證。面臨的難題是在資料中 容器 (Google 簽署的 blob),有效期限為 1 分鐘 如果使用過時驗證方式,驗證回應驗證 (步驟 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
      }
    };
    

    編碼挑戰的輔助程式碼:如果您使用的是 API 第 1 版, 將需要編碼。

    // 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);
      }
    };
    
  2. 產生驗證問題:Chrome 擴充功能會使用驗證方式 以便呼叫 enterprise.platformKeys Chrome API。這個 產生已簽署的加密挑戰回應,而 包含在傳送至網路服務的存取要求中。

    在這個步驟中,系統不會嘗試定義擴充功能 以及網路服務使用的通訊方式這兩種實體都會 而並未主張自己彼此之間的溝通方式。一個 可能是以查詢字串形式傳送 (網址編碼) 驗證回應 參數,透過 HTTP POST 或使用特殊的 HTTP 標頭。

    以下是產生挑戰回應的程式碼範例:

    產生驗證問題回應

      // 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;
      }
    
  3. 驗證挑戰回應:收到來自 裝置 (也許可作為現有驗證通訊協定的擴充功能) 網路服務應呼叫 Verified Access API 來驗證裝置 身分和政策防護機制 (請見下方的程式碼範例)。為打擊詐騙,我們 建議網路服務辨識正在通訊的用戶端 在要求中加入用戶端的預期身分:

    • 如要驗證裝置,請務必提供預期的裝置網域 ,直接在 Google Cloud 控制台實際操作。在許多情況下,這很可能是硬式編碼值,因為網路 服務可保護特定網域中的資源。如果不清楚 便可根據使用者身分推斷。
    • 進行使用者驗證時,預期使用者的電子郵件地址應為 或更新提示我們希望網路服務能夠知道使用者 (一般為它) 將要求使用者登入)。

    呼叫 Google API 時,系統會執行多項檢查,例如:

    • 確認驗證回應是由 ChromeOS 產生,而不是 修改運送中
    • 確認裝置或使用者受到企業管理。
    • 確認裝置/使用者的身分與預期相符 身分 (如果提供後者)。
    • 驗證目前回應的挑戰 即時 (不超過 1 分鐘)。
    • 確認裝置或使用者符合 網域管理員。
    • 確認呼叫端 (網路服務) 已獲得呼叫權限 並嚴謹測試及提升 API 的公平性後 我們才能放心地推出 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();
    }
    
  4. 授予存取權:這個步驟也是網路服務專屬。這是 以下是可能的動作:

    • 建立工作階段 Cookie
    • 為使用者或裝置核發憑證。對於成功的使用者 並假設網路服務已取得存取權 至其他使用者資料 (透過 Google 管理控制台政策), 會收到使用者簽署的 CSR,然後透過這個 CSR 取得憑證授權單位核發的憑證整合 MicrosoftR CA,網路服務可做為 中介商 並可使用 ICertRequest 介面

以「已驗證存取權」使用用戶端憑證

以「已驗證存取權」使用用戶端憑證。

大型組織內可能有多個網路服務 (VPN 伺服器、Wi-Fi 存取點、防火牆,以及多個內部網路網站) 使用「已驗證存取權」功能不過,建構步驟邏輯 每個網路服務中的 2–4 個 (請參閱上一節) 可能無法 實際可行這些網路服務通常已具備 需要用戶端憑證做為驗證的一部分 (例如, EAP-TLS 或雙向 TLS 內部網路網頁)。如果企業憑證 核發這些用戶端憑證的授權者可以執行步驟 2 至 4 和 對「詢問/回應」回應的用戶端憑證核發狀況 憑證的持有者就可能符合 用戶端的真實性,並且符合公司政策。每次 Wi-Fi 連線後 存取點、VPN 伺服器等,可能會檢查這個用戶端憑證 您不必執行步驟 2 到 4

換句話說,這裡的 CA (核發用戶端憑證給企業) 裝置) 擔任網路服務的角色 (如圖 1 所示)。需要叫用 Verified Access API,且只會在收到驗證回應驗證時執行 將憑證提供給用戶端提供憑證給 用戶端等同於步驟 4 的「授予存取權」,如圖 1 所示。

安全取得用戶端憑證到 Chromebook 的程序說明瞭 這篇文章中。如果 依序描述此段落所述的設計,以及「已驗證存取權擴充功能」 和用戶端憑證登入擴充功能可以合併為一個擴充功能瞭解詳情 瞭解如何撰寫用戶端憑證註冊擴充功能