Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement][integrity] Add Accept-CH header to obtain User-Agent Client Hints #389

Open
t2ym opened this issue Sep 3, 2020 · 0 comments

Comments

@t2ym
Copy link
Owner

t2ym commented Sep 3, 2020

[Enhancement][integrity] Add Accept-CH header to obtain User-Agent Client Hints

Backgrounds

Expected Handling in Validation Service/Console (tentative)

  • Validate the frozen User-Agent header as before in integrityService.js
  • Pick up values of some of these request headers to produce keys for browser differentiation with fixed User-Agent headers
    sec-ch-ua: "Chromium";v="86", "\"Not\\A;Brand";v="99", "Google Chrome";v="86"
    sec-ch-ua-arch: "x86"
    sec-ch-ua-full-version: "86.0.4240.8"
    sec-ch-ua-mobile: ?0
    sec-ch-ua-platform: "Linux"
  • sec-ch-ua and sec-ch-ua-mobile request headers are attached even when accept-ch response header is missing

Security Considerations

  • navigator.userAgentData.brands contains values of sec-ch-ua header
    • Values of other sec-ch-ua* headers can be obtained via navigator.userAgentData.getHighEntropyValues()
navigator.userAgentData.getHighEntropyValues(    [
      "platform",
      "platformVersion",
      "architecture",
      "model",
      "uaFullVersion"
    ]).then(values => console.log(JSON.stringify(values, null, 2)))
{
  "architecture": "x86",
  "model": "",
  "platform": "Linux",
  "platformVersion": "",
  "uaFullVersion": "85.0.4183.83"
}
navigator.userAgentData.brands =
[
  {
    "brand": "\\Not;A\"Brand",
    "version": "99"
  },
  {
    "brand": "Google Chrome",
    "version": "85"
  },
  {
    "brand": "Chromium",
    "version": "85"
  }
]
  • The request headers might be altered by MITM attackers or proxies
  • Encrypted ClientIntegrity.BrowserHash is still consistent and reliable
    • It should be better to exclude navigator.userAgentData from the hash generation since the object can contain randomized data, whose randomization cycles are not confirmed but can be per browser launch
  • It is essential for Validation Service/Console to validate clients with genuine sec-ch-ua* headers and associated ClientIntegrity.BrowserHash

Change

  • Add --clientHints option to demo-backend/demoServer.js and demo-backend/integrityService.js
    • Default accept-ch header value: UA, UA-Arch, UA-Platform, UA-Full-Version
      • Note: UA-Model and UA-Platform-Version should be unnecessary for Validation Service/Console
    • How to disable accept-ch response header
diff --git a/package.json b/package.json
index 5c443eb1..f6980032 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,8 @@
     "cache-bundle": "run-p -r -l buildServer cacheBundleUploadService cacheBundleGeneration",
     "updateHtmlHash": "run-p -r -l buildServer cacheBundleUploadService loadOnly",
     "buildServer": "node demo-backend/demoServer.js -p 8080 -m build -P https -H \"localhost:8080\"",
-    "demoServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\"",
-    "httpsServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -P https -H \"${SERVER_HOST}:8080\"",
+    "demoServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\" --clientHints \"\"",
+    "httpsServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -P https -H \"${SERVER_HOST}:8080\" --clientHints \"\"",
     "debugServer": "node --inspect-brk=0.0.0.0:9229 demo-backend/demoServer.js -p 8080 -m debug -c 1 -H \"${SERVER_HOST}\"",
     "postHtml": "run-p -l postHtmlServer errorReportService",
     "postHtmlServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\" --middleware ./postHtml.js",
  • Add "accept-ch" response header to request User-Agent Client Hints headers from browsers
diff --git a/demo-backend/demoServer.js b/demo-backend/demoServer.js
index 7f5878a9..46a9efaa 100644
--- a/demo-backend/demoServer.js
+++ b/demo-backend/demoServer.js
@@ -25,6 +25,8 @@ const defaultServerPort = 8080;
 const defaultMode = 'debug';
 const defaultProtocol = 'http';
 const defaultHostName = 'localhost';
+const defaultClientHints = 'default';
+const defaultClientHintsForHelp = 'UA, UA-Arch, UA-Platform, UA-Full-Version';
 const demoCAPath = 'demo-keys/demoCA/';
 const whitelistPath = path.join(__dirname, 'whitelist.json');
 const blacklistPath = path.join(__dirname, 'blacklist.json');
@@ -40,6 +42,7 @@ argParser.addArgument([ '-p', '--port' ], { help: 'HTTP server port to listen. D
 argParser.addArgument([ '-m', '--mode' ], { help: 'Server mode "build" or "debug". Default: ' + defaultMode });
 argParser.addArgument([ '-P', '--protocol'], { help: 'Server protocol. Default: ' + defaultProtocol });
 argParser.addArgument([ '-H', '--host'], { help: 'Server host name. Default: ' + defaultHostName });
+argParser.addArgument([ '--clientHints'], { help: 'Accept-CH header for User-Agent Client Hints. Default: ' + defaultClientHintsForHelp });
 argParser.addArgument([ '--middleware'], { help: 'middleware to import. Default: null' });
 const args = argParser.parseArgs();
 
@@ -56,6 +59,7 @@ else {
   const middleware = args.middleware ? require(args.middleware) : null;
   const mode = args.mode || defaultMode;
   const protocol = args.protocol || defaultProtocol;
+  const clientHints = typeof args.clientHints === 'string' ? args.clientHints : defaultClientHints;
   let hostname = args.host || defaultHostName;
   if (!hostname.split(':')[0]) {
     hostname = [defaultHostName, hostname.split(':')[1]].join(':');
@@ -161,7 +165,7 @@ else {
       }
     })
     */
-    .all('/*', integrityService({ mode, entryPageURLPath, authority: hostname, whitelist, blacklist }))
+    .all('/*', integrityService({ mode, entryPageURLPath, authority: hostname, whitelist, blacklist, clientHints }))
     .use(errorReportServiceURLPath, proxy({
       target: errorReportServiceOriginURL, 
       changeOrigin: true,
diff --git a/demo-backend/integrityService.js b/demo-backend/integrityService.js
index fa3d399f..193a35b2 100644
--- a/demo-backend/integrityService.js
+++ b/demo-backend/integrityService.js
@@ -222,6 +222,8 @@ const CacheControl = {
   encryptedResponse: 'no-cache, no-transform',
 };
 
+const clientHintsDefault = 'UA, UA-Arch, UA-Platform, UA-Full-Version';
+
 const RecordType = {
   size: 1, // byte
   Connect: 0x01, // client -> server
@@ -311,6 +313,7 @@ const setResponseHeaders = function setResponseHeaders(req, res, {
       'date': true,
       //'cache-control': true,
     },
+    clientHints = 'default',
   }) {
   res.removeHeader('content-length');
   if (body) {
@@ -332,7 +335,9 @@ const setResponseHeaders = function setResponseHeaders(req, res, {
     res.removeHeader('cache-control');
     res.setHeader('cache-control', CacheControl.integrityResponse);
   }
-  res.setHeader('accept-ch', 'UA, UA-Arch, UA-Platform, UA-Model, UA-Full-Version, UA-Platform-Version');
+  if (clientHints) {
+    res.setHeader('accept-ch', clientHints === 'default' ? clientHintsDefault : clientHints);
+  }
   res.setHeader('x-status', '' + statusCode);
   res.setHeader('x-scheme', scheme);
   if (authority) {
@@ -738,7 +743,7 @@ const prepareNextSession = function prepareNextSession(req, res, Record, Accept,
 }
 
 const aboutBlankRedirectorHTML = `<script no-hook>location = 'about:blank';</script>`;
-const integrityService = function integrityService({ mode = 'debug', entryPageURLPath = '/', authority = 'localhost', whitelist = null, blacklist = null }) {
+const integrityService = function integrityService({ mode = 'debug', entryPageURLPath = '/', authority = 'localhost', whitelist = null, blacklist = null, clientHints = 'default' }) {
   const integrityServiceURLPath = path.join(entryPageURLPath, 'integrity');
   validate = validationService({ mode, host: process.env['VALIDATION_HOST'] || 'localhost', keys });
   //const aboutBlankURL = mode !== 'debug' ? 'about:blank' : 'about:blank?from=integrityService.js';
@@ -805,6 +810,7 @@ const integrityService = function integrityService({ mode = 'debug', entryPageUR
           salt: NextSession.server_write_salt,
           body: responseBody,
           type: 'application/octet-stream',
+          clientHints: clientHints,
         });
         res.send(responseBody);
       }
@@ -969,6 +975,7 @@ const integrityService = function integrityService({ mode = 'debug', entryPageUR
               salt: this.locals.CurrentSession.server_write_salt,
               statusCode: statusCode,
               body: this.locals.data,
+              clientHints: clientHints,
             });
             this.locals.headergenerated = Date.now();
             /*
@@ -982,6 +989,7 @@ const integrityService = function integrityService({ mode = 'debug', entryPageUR
               salt: this.locals.CurrentSession.server_write_salt,
               statusCode: statusCode,
               body: null,
+              clientHints: clientHints,
             });
           }
           //console.log('integrityService.js: calling res.writeHead', statusCode, reason, obj, this.getHeaders(), this.locals.url);
t2ym added a commit that referenced this issue Sep 3, 2020
t2ym added a commit that referenced this issue Sep 3, 2020
t2ym added a commit that referenced this issue Sep 4, 2020
… from browser hash generation to avoid unexpected randomness
t2ym added a commit that referenced this issue Sep 4, 2020
…igator.userAgentData from browser hash generation to avoid unexpected randomness
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant