Chrome Verified Access 开发者指南

关于本指南

Chrome Verified Access API 允许使用 VPN、Intranet 等网络服务 网页等,以便以加密方式验证其客户是否真实且 符合企业政策。大多数大型企业都要求 仅允许受企业管理的设备连接到其 WPA2 EAP-TLS 网络, VPN 和双向 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 设备

您需要对 Chrome 进行妥善管理的 Chromebook 设备设置 “已验证的访问权限”扩展程序

  1. Chromebook 设备必须已注册企业或教育机构管理
  2. 该设备的用户必须是同一网域的注册用户。
  3. 必须在设备上安装“已验证的访问权限”Chrome 扩展程序。
  4. 政策已配置为启用已验证的访问权限,请将 Chrome 列入许可名单 并为代表 网络服务(请参阅 Google 管理控制台帮助文档)。

验证用户和设备

开发者可以将“已验证的访问权限”用于用户或设备验证,或者同时使用两者 以提高安全性:

  • 设备验证 - 如果成功,设备验证会提供 保证该 Chrome 设备已在受管理的网域中注册,且该设备 符合网域指定的启动时验证模式设备政策 管理员。网络服务是否已获得查看设备的权限 (请参阅 Google 管理控制台帮助文档),则还会收到 可用于审核、跟踪或调用 Directory API 的设备 ID。

  • 用户验证 - 用户验证成功后,即可保证 已登录的 Chrome 用户是受管理的用户、使用的是已注册的设备,并且 符合网域指定的启动时验证模式用户规范 管理员。如果网络服务被授予接收 其他用户数据,也会获得已签发的证书签名请求 由用户(CSR 以签名公钥和质询 (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
      }
    };
    

    用于对验证码进行编码的帮助程序代码 - 如果您使用的是 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);
      }
    };
    
  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 API 时,它会执行许多检查,例如:

    • 验证质询响应是否由 ChromeOS 生成,而不是 已在传输中修改
    • 验证设备或用户是否受企业管理。
    • 验证设备/用户的身份是否与预期一致 (如果提供了后者)。
    • 验证作为回复的验证问题 新鲜度(不超过 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();
    }
    
  4. 授予访问权限 - 此步骤也因网络服务而异。这是一个 建议的(非规定的)实施方式。可能的操作包括:

    • 创建会话 Cookie
    • 为用户或设备颁发证书。如果用户成功 验证并假设网络服务已获得访问权限 其他用户数据的访问权限(通过 Google 管理控制台政策), 它会收到用户签名的 CSR,此 CSR 随后可用于获取实际的 或证书授权中心提供的证书与 MicrosoftR CA,则网络服务可充当 中介 并使用 ICertRequest 接口

将客户端证书与已验证的访问权限搭配使用

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> 使用具有“已验证的访问权限”功能的客户端证书。

在大型组织中,可能有多个网络服务 (VPN 服务器、Wi-Fi 接入点、防火墙和多个内网网站), 将受益于“经过验证的访问权限”功能但是,构建步骤逻辑 每个网络服务中的 2–4(在上文中) 非常实用通常,许多网络服务都能 要求在验证身份时包含客户端证书(例如 EAP-TLS 或双向 TLS 内网页面)。如果 Enterprise Certificate(企业证书) 颁发这些客户端证书的证书授权机构可以执行第 2-4 步 设置在质询响应上颁发客户端证书的条件 那么持有证书就可以证明 客户真实可信且符合公司政策。此后,每个 WLAN 网络 接入点、VPN 服务器等设备可以检查是否有此客户端证书 而无需按照第 2-4 步进行操作。

也就是向企业颁发客户端证书的 CA 设备)充当图 1 所示的网络服务。它需要调用 Verified Access API (仅在通过质询响应验证时提供) 通过测试,请向客户端提供证书。将证书提供给 该客户端相当于图 1 中的“第 4 步 - 授予访问权限”。

下面介绍了将客户端证书安全地发送到 Chromebook 的过程 这篇文章。如果 验证方式之后, 和客户端证书初始配置扩展程序可以合并为一个。了解详情 了解如何编写客户端证书初始配置扩展程序