关于本指南
Chrome Verified Access API 允许使用 VPN、Intranet 等网络服务 网页等,以便以加密方式验证其客户是否真实且 符合企业政策。大多数大型企业都要求 仅允许受企业管理的设备连接到其 WPA2 EAP-TLS 网络, VPN 和双向 TLS 内网页面实现更高级别的访问权限。许多现有的 解决方案都依赖于对同一个客户进行启发式检查, 遭到入侵。这就带来了一个挑战,即依赖信号 以证明设备本身的合法状态 伪造的。本指南为 设备的身份,并且设备状态未经修改且符合政策 启动时;称为“已验证的访问权限”
主要受众群体 | 企业 IT 网域管理员 |
技术组件 | ChromeOS、Google Verified Access API |
已验证访问权限的前提条件
在实施“已验证的访问权限”流程之前,请先完成以下设置。
启用 API
设置 Google API 控制台项目并启用 API:
- 在 Google API 控制台。
- 前往 已启用的 API 和服务页面。
- 启用 Chrome Verified Access API。
- 按照 Google Cloud API 文档中的说明,为您的应用创建 API 密钥。
创建服务账号
要让网络服务访问 Chrome Verified Access API 以验证 质询响应, 创建服务账号和服务账号密钥 (您无需创建新的 Cloud 项目,可以使用同一个项目)。
创建服务账号密钥后,您应该就会获得一个服务账号 私钥文件。这是私钥的唯一副本,因此请确保 请务必妥善保存
注册受管理的 Chromebook 设备
您需要对 Chrome 进行妥善管理的 Chromebook 设备设置 “已验证的访问权限”扩展程序
- Chromebook 设备必须已注册企业或教育机构管理。
- 该设备的用户必须是同一网域的注册用户。
- 必须在设备上安装“已验证的访问权限”Chrome 扩展程序。
- 政策已配置为启用已验证的访问权限,请将 Chrome 列入许可名单 并为代表 网络服务(请参阅 Google 管理控制台帮助文档)。
验证用户和设备
开发者可以将“已验证的访问权限”用于用户或设备验证,或者同时使用两者 以提高安全性:
设备验证 - 如果成功,设备验证会提供 保证该 Chrome 设备已在受管理的网域中注册,且该设备 符合网域指定的启动时验证模式设备政策 管理员。网络服务是否已获得查看设备的权限 (请参阅 Google 管理控制台帮助文档),则还会收到 可用于审核、跟踪或调用 Directory API 的设备 ID。
用户验证 - 用户验证成功后,即可保证 已登录的 Chrome 用户是受管理的用户、使用的是已注册的设备,并且 符合网域指定的启动时验证模式用户规范 管理员。如果网络服务被授予接收 其他用户数据,也会获得已签发的证书签名请求 由用户(CSR 以签名公钥和质询 (SPKAC) 的形式提供) 也称为 Keygen 格式)。
如何验证用户和设备
进行验证 - 设备上的 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); } };
生成质询响应 - 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; }
验证质询响应 - 收到 设备(可能是对现有身份验证协议的扩展), 网络服务应调用 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(); }
授予访问权限 - 此步骤也因网络服务而异。这是一个 建议的(非规定的)实施方式。可能的操作包括:
- 创建会话 Cookie
- 为用户或设备颁发证书。如果用户成功 验证并假设网络服务已获得访问权限 其他用户数据的访问权限(通过 Google 管理控制台政策), 它会收到用户签名的 CSR,此 CSR 随后可用于获取实际的 或证书授权中心提供的证书与 MicrosoftR CA,则网络服务可充当 中介 并使用 ICertRequest 接口
将客户端证书与已验证的访问权限搭配使用
<ph type="x-smartling-placeholder">在大型组织中,可能有多个网络服务 (VPN 服务器、Wi-Fi 接入点、防火墙和多个内网网站), 将受益于“经过验证的访问权限”功能但是,构建步骤逻辑 每个网络服务中的 2–4(在上文中) 非常实用通常,许多网络服务都能 要求在验证身份时包含客户端证书(例如 EAP-TLS 或双向 TLS 内网页面)。如果 Enterprise Certificate(企业证书) 颁发这些客户端证书的证书授权机构可以执行第 2-4 步 设置在质询响应上颁发客户端证书的条件 那么持有证书就可以证明 客户真实可信且符合公司政策。此后,每个 WLAN 网络 接入点、VPN 服务器等设备可以检查是否有此客户端证书 而无需按照第 2-4 步进行操作。
也就是向企业颁发客户端证书的 CA 设备)充当图 1 所示的网络服务。它需要调用 Verified Access API (仅在通过质询响应验证时提供) 通过测试,请向客户端提供证书。将证书提供给 该客户端相当于图 1 中的“第 4 步 - 授予访问权限”。
下面介绍了将客户端证书安全地发送到 Chromebook 的过程 这篇文章。如果 验证方式之后, 和客户端证书初始配置扩展程序可以合并为一个。了解详情 了解如何编写客户端证书初始配置扩展程序。