Chrome Verified Access Developer's Guide

About this guide

The Chrome Verified Access API allows network services, such as VPNs, intranet pages, and so on to cryptographically verify that their clients are genuine and conform to corporate policy. Most large enterprises have the requirement to allow only enterprise-managed devices onto their WPA2 EAP-TLS networks, higher-tier access in VPNs, and mutual-TLS intranet pages. Many existing solutions rely on heuristic checks on the same client that may have been compromised. This presents the challenge that the signals being relied on to attest to the legitimate status of the device may themselves have been falsified. This guide provides hardware-backed cryptographic guarantees of the identity of the device and that its state was unmodified and policy compliant at boot; called Verified Access.

Primary audience Enterprise IT domain administrators
Technical components ChromeOS, Google Verified Access API

Prerequisites to Verified Access

Complete the following setup before implementing the Verified Access process.

Enable the API

Setup a Google API console project and enable the API:

  1. Create or use an existing project in the Google API console.
  2. Go to the Enabled APIs & services page.
  3. Enable the Chrome Verified Access API.
  4. Create an API key for your application by following the Google Cloud API documentation.

Create a service account

For the network service to access the Chrome Verified Access API to verify your challenge-response, create a service account and a service account key (you don't need to create a new Cloud project, you may use the same one).

Once you create the service account key, you should then have a service account private key file downloaded. This is the only copy of the private key, so make sure to store it securely.

Enroll a managed Chromebook device

You need a properly managed Chromebook device setup with your Chrome extension for Verified Access.

  1. The Chromebook device must be enrolled for enterprise or education management.
  2. The user of the device must be a registered user from the same domain.
  3. The Chrome extension for Verified Access must be installed on the device.
  4. Policies are configured to enable Verified Access, allowlist the Chrome extension, and grant access to the API for the service account representing the network service (see the Google Admin console Help documentation).

Verify user and device

Developers can use Verified Access for user or device verification, or use both for the added security:

  • Device verification—If successful, device verification provides a guarantee that the Chrome device is enrolled in a managed domain and that it conforms to the verified boot mode device policy as specified by the domain administrator. If the network service is granted a permission to see the device identity (see Google Admin console Help documentation), then it also receives a device ID that can be used for auditing, tracking, or calling the Directory API.

  • User verification—If successful, user verification provides a guarantee that a signed-in Chrome user is a managed user, is using an enrolled device, and conforms to the verified boot mode user policy as specified by the domain administrator. If the network service is granted a permission to receive additional user data, it would also obtain a certificate signing request issued by the user (CSR in the form of signed-public-key-and-challenge, or SPKAC, also known as keygen format).

How to verify user and device

  1. Get a challenge—The Chrome extension on the device contacts the Verified Access API to obtain a challenge. The challenge is an opaque data structure (a Google-signed blob) that’s good for 1 minute, meaning the challenge-response verification (step 3) fails if a stale challenge is used.

    In the simplest use case, the user initiates this flow by clicking a button that the extension generates (this is also what the Google-provided sample extension does).

    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
      }
    };
    

    Helper code to encode challenge—If you are using v1 of the API, the challenge will need to be encoded.

    // 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. Generate a challenge response—The Chrome extension uses the challenge it received in step 1 to call the enterprise.platformKeys Chrome API. This generates a signed and encrypted challenge response, which the extension includes in the access request that it sends to the network service.

    In this step, there’s no attempt to define a protocol that the extension and network service use for communicating. Both of these entities are implemented by external developers and aren’t prescribed how they talk to each other. An example would be sending a (URL-encoded) challenge response as a query string parameter, using HTTP POST, or using a special HTTP header.

    Here’s a sample code to generate a challenge response:

    Generate challenge response

      // 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);
      }
    

    Challenge callback function

      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
       };
      }
    

    Helper code for ArrayBuffer conversion

      /**
       * 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. Verify challenge response—Upon receiving a challenge response from a device (perhaps as an extension to an existing authentication protocol), the network service should call the Verified Access API to verify the device identity and policy posture (see example code below). To combat spoofing, we recommend that the network service identify the client it’s talking to and include the expected identity of the client in its request:

    • For device verification, the expected device domain should be provided . This is likely a hard-coded value in many cases, because the network service protects resources for a particular domain. If this isn’t known ahead of time, it can be inferred from user identity.
    • For user verification, the expected user’s email address should be provided. We expect the network service to know its users (normally it would require users to sign in).

    When the Google API is called, it performs a number of checks, such as:

    • Verify that the challenge response is produced by ChromeOS and isn’t modified in transit
    • Verify that the device or user is enterprise-managed.
    • Verify that the identity of the device/user matches the expected identity (if the latter is provided).
    • Verify that the challenge that’s being responded to is fresh (no more than 1 minute old).
    • Verify that the device or user conforms to the policy as specified by the domain administrator.
    • Verify that the caller (network service) is granted permission to call the API.
    • If the caller is granted permission to obtain additional device or user data, include the device ID or the user’s certificate signing request (CSR) in the response.

    This example uses gRPC library

    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. Grant access—This step is also network-service specific. This is a suggested (not prescribed) implementation. Possible actions could be:

    • Creation of a session cookie
    • Issuing a certificate for the user or device. In case of successful user verification, and assuming the network service has been granted access to additional user data (via the Google Admin console policy), it receives a user-signed CSR, which can then be used to obtain the actual certificate from the certification authority. When integrating with Microsoft CA, the network service could act as an intermediary and make use of the ICertRequest interface.

Using client certificates with Verified Access

Using client certificates with Verified Access.

In a large organization, there may be multiple network services (VPN servers, Wi-Fi access points, firewalls, and multiple intranet sites) that would benefit from Verified Access. However, building the logic of steps 2–4 (in the section above) in each of these network services may not be practical. Often, many of these network services already have the capability to require client certificates as part of their authentications (for example, EAP-TLS or mutual TLS intranet pages). So if the Enterprise Certificate Authority that issues these client certificates could implement steps 2–4 and condition the issuance of the client certificate on the challenge-response verification, then the possession of the certificate could be the proof that the client is genuine and conforms to corporate policy. Thereafter each Wi-Fi access point, VPN server, and so on could check for this client certificate instead of needing to follow steps 2–4.

In other words, here the CA (that issues the client certificate to enterprise devices) takes the role of the Network Service in Figure 1. It needs to invoke the Verified Access API and, only upon the challenge response verification passing, provide the certificate to the client. Providing the certificate to the client is the equivalent of Step 4 - Grant Access in Figure 1.

The process of getting client certificates securely to Chromebooks is described in this article. If the design described in this paragraph is followed, then Verified Access Extension and Client certificate onboarding extension can be combined into one. Learn more about how to write a client certificate onboarding extension.