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 .

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 .

Design a Mobile Site
View Site in Mobile | Classic
Share by: