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

[integrity.js][WIP] Optional double encryption for integrity #310

Open
3 of 5 tasks
t2ym opened this issue Aug 22, 2019 · 2 comments
Open
3 of 5 tasks

[integrity.js][WIP] Optional double encryption for integrity #310

t2ym opened this issue Aug 22, 2019 · 2 comments

Comments

@t2ym
Copy link
Owner

t2ym commented Aug 22, 2019

Prototyping optional double encryption for integrity

Design Principles

  • Following the concepts of the common/standard technologies as much as possible in order to achieve security on the proven (de facto) standards
  • Simplifying the data formats and the protocol negotiation by hard-coding security parameters into the source code
    • ASN.1 formats are excessive for hard-coded security parameters
      • For example, if security parameters are fixed as ECDHE-ECDSA-AES256-GCM-SHA384, most of the payloads in ASN.1 become fixed values and can be omitted on handshaking
    • Such parameters can be updated by just changing the sources synchronously at the server and the client on upgrading the target site
  • Covering the vulnerabilities of HTML5/JavaScript/HTTPS/TLS technologies
    • This is the key to achieve integrity
    • Unfortunately, PKI does NOT provide any feasible mechanisms to overcome the vulnerabilities coming from the very fundamental architectures
      • Vulnerabilities
        • Compromised clients are out of scope
        • Root CAs can be easily compromised at clients, i.e., CERTIFICATES CERTIFY NOTHING
        • TLS can be decrypted and the contents can be altered arbitrarily by MITM attacks
          • Even client certificates can be attacked by MITM proxies connecting with the certificates
          • Enterprise networks can prohibit exporting of client certificates via Windows domain policies but cannot avoid compromised browsers from using them unless all the valid applications are strictly whitelisted
        • SSLKEYLOGFILE and Wireshark can decrypt SSL/TLS
    • Switching to an alternative approach is mandatory

Current Status

  • 0.4.0-alpha.1
    • Request/Response integrity
    • Double encryption of Request/Response body data
    • Skeleton of Validation Service and Validation Console (always "validated")
      • No agents
    • I hope you read Dear Users on community feedbacks #232.
  • Implementing as integrity.js thin-hook plugin and integrityService.js express middleware; validationService.js for ClientIntegrity.browserHash validation
    • Implementation as nginx module is in consideration
    • Dependent on Node.js 11.x or later for new methods in crypto library
    • Incompatible with Android Chrome due to the different behaviors of only-if-cached fetching
    • Constraints of express middleware
      • body-parser can process a request body only once per request
        • How can other body-parsers process decrypted body by integrityService.js middleware? Any tricks?
          • http-proxy-middleware needs a patch for proxyReq events (working for /errorReport.json POST requests)
          • express.static and expressStaticGzip work well (non-POST, no request body)
          • postHtml.js needs a trick to parse the decrypted req.body in application/x-www-form-urlencoded format for itself like this
            • decodeURIComponent(new URL('https://localhost/?' + req.body.toString()).searchParams.get('type'))
  • RSA-OAEP-SHA256, which is missing in Node.js crypto and slow in node-forge, is using openssl in C++ via node-addon-api for more than 40 times acceleration from node-forge
    • The node addon package does not compile on macOS probably due to incompatible C++ compiler configuration in my environment
      • Fixed by copying config from Configure for Xcode nodejs/node-gyp#1293
      • Suppress build errors by updating some packages
        • Note: The build time for macOS with Core i5-4308U is almost the same as that for Linux with Xeon E3-1220v2.
    • The node addon function fails at RSA decryption on WSL Ubuntu 18.04 while the compilation passes without errors
      • make, gcc, g++, openssl versions seem to be valid
  • Connection and session update are working with primitive error handling
    • Protocol design is in progress
      • "Connect" and "Update" request records, "Accept" response record at /integrity
      • 403 Forbidden 307 Redirect to about:blank on errors
        • Undecyptable responses indicate that the server has been upgraded
        • Error responses are converted to Response.error() to avoid vulnerabilities in iframe
      • whitelist.json to specify the list of URL paths that can be retrieved without checking integrity at the server
        • including the entry page and the mandatory no-hook scripts
      • blacklist.json to specify the list of URL paths that cannot be retrieved
        • including index.html for the entry page
      • errors
        • non-whitelisted
        • blacklisted
        • inexistent URL
        • invalid headers
          • out of range timestamp (lifetime: 10 minutes)
          • invalid method, protocol, authority, path
          • invalid digest for POST body
          • invalid session ID
          • invalid x-integrity HMAC for headers
          • TBD
        • invalid (undecryptable) POST body
        • errors in upstream (TODO: must be converted to 307 redirect)
    • upgrade event is fired on error responses so that the app can suspend for upgrading
      • upgrade events must be dispatched to all the tabs of the app
      • Is the similar suspend process required when the server is unresponsive and the session has expired?
    // Custom event handler for 'upgrade' event
    //  - immediate dispatching of 'upgrade-notified' event is expected
    //  - dispatching of 'upgrade-ready' event is expected if 'upgrade-notified' event is dispatched
    //  - 'upgrade-ready' event must have its detail object with upgradeURL string { upgradeURL: upgradeURL }
    //  - if the 'upgrade' handler is missing, the integrity plugin just reloads the current location after upgrading
    window.addEventListener('upgrade', function onUpgrade(event) {
      window.removeEventListener('upgrade', onUpgrade);
      window.dispatchEvent(new CustomEvent('upgrade-notified', {})); // immediate response to notify pre-upgrading processes are in progress
      preUpgradeProcess();
    });
    const preUpgradeProcess = async function() {
      // Notes:
      //  - Perform some suspending tasks here for the app before upgrading as follows
      //    - suspending UI operations
      //    - notify user for trying to upgrade
      //    - saving session information
      //    - etc.
      const div = document.createElement('div');
      div.setAttribute('style', 'z-index: 999999; position: fixed; width: 100%; height: 100%; background-color: rgba( 128, 128, 128, 0.50 ); opacity: 50%;');
      div.setAttribute('id', 'upgrade-mask');
      document.body.appendChild(div);

      // Generate upgradeURL string for the app to load
      // Notes:
      //  - Interpretation of the upgradeURL is fully up to the app itself
      //  - upgradeURL must be a valid entry page URL string
      const currentLocation = new URL(location.href);
      const upgradeURL = new URL(currentLocation.pathname + '?upgrade=true' + currentLocation.hash, currentLocation).href; // example upgradeURL string

      setTimeout(() => {
        // notify the integrity plugin of the ready state
        window.dispatchEvent(new CustomEvent('upgrade-ready', { detail: { upgradeURL: upgradeURL } }));
      }, 1000);
    }
  • Performance summary as HTTP/1.1 server as a back end of nginx
    • Server: Fedora 29 with 4 logical CPUs on OpenStack KVM on Xeon E3-1220v2 (Passmark Score: 6719) with 4 Node.js workers
    • Client: h2load on Mac mini 2014 (Core i5-4308U)
    • About 6,800 req/sec for small contents with session caching (bypassing nginx reverse proxy)
      • (As compared with about 10,000 req/sec for plain app.all('/*', express.static(dir)) with 4 workers)
    • About 3,400 req/sec for small contents with session caching (with http2 nginx reverse proxy on the same VM)
      • (As compared with about 4,300 req/sec for plain app.all('/*', express.static(dir)) with 4 workers as a back end for http2 nginx reverse proxy)
    • Wirespeed (about 1Gbps) for large contents (> 100Kb)
    • Throughputs for small contents
client edge workers backend workers TLS body encrypted req/sec
h2load - - express (http/1.1) 4 no yes 6,800
h2load express (http/1.1 with TLS) 4 - - yes yes 6,300
h2load nginx (http2) 4 express (http/1.1) 4 yes yes 3,400
h2load - - express (http/1.1) 4 no no 10,000
h2load express (http/1.1 with TLS) 4 - - yes no 8,000
h2load nginx (http2) 4 express (http/1.1) 4 yes no 4,300
h2load nginx (http2) 4 - - yes no 16,000
  • Throughputs for Session "Connect" requests (1 req per client) : Some features are still missing in the current implementation
    • Most heavy operations are RSA_OAEP decryption and ECDHE key generation
client edge workers backend workers TLS body encrypted req/sec
h2load nginx (http2) 4 express (http/1.1) 4 yes yes 1,370
  • Throughputs for Session "Update" requests (1 req per client in every 5 minutes)
    • Most heavy operations are ECDHE key generation and HKDF key derivation
client edge workers backend workers TLS body encrypted req/sec
h2load nginx (http2) 4 express (http/1.1) 4 yes yes 1,960
  • Logs for h2load
$ h2load -p http/1.1 -c 128 -n 100000 --header="user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3887.5 Safari/537.36" --header="x-authority: www.local162.org" --header="x-integrity: user-agent,x-method,x-scheme,x-authority,x-path,x-session-id,x-timestamp;hmac-sha256-DGUcHj0+LXzQhhLZMp0cJ8NPN5SbWmgHQihqxfvusoU=" --header="x-method: GET" --header="x-path: /components/thin-hook/hook.js" --header="x-scheme: https" --header="x-session-id: OLw2l0Fvy+3WiT2kHcIx2zPO0wxQ4AZWdJuK2Efx+IvP4xanYFFSkbf7xf7F8sz9UUQ8AYcfooJHdD0gI19EzP/FRdCW4q/jN3CW5q1X5uBwN8gm" --header="x-timestamp: 1566623283359" http://www.local162.org:8080/components/thin-hook/hook.js
starting benchmark...
spawning thread #0: 128 total client(s). 100000 total requests
Application protocol: http/1.1

finished in 14.68s, 6811.87 req/s, 12.47MB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 183.11MB (192000000) total, 65.04MB (68200000) headers (space savings 0.00%), 109.39MB (114700000) data
                     min         max         mean         sd        +/- sd
time for request:      839us    145.81ms     17.06ms      5.51ms    83.89%
time for connect:      309us      6.56ms      3.30ms      1.82ms    57.03%
time to 1st byte:    12.30ms    146.13ms     62.70ms     38.30ms    58.59%
req/s           :      53.22       64.32       58.88        4.07    47.66%
$ h2load -p h2c -c 128 -n 100000 --header="user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3887.5 Safari/537.36" --header="x-authority: www.local162.org" --header="x-integrity: user-agent,x-method,x-scheme,x-authority,x-path,x-session-id,x-timestamp;hmac-sha256-Jl+S54vaaQgAZMEoJEPcaLv7+nO57/LmrCBBo9ivj3s=" --header="x-method: GET" --header="x-path: /components/thin-hook/hook.js" --header="x-scheme: https" --header="x-session-id: OLwR8gcPdPvG6Od5T4U2ZkI9qMzxrx9Lm5Lhulq+A7BX2pu/4ZFH1uT3m7RfjygAR2pZgrjEDhkcqCPQ35jo8bIJmNmJ5BVAwJLPSJ9OfdQ+D75k" --header="x-timestamp: 1566630294306" https://www.local162.org/components/thin-hook/hook.js
starting benchmark...
spawning thread #0: 128 total client(s). 100000 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-ECDSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-256 256 bits
Application protocol: h2

finished in 29.35s, 3407.05 req/s, 5.59MB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 163.98MB (171946491) total, 52.87MB (55440219) headers (space savings 22.24%), 109.39MB (114700000) data
                     min         max         mean         sd        +/- sd
time for request:     2.16ms     87.38ms     37.34ms      4.85ms    85.79%
time for connect:    43.69ms    104.43ms     88.97ms     17.80ms    88.28%
time to 1st byte:    88.51ms    162.95ms    118.36ms     18.36ms    71.09%
req/s           :      26.61       26.94       26.69        0.07    68.75%
$ h2load -p h2c -c 128 -n 100000 --header="user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3887.5 Safari/537.36" --header="x-authority: www.local162.org:8080" --header="x-integrity: user-agent,x-method,x-scheme,x-authority,x-path,x-session-id,x-timestamp;hmac-sha256-fVH8wBMCZIE2pispvgao0SxDFmlYxIJ6hlMNFsOteaQ=" --header="x-method: GET" --header="x-path: /components/thin-hook/hook.js" --header="x-scheme: https" --header="x-session-id: OLwf2BlsMPXThi0BU5WIwdT7T+6DuRZZcVG39ZOmtM0HSHwm0c8vFmqZx/sRPiDO3r/JzqUPmigMV5Z3AilALxJlVTyynjYTv6nx7vbynGqIA1Wt" --header="x-timestamp: 1566633831651" https://www.local162.org:8080/components/thin-hook/hook.js
starting benchmark...
spawning thread #0: 128 total client(s). 100000 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-256 256 bits
Application protocol: http/1.1

finished in 15.83s, 6315.84 req/s, 11.90MB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 188.35MB (197500000) total, 69.90MB (73300000) headers (space savings 0.00%), 109.39MB (114700000) data
                     min         max         mean         sd        +/- sd
time for request:     1.13ms     59.45ms     18.07ms      5.39ms    76.71%
time for connect:    15.22ms    199.07ms     90.68ms     50.48ms    64.06%
time to 1st byte:    37.35ms    228.36ms    113.94ms     53.82ms    57.03%
req/s           :      49.34       62.85       55.36        4.69    50.00%
$ h2load -p h2c -c 128 -n 100000 --data=connect.dat --header="user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3887.5 Safari/537.36" --header="content-type: application/octet-stream" --header="x-authority: www.local162.org" --header="x-digest: sha256-B7ZiYxZ/8b1AVmwc6A3b5/ILWMPWPPGok7ejOF723+M=" --header="x-integrity: content-type,x-method,x-scheme,x-authority,x-path,x-timestamp,x-digest;hmac-sha256-GBN3W8TC+IEWd2YeRibAxh/bobI4hJBWR6XNRdKsGt4=" --header="x-method: POST" --header="x-path: /components/thin-hook/demo/integrity" --header="x-scheme: https" --header="x-timestamp: 1566640740235" https://www.local162.org/components/thin-hook/demo/integrity
starting benchmark...
spawning thread #0: 128 total client(s). 100000 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-ECDSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-256 256 bits
Application protocol: h2

finished in 72.97s, 1370.37 req/s, 1005.41KB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 71.65MB (75128214) total, 42.17MB (44221942) headers (space savings 22.96%), 26.51MB (27800000) data
                     min         max         mean         sd        +/- sd
time for request:     1.31ms    329.03ms     92.80ms     55.20ms    52.95%
time for connect:    43.51ms    103.83ms     87.23ms     15.74ms    90.63%
time to 1st byte:    85.25ms    239.88ms    157.66ms     44.60ms    57.03%
req/s           :      10.70       10.92       10.76        0.08    71.88%
$ h2load -p h2c -c 128 -n 100000 --data=update.dat --header="user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3887.5 Safari/537.36" --header="content-type: application/octet-stream" --header="x-authority: www.local162.org" --header="x-digest: sha256-4AkxV17sPXbLfAFDminMqxpGgFj+2TpP0MPgG89Xi6k=" --header="x-integrity: content-type,x-method,x-scheme,x-authority,x-path,x-session-id,x-timestamp,x-digest;hmac-sha256-KL6DH+E+hCp//6Dh0N+aXSXCWvQY7zj51L0waFawDQc=" --header="x-method: POST" --header="x-path: /components/thin-hook/demo/integrity" --header="x-scheme: https" --header="x-session-id: bb4MgLkT21iXLR143nIAVUOXJW8dtYo3xkhRo0NIYeqD/DLd7AKLq0zQlh3StQOZKeBoTu4uYPEo70WsqgadU0E4wrV6XWJ7VMsQjOcrlIBXW1BB" --header="x-timestamp: 1566641932893" https://www.local162.org/components/thin-hook/demo/integrity
starting benchmark...
spawning thread #0: 128 total client(s). 100000 total requests
TLS Protocol: TLSv1.2
Cipher: ECDHE-ECDSA-AES128-GCM-SHA256
Server Temp Key: ECDH P-256 256 bits
Application protocol: h2

finished in 50.90s, 1964.57 req/s, 1.41MB/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 71.64MB (75116963) total, 42.16MB (44210691) headers (space savings 22.98%), 26.51MB (27800000) data
                     min         max         mean         sd        +/- sd
time for request:     3.01ms    246.65ms     64.92ms     14.27ms    89.51%
time for connect:    55.33ms    112.11ms    101.14ms     15.84ms    89.84%
time to 1st byte:   104.41ms    193.28ms    145.37ms     27.14ms    60.16%
req/s           :      15.34       15.43       15.37        0.02    68.75%

Encrypted Response

Timeline

Request Headers

Referer: https://www.local162.org/components/thin-hook/hook.min.js?version=651&no-hook-authorization=7ab8ff3ba15c81c08f77a5488c06cb15fab49ac44c179f41335b3180c4643cf9,a578e741369d927f693fedc88c75b1a90f1a79465e2bb9774a3f68ffc6e011e6,log-no-hook-authorization&sw-root=/&no-hook=true&hook-name=__hook__&context-generator-name=method&discard-hook-errors=false&fallback-page=index-fb.html&hook-property=true&hook-global=true&hook-prefix=_uNpREdiC4aB1e_&compact=true&service-worker-initiator=/components/thin-hook/demo/
Sec-Fetch-Mode: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
x-authority: www.local162.org
x-integrity: user-agent,x-method,x-scheme,x-authority,x-path,x-session-id,x-timestamp;hmac-sha256-9BfCPIS5MTZ4Q9TNiHaMlBkPfGbQ57xcoPc5hY+Azc8=
x-method: GET
x-path: /components/thin-hook/demo/locales/bundle.ja.json
x-scheme: https
x-session-id: OIKzf0kCdo63lfEzBeazXvaF3tnKEfc16LVpMCevYvyOiDkV9igpJJRfrU2+bKwdJtOz9qqDottauX9ncCxEEqlnEwAKfm1TiAnTT0EcQq7/ii0D
x-timestamp: 1566458873475

Response Headers

accept-ranges: bytes
cache-control: no-cache
content-length: 984
content-type: application/json; charset=UTF-8
date: Thu, 22 Aug 2019 07:27:53 GMT
etag: W/"3c8-16ca95a3ac3"
last-modified: Mon, 19 Aug 2019 10:09:18 GMT
server: nginx/1.14.2
service-worker-allowed: /
status: 200
vary: Accept-Encoding
x-authority: www.local162.org
x-content-encoding: gzip+aes-256-gcm
x-digest: sha256-twh7JZz83nfzzIYqFWiKAXQkiXD1uyou8IsTVr1IuvM=
x-integrity: x-powered-by,vary,content-type,last-modified,etag,x-content-encoding,x-status,x-scheme,x-authority,x-path,x-request-timestamp,x-timestamp,x-digest;hmac-sha256-P1cAGbcnq3rpftjhNmvb1EvSnLIOz1P6gwCGZQ2H5ZI=
x-path: /components/thin-hook/demo/locales/bundle.ja.json
x-powered-by: Express
x-request-timestamp: 1566458873475
x-scheme: https
x-status: 200
x-timestamp: 1566458873492
@t2ym
Copy link
Owner Author

t2ym commented Aug 25, 2019

Protocol Design (Tentative) in Pseudo-Code

Design Issues

  • Encrypted heading 4 bytes (6 chars in Base64) of SessionID change regularly according to session_timestamp as the key and the iv for encryption are fixed for each version. Can this be a potential vulnerability? Should this be obfuscated as in TLS 1.3 session lifetime?
  • Current implementation does not have any "Reject" response for invalid "Connect" and "Update" requests but just responds with a fake "Accept" response starting with 0x02(RecordType.Accept) followed by random bytes. Is this feasible?
  • How can selective encryption be implemented?
    • For enterprise proxies to detect undesirable traffic while preserving security for the code
      • Encrypt all the code (HTML, JavaScript, CSS, etc.)
      • Do not encrypt the user data (POST data, GET contents) but their integrity is verified
      • Can a custom header such as x-auditing-proxy: contents be inserted at the proxy to request the server and the client to decrypt contents selectively? If so, can an auditing notification message be shown so that the user can understand their contents are open to the auditing proxy?
  • Validation of ClientIntegrity.browserHash is going to be handled in a separate component.

  • dummy_item_for_indentation
    • Key Concepts
      • Released versions of browsers are Pre-shared Secrets for authentication
        • Evergreen browsers are evolving so fast that their window object structures can be their unique fingerprints
          • Each version of Chrome, Chrome Beta, Chrome Canary, Chromium can be identified
            • How to handle released Chrome browsers with experimental features enabled?
              • Treat them as compromised browsers and block them
        • Accesses from validated browsers by agents/manual validation URLs are stored at validationService.js repository
      • Identify browsers with ClientIntegrity.browserHash associated with user-agent
        • browserHash must be kept secret, unpredictable, but deterministic
          • browserHash varies on
            • browser versions
            • application versions
            • integrity.js source code including RSA, ECDSA public keys in base64
            • other deterministic entropy such as new Error().stack
            • TBD
          • Device/User-specific private configurations are excluded as noises
            • Issue: inconsistent browserHash with URL search parameters
              • Root Cause: document.location is missing in the excluded volatile object list
              • Fixed at 0.4.0-alpha.49
            • Currently excluded properties in integrity.js
      /* volatileXXX patterns for excluding device/user-specific data as noises for the browser hash */
      // device/user-specific number data to be excluded as noises
      const volatileNumbers = [
        '.document:object.timeline:object.currentTime:number', // current time
        '.innerWidth:number', // window size
        '.innerHeight:number', // window size
        '.screenX:number', // screen size
        '.screenY:number', // screen size
        '.outerWidth:number', // window size
        '.outerHeight:number', // window size
        '.screenLeft:number', // screen size
        '.screenTop:number', // screen size
        '.performance:object.timeOrigin:number', // current time
        '.visualViewport:object.width:number', // window size
        '.visualViewport:object.height:number', // window size
        '.devicePixelRatio:number', // screen device
        '.navigator:object.connection:object.downlink:number', // network connection
        '.navigator:object.connection:object.downlinkMax:number', // network connection
        '.navigator:object.connection:object.rtt:number', // network connection
        '.navigator:object.maxTouchPoints:number', // touch screen device feature
        '.navigator:object.hardwareConcurrency:number', // CPU device information
        '.navigator:object.deviceMemory:number', // Memory device information
        '.history:object.length:number', // history
      ];
      const volatileNumbersSet = new Set();
      volatileNumbers.forEach(pos => volatileNumbersSet.add(pos));
      // patterns for device/user-specific number data to be excluded as noises
      const volatileNumbersRegExp = new RegExp(
        '^((\.performance:object\.timing:object|' + // current time in performance object
        '\.performance:object\.memory:object|' + // memory information in performance object
        '\.console:object\.memory:object|' + // memory information in console API
        '\.navigator:object\.connection:object|' + // network connection information in navigator object
        '\.screen:object).*|' + // screen device information
        '.*(:object\.offsetWidth:number|' + // window/frame size
        ':object\.scrollWidth:number|' + // window/frame size
        ':object\.scrollHeight:number|' + // window/frame size
        ':object\.clientWidth:number|' + // window/frame size
        ':object\.clientHeight:number))$'); // window/frame size
      // device/user-specific string data to be excluded as noises
      const volatileStrings = [
        '.navigator:object.connection:object.type:string', // network connection type
        '.navigator:object.connection:object.effectiveType:string', // network connection type
        '.navigator:object.language:string', // language preference
        '.navigator:object.userAgent:string', // user agent, which is sent separately as userAgentHash
        '.navigator:object.appVersion:string', // app version, which can be detectable from user agent
        '.navigator:object.platform:string', // platform OS, which can be detectable from user agent
        '.document:object.referrer:string', // navigation history
        '.document:object.lastModified:string', // navigation timing
        '.document:object.URL:string', // document URL
        '.document:object.documentURI:string', // document URI
        '.document:object.visibilityState:string', // document visiblitity state
        '.document:object.webkitVisibilityState:string', // document visiblitity state
      ];
      const volatileStringsSet = new Set();
      volatileStrings.forEach(pos => volatileStringsSet.add(pos));
      const volatileStringsRegExp = new RegExp(
        '^.*(\.baseURI:string)$'); // baseURI
      // device/user-specific object data to be excluded as noises
      const volatileObjectsRegExp = new RegExp(
        '^(\.navigator:object\.languages:object|' + // language preferences
        '\.navigator:object\.serviceWorker:object\.controller:object|' + // Service Worker status
        '\.performance:object\.navigation:object|' + // navigation history
        '\.screen:object\.orientation:object|' + // screen device
        '\.location:object|' + // location object
        '\.document:object\.location:object|' + // document.location object
        '\.localStorage:object|' + // browser storage
        '\.sessionStorage:object).*$'); // browser storage
      const volatileBooleans = [
        '.navigator:object.connection:object.saveData:boolean', // a reduced data usage option
        '.document:object.hidden:boolean', // hidden status of the document
        '.document:object.webkitHidden:boolean', // hidden status of the document
      ];
  • dummy_item_for_indentation
    • Validation Protocol
      • /update - Update in every second, merge at validationService.js, and respond replica
        • POST data: dictionary of UA:hash -> count, etc. since the last /update
        • POST response: dictionary of UA:hash -> status, count, etc.
      • Repository at validationService.js Server
        • UA:hex(hash) -> status, count, etc.
      • Repository Replica at validationService.js client API for integrityService.js
        • UA:hex(hash) -> status, count, etc.
        • Log: Dictionary of UA:hex(hash) -> count, etc. since the last /update
      • Validation at validationService.js client API for integrityService.js
        • RepoReplica[UA:hex(hash)].status === "validated"
        • Increment Log[UA:hex(hash)].count, etc.
      • Validation URL
        • (tentative) entryURL/?validate=base64URL(AES_GCM(action=validate&timestamp=X...))
      • Version upgrade processes
        • TBD
    • Components
      • validationService.js
        • Skeleton
          • Client certificate authentication
          • Dummy /update API
      • Agent validationAgent.js
        • Key Concepts
          • Automatically launch target browsers for validation like Selenium grid
          • Automatically control latest and 2nd latest browser versions
            • Automation of snapshotting VMs and stopping browser auto-update services might be tricky
          • Note: browserHash changes via puppeteer manipulation
        • Platforms
          • Windows (via Node.js on WSL or git bash?)
          • macOS
          • Linux
          • Chrome OS?
            • Access via Validation Console and open a validation URL in a new tab?
        • Features
          • Launch validation URL
            • PoC Launch and terminate chrome.exe with a target URL from Node.js
          • Control Chrome Update Service
            • Deploy n + 1 agents to control the last n (presumably 2) release versions of Chrome browsers
              • If n is 2, 3 agents (Agent0, Agent1, Agent2) will rotate their roles on each update
                • Agent0 runs the 2nd latest Chrome; Update Service is suspended
                • Agent1 runs the latest Chrome; Update Service is suspended
                • Agent2 runs the latest Chrome; Update Service is running
                  • When Chrome on Agent2 is updated, Update Service is suspended on Agent2
                    • Update Service on Agent0 is resumed and Chrome on Agent0 is updated to the latest version (soon), which is the same version as that on Agent2
                    • The 3rd latest Chrome might be invalidated later at an appropriate timing
                    • Status after the Chrome update until the next update
                    • Agent0 runs the latest Chrome; Update Service is running
                    • Agent1 runs the 2nd latest Chrome; Update Service is suspended
                    • Agent2 runs the latest Chrome; Update Service is suspended
            • On Windows
              • Tasks
              • schtasks.exe /CHANGE /DISABLE /TN GoogleUpdateTaskMachineCore
              • schtasks.exe /CHANGE /DISABLE /TN GoogleUpdateTaskMachineUA
              • schtasks.exe /CHANGE /ENABLE /TN GoogleUpdateTaskMachineCore
              • schtasks.exe /CHANGE /ENABLE /TN GoogleUpdateTaskMachineUA
              • Services
                • Manual startup for "Google Update Service (gupdate)" service
      • Validation Console
        • Skeleton
          • Scaffolded via npm init @open-wc
          • Accessible via validationService.js with client certificate authentication
        • Browser List
          • In design
        • Manual Validation
          • In design
        • Configuration
          • In design
  • Fetching of cache-bundle.json should be skipped when that for the same version has been loaded
    • The current implementation unregisters the existing Service Worker before sending a Connect request to avoid too complicated situations with Connect requests proxied via the Service Worker
  • Multiple tabs try to upgrade the app and send Connect requests at once, which should be avoided and serialized with a single Connect request
    • How to serialize the connections?
  • Dynamic HTML responses for iframe elements are locally cached in spite of cache-control: no-cache response headers for all encrypted responses including non-HTML responses.
    • Edge servers like nginx can avoid unexpected caching by the headers
      • cache-control: private does not work as expected in nginx
    • Workaround # 1: Object.defineProperty(cache, 'put', { value: () => Promise.resolve() }) to disable cache.put() for dynamic HTML responses with targeted URLs in checkResponse() at cache-bundle.js
    • What is a desirable mechanism to control local caching of dynamic HTML responses?
      • SPA with a fixed entry page is preferred in thin-hook. Dynamic HTML responses are not very friendly to the architecture.
  • Leaking of secrets from process memory dumps has to be avoided
    • Notes:
      • Chromium browsers with debug symbols must not be treated as validated browsers
      • If Microsoft would provide official debug symbols for Chromium-based Edge browsers, there would be significant risks of leaking secret key data from memory dumps
      • There are no perfect solutions to eliminate raw secret key data from memory dumps completely
    • Mitigate risks of leaking ClientIntegrity by clearing randomizing and deleting ArrayBuffer objects after they are used
      • Note: It is relatively easy to extract ClientIntegrity data objects from a memory dump compared with other secrets
    • Other secrets
      • Are wrapKey() and unwrapKey() feasible?
        • Wrapping keys are still stored in ArrayBuffer objects
        • Are CryptoKey objects stored with any encryption?
        • Wrapped keys must be unwrapped (decrypted) anyway for use, which might impact performance as well
      • TBD

Change Log

  • 2019-08-27: Remove #hash from x-path header according to HTTP/1.1, HTTP/2 RFCs
  • 2019-09-04: Skip fetching and loading cache-bundle.json when it has already been loaded

Pseudo-Code

  const RecordType = {
    Connect: 1, // client -> server
    Accept: 2, // server -> client
    Update: 3, // client -> server
  };

  const key_length = 32; // bytes
  const iv_length = 12; // bytes
  const salt_length = 32; // bytes
  Hash.length = 32; // bytes
  Hash = SHA256;

  const entryPageURL = "https://host.name/entry/page/"; // must be a directory path
  const integrityServiceURL = new URL("integrity", entryPageURL).href; // https://host.name/entry/page/integrity

  // at server
  keys = {
    "version": "version_638",
    "rsa-private-key.pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAk4Rm72FO94F4KWny7JRE8YUgAzy0hrjsg3XTnieteK0VGDID\nJUyndaCA7HvmLbMRLLkTLg4hcQxAet8SAIM/iSbo51JtNu3qkDXADON2KnZf5P1Y\n5THL3jkwFQQjucbytF92C5yqrKL5wBSH2VFnnv/WR2Mk/GE8B3dPD6X1TdQWRQpD\n0wuWzx/R4DCg1zXcwjD2KK65VDZ4BB9dk3HX0SrtAiFhgP+kydfwgkhQDhBO3rIb\n+qAzRXLB2a3KkYKO5DVFaNWt0J0qRkR+ShTsYfhHCqRemO4ZXfd2aV0lIEtJwmdO\nIl19ijVv+XjDeXEpDDHMwcp5pbnNb6YKTnRu9wIDAQABAoIBAAqY5Fwl/WpCXsN6\n3Pyp2hoPmjEhV0amWjdHa6Bc8VVN+cn3LcqsKwuEMD7M18hIqN8xnHMeiMB6RNeO\n1tg6lYHgzbJwdXAQv10Ev3sti/uY7WKh4JT2ctLQAOhBl99sr1rN0MkcxBYKzy5B\nS1ENTAhcEKSoNqv6wDk5FPDm1yx0B3qZhrws6EkRt29yOO7bhqjF4c5GfeuAkuDB\n44OJ6B+klxT4YsUbfSkUfnlTw3IBfM37dlBEbLH7f3dMeK0UVq2L1MRBjlmOzk8c\nriuhAxeBNGGcTNdQo2fzTMGesg/Ixqoc0Odh1BB9HVVkoCiNFTnACY2oXh3rotxs\nL2ZRMaECgYEA7VFczuTVxtpzPUmAKrNGk9WRLuW+eb08W4DYOf5yYLhxLMHishnR\nBO+t4pkuoJ8SgH4jvb/mxEBdV2wjlJs2QGYvX9EYTwdezHzcpwK03XFHLwpKpRD1\nsTw6UnucWb+63ZB0r5NyVkHtaxhtpPU6hAlTKZXHocPKahEypnsSKQsCgYEAnyFM\nYNPpcat8DM3YPmau0l6Q2RaNRjlQw+rEZ8zRlzPf4hiwPENjklYM7u0ceIbdrjc3\nWeThLyktsnyNCijrv7VZgmgyrgFL29VsVrfeR3ZwgHeBLUNgcTwggBmANrA7rmBN\nzmXhLa5xdn8PmjYQDNK7qEy/3WnHU0VFuWKvfUUCgYBVqytfnIf3cuBq3V+hCnqN\n32i7j0AFXmSte4OS2+GaPLrON2eId31W1Nbml/mXDhV1wRNR6jZ53epUJrtpZ+Zb\ntQehBTBLRxPXqbNVrspvrfbOal6r28V1p5I+OFUmqOniFcWppAaAUOhN4tGh3My0\n4VDeEC2ynaUySOcJ5h+WJQKBgA3lX4EZIEqf2f5YP2j7mIqgXW/Hq2CVgrsJFkum\nNCtLCWL6GvG4RMqznv+CTzkrNdKP2dKMzSlMJERw4fQgLK4aDQ35QWu2i0RQN9y+\nw7dj3WEqjmpAdvyMbp4hG/QqoZuRp1m9xdMyZ5AcemVSEUa9ZEvHH/4azaA07WjJ\n+F8tAoGBAJUQs3F9GTm+TU6Ggeq1t03vkma76Mi2SPpFdmi56SIjGUEsz/SgxvE9\n/CeWuBPxj+viJ9Yinx3wLXEYGFUfDz8/XTUsu+h8q6HLNtR9O2WNadme4AEiSli7\njiQ21H6pWu0NM5e1rNoPTOCh7RFmignOchDFFpl0vXSNUPLn6zA7\n-----END RSA PRIVATE KEY-----\n",
    "rsa-public-key.pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk4Rm72FO94F4KWny7JRE\n8YUgAzy0hrjsg3XTnieteK0VGDIDJUyndaCA7HvmLbMRLLkTLg4hcQxAet8SAIM/\niSbo51JtNu3qkDXADON2KnZf5P1Y5THL3jkwFQQjucbytF92C5yqrKL5wBSH2VFn\nnv/WR2Mk/GE8B3dPD6X1TdQWRQpD0wuWzx/R4DCg1zXcwjD2KK65VDZ4BB9dk3HX\n0SrtAiFhgP+kydfwgkhQDhBO3rIb+qAzRXLB2a3KkYKO5DVFaNWt0J0qRkR+ShTs\nYfhHCqRemO4ZXfd2aV0lIEtJwmdOIl19ijVv+XjDeXEpDDHMwcp5pbnNb6YKTnRu\n9wIDAQAB\n-----END PUBLIC KEY-----\n",
    "ecdsa-private-key.pem": "...",
    "ecdsa-public-key.pem": "...",
    "session-id-aes-key": "1b8c0dde7832e78bf9097421179d004af5ed83e34ea0c5c067cafa469a3d2b49", // no forward secrecy
    "session-id-aes-iv": "79706eec5a6c9bf41bf02d1b", // no forward secrecy
  };
  ECDSA.integrityJSONSignature = ECDSA.sign(ECDSA.serverPrivateKey, SHA_256.digest(integrityJSON));

  // at client (main document; encoded)
  RSA.serverPublicKey = spki("rsa-public-key.pem");
  ECDSA.serverPublicKey = spki("ecdsa-public-key.pem");
  Sessions = [];

  // on Connect
  CurrentSession = { isConnect: true, session_timestamp: Date.now() };
  Sessions.push(CurrentSession); // store Connect Session

  //requestId = 0; // TODO: revisit later
  AES_GCM.{ clientOneTimeKey, clientOneTimeIv } = { Random(hashSize), Random(ivSize) };
  NextSession.clientRandom = Random(hashSize);
  NextSession.ECDHE.{ clientPublicKey, clientPrivateKey } = ECDH.generateKeys('prime256v1');
  CurrentSession.ClientIntegrity =
    userAgentHash (= SHA256.digest(navigator.userAgent)) +
    browserHash (= SHA256.digest(JSON.stringify(_traverse(wrapper, window)))) + // TODO: Forward secrecy. timestamp, rotation, etc.
    scriptsHash (= SHA256.digest(document.querySelectorAll('script').join('\0'))) +
    htmlHash (= SHA256.digest(document.querySelector('html').outerHTML))
  }
  CurrentSession.connect_early_secret =
    HKDF-Expand(0, AES_GCM.clientOneTimeKey + AES_GCM.clientOneTimeIv + NextSession.clientRandom + NextSession.ECDHE.clientPublicKey + CurrentSession.ClientIntegrity);
  CurrentSession.connect_salt = HKDF-Expand-Label(connect_early_secret, "salt", "", salt_length)

  // request
  // initial connection
  req.headers = {
    "x-method": "POST",
    "x-scheme": integrityServiceURL.protocol.replace(/:$/, ''),
    "x-authority": integrityServiceURL.host,
    "x-path": integrityServiceURL.pathname + integrityServiceURL.search, // Note: integrityServiceURL.hash is removed
    "content-type": "application/octet-stream",
    //"x-request-id": requestId, // TODO: revisit later
    "x-timestamp": Date.now(),
    "x-digest": "sha256-" + base64(SHA256.digest(req.body)),
    "x-integrity": "x-method,x-scheme,x-authority,x-path,header-list,x-timestamp,x-digest;" +
      "hmac-sha256-" + base64(HMAC_SHA256(CurrentSession.connect_salt, headers.join("\n") + "\n")) // TODO: browserHash is inappropriate as it is shared among client browsers with the same version
  }
  req.body =
    RecordType.Connect [1] +
    RSA_OAEP.encrypt(RSA.serverPublicKey,
      AES_GCM.clientOneTimeKey [32] +
      AES_GCM.clientOneTimeIv [12]) [256] +
    AES_GCM.encrypt(aesKey = AES_GCM.clientOneTimeKey, iv = AES_GCM.clientOneTimeIv,
      NextSession.clientRandom [32] +
      NextSession.ECDHE.clientPublicKey [65] +
      CurrentSession.ClientIntegrity (= userAgentHash + browserHash + scriptsHash + htmlHash) [128] +
    )
  // session update (Service Worker)
  req.headers = {
    "x-method": "POST",
    "x-scheme": integrityServiceURL.protocol.replace(/:$/, ''),
    "x-authority": integrityServiceURL.host,
    "x-path": integrityServiceURL.pathname + integrityServiceURL.search, // Note: integrityServiceURL.hash is removed
    "content-type": "application/octet-stream",
    "x-session-id": base64(CurrentSession.SessionID),
    //"x-request-id": requestId, // TODO: revisit later
    "x-timestamp": Date.now(),
    "x-digest": "sha256-" + base64(SHA256.digest(req.body)),
    "x-integrity": "x-method,x-scheme,x-authority,x-path,header-list,x-session-id,x-timestamp,x-digest;" +
      "hmac-sha256-" + base64(HMAC_SHA256(CurrentSession.client_write_salt, headers.join("\n") + "\n"))
  }
  req.body =
    RecordType.Update [1] +
    AES_GCM.encrypt(aesKey = CurrentSession.client_write_key, iv = CurrentSession.client_write_iv,
      AES_GCM.clientOneTimeKey [32] +
      AES_GCM.clientOneTimeIv [12] +
      NextSession.clientRandom [32] +
      NextSession.ECDHE.clientPublicKey [65]
    )
  // proxied request (Service Worker)
  req.headers = {
    "x-method": req.method,
    "x-scheme": req.url.protocol.replace(/:$/, ''),
    "x-authority": req.url.host,
    "x-path": req.url.pathname + req.url.search, // Note: req.url.hash is removed
    "x-session-id": base64(CurrentSession.SessionID),
    //"x-request-id": requestId, // TODO: revisit later
    "x-timestamp": Date.now(),
    "x-digest": "sha256-" + base64(SHA256.digest(req.body)), // on POST/PUT
    "x-content-encoding": "aes-256-gcm", // on POST/PUT; no gzip support for request body encryption
    "x-integrity": "x-method,x-scheme,x-authority,x-path,header-list,x-session-id,x-timestamp,x-digest,x-content-encoding;" +
      "hmac-sha256-" + base64(HMAC_SHA256(CurrentSession.client_write_salt, headers.join("\n") + "\n"))
  }
  req.body =
    AES_GCM.encrypt(aesKey = CurrentSession.client_write_key, iv = CurrentSession.client_write_iv,
      Body = event.request.body // Note: Body is authenticated by x-digest and x-integrity headers
    )
  // response
  res.headers = {
    "x-scheme": req.url.protocol.replace(/:$/, ''),
    "x-authority": req.url.host,
    "x-path": req.url.pathname + req.url.search, // Note: req.url.hash is removed
    //"x-request-id": requestId, // TODO: revisit later
    "x-request-timestamp": req.headers["x-timestamp"],
    "x-timestamp": Date.now(),
    "x-digest": "sha256-" + base64(SHA256.digest(res.body)),
    "x-content-encoding": "aes-256-gcm"/"gzip+aes-256-gcm"
    "x-integrity": "x-scheme,x-authority,x-path,header-list,x-request-timestamp,x-timestamp,x-digest;" +
      "hmac-sha256-" + base64(HMAC_SHA256(CurrentSession.server_write_salt, headers.join("\n") + "\n"))
  }
  res.body = 
    AES_GCM.encrypt(CurrentSession.server_write_key, CurrentSession.server_write_iv,
      // no explicit field for body length
      Body = identity/gzip(response.body)
    )

  // at server
  encrypted = req.body;
  CurrentSession = {};

  // Validate headers
  req.headers['content-type'] === 'application/octet-stream';
  req.headers['x-method'] === req.method === 'POST';
  req.headers['x-scheme'] === req.url.protocol.replace(/:$/, ''); // TODO: validate req.url
  req.headers['x-authority'] === req.url.host;
  req.headers['x-path'] === req.url.pathname + req.url.search; // Note: req.url.hash is removed
  req.headers['x-timestamp'] within acceptable_timestamp_range // validate clock of the client
  req.headers['x-digest'] === 'sha256-' + base64(SHA256.digest(req.body));

  if (RecordType.type === RecordType.Connect) {
    // Validate Connect
  
    // decrypt Connect
    RecordType.type = encrypted.subarray(0, 1) === RecordType.Connect
    [ AES_GCM.clientOneTimeKey, AES_GCM.clientOneTimeIv ] = RSA_OAEP.decrypt(RSA.serverPrivateKey, encrypted.subarray(1, 1 + keySize));
    [ NextSession.clientRandom, NextSession.ECDHE.clientPublicKey, CurrentSession.ClientIntegrity ] =
      AES_GCM.decrypt(AES_GCM.clientOneTimeKey, AES_GCM.clientOneTimeIv, encrypted.subarray(1 + keySize, encrypted.byteLength));

    // save at build time; verify at runtime
    SHA_256.digest(req.header['user-agent']) === userAgentHash;
    keys["scriptsHashHex", "htmlHashHex"] (=:build, ===:runtime) [toHex(scriptsHash), toHex(htmlHash)];
    // Validate browserHash at integrityService
    browserHash === verifiedBrowserHash(req.header['user-agent']) // Note: This is the key to the authentication in the handshake
    // Derive connect_early_secret 
    CurrentSession.connect_early_secret =
      HKDF-Extract(0, AES_GCM.clientOneTimeKey + AES_GCM.clientOneTimeIv + NextSession.clientRandom + NextSession.ECDHE.clientPublicKey + CurrentSession.ClientIntegrity);
    // Derive Pseudo-PSK for initial key derivation
    CurrentSession.PSK = HKDF-Expand-Label(CurrentSession.connect_early_secret, "connect", "", Hash.length) // pseudo-PSK
  }
  else if (RecordType.type === RecordType.Update && req.header['x-session-id']) {

    // decrypt SessionID
    CurrentSession.SessionID = Buffer.from(req.header['x-session-id'], 'base64');
    CurrentSession.SessionIDPayload = AES_GCM.decrypt(sessionIdAESKey, sessionIdAESIv, SessionID);
    CurrentSession.[ session_timestamp [4], master_secret [32], transcript_hash = Transcript-Hash(Connect/Update + Accept.header) [32] ] = CurrentSession.SessionIDPayload;

    // Validate session_timestamp
    CurrentSession.session_timestamp within expected lifetime

    // Derive current secrets (not for the next updated ones)
    CurrentSession.client_traffic_secret = HKDF-Expand-Label(CurrentSession.master_secret, "c ap traffic", CurrentSession.transcript_hash, Hash.length)
    //CurrentSession.server_traffic_secret = HKDF-Expand-Label(CurrentSession.master_secret, "s ap traffic", CurrentSession.transcript_hash, Hash.length)
    CurrentSession.session_master_secret = HKDF-Expand-Label(CurrentSession.master_secret, "session", CurrentSession.transcript_hash, Hash.length)

    //CurrentSession.server_write_key = HKDF-Expand-Label(CurrentSession.server_traffic_secret, "key", "", key_length);
    //CurrentSession.server_write_iv = HKDF-Expand-Label(CurrentSession.server_traffic_secret, "iv", "", iv_length);
    //CurrentSession.server_write_salt = HKDF-Expand-Label(CurrentSession.server_traffic_secret, "salt", "", salt_length);

    CurrentSession.client_write_key = HKDF-Expand-Label(CurrentSession.client_traffic_secret, "key", "", key_length);
    CurrentSession.client_write_iv = HKDF-Expand-Label(CurrentSession.client_traffic_secret, "iv", "", iv_length);
    CurrentSession.client_write_salt = HKDF-Expand-Label(CurrentSession.client_traffic_secret, "salt", "", salt_length);

    CurrentSession.PSK = HKDF-Expand-Label(CurrentSession.session_master_secret, "update", "", Hash.length)

    // decrypt Update
    RecordType.type = encrypted.subarray(0, 1) === RecordType.Update
    [ AES_GCM.clientOneTimeKey, AES_GCM.clientOneTimeIv, NextSession.clientRandom, NextSession.ECDHE.clientPublicKey ] =
      AES_GCM.decrypt(CurrentSession.client_write_key, CurrentSession.client_write_iv, encrypted.subarray(1, encrypted.byteLength));

  }

  // Validate x-integrity (delayed)
  // parse headers
  [ headerNamesCSV, 'hmac-sha256-' + headerSignatureBase64 ] = req.headers['x-integrity'].split(';')
  headerNames = headersCSV.split(',');
  headerSignature = atob(headerSignatureBase64);
  headers = headerNames.map((headerName) => headerName + ': ' + req.headers[headerName] + '\n').join('');
  // for Connect
  connect_salt = HKDF-Expand-Label(CurrentSession.connect_early_secret, "salt", "", salt_length)
  salt = connect_salt;
  // for Update
  salt = CurrentSession.client_write_salt;
  // verify hmac
  HMAC_SHA256(salt, headers) === headerSignature;

  // Request has been validated

  // prepare the next session
  NextSession = {};
  NextSession.serverRandom = Random(hashSize);
  NextSession.ECDHE.{ serverPublicKey, serverPrivateKey } = ECDH.generateKeys('prime256v1');
  NextSession.ECDHE.sharedKey = ECDH.deriveKey(NextSession.ECDHE.serverPrivateKey, NextSession.ECDHE.clientPublicKey);

  RFC5869

    HKDF-Extract(salt, IKM) = HMAC-Hash(salt, IKM)

      Inputs:
        salt     optional salt value (a non-secret random value); if not provided, it is set to a string of HashLen zeros.
        IKM      input keying material

    HKDF-Expand(PRK, info, L) -> OKM

      Options:
        Hash     a hash function; HashLen denotes the length of the hash function output in octets

      Inputs:
        PRK      a pseudorandom key of at least HashLen octets (usually, the output from the extract step)
        info     optional context and application specific information (can be a zero-length string)
        L        length of output keying material in octets (<= 255*HashLen)

      Output:
        OKM      output keying material (of L octets)

      The output OKM is calculated as follows:

      N = ceil(L/HashLen)
      T = T(1) | T(2) | T(3) | ... | T(N)
      OKM = first L octets of T

      where:
      T(0) = empty string (zero length)
      T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
      T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
      T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
      ...

  RFC8446

    Transcript-Hash(M1, M2, ... Mn) = Hash(M1 || M2 || ... || Mn)

    HKDF-Expand-Label(Secret, Label, Context, Length) =
      HKDF-Expand(Secret, HkdfLabel, Length)

    Where HkdfLabel is specified as:

    struct {
      uint16 length = Length;
      opaque label<7..255> = "tls13 " + Label;
      opaque context<0..255> = Context;
    } HkdfLabel;

    Derive-Secret(Secret, Label, Messages) =
      HKDF-Expand-Label(Secret, Label,
                        Transcript-Hash(Messages), Hash.length)

    Customized Key Schedule:

               0
               |
               v
     PSK ->   HKDF-Extract = Early Secret
               |
               v
         Derive-Secret(., "derived", "")
               |
               v
     ECDHE -> HKDF-Extract = Handshake Secret
               |
               v
         Derive-Secret(., "derived", "")
               |
               v
     0 ->     HKDF-Extract = Master Secret
               |
               +-----> Derive-Secret(., "c ap traffic",
               |                     Connect/Update + Accept.header)
               |                     = client_traffic_secret
               |
               +-----> Derive-Secret(., "s ap traffic",
               |                     Connect/Update + Accept.header)
               |                     = server_traffic_secret
               |
               +-----> Derive-Secret(., "session",
                                     Connect/Update + Accept.header)
                                     = session_master_secret

    [sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
    [sender]_write_iv  = HKDF-Expand-Label(Secret, "iv", "", iv_length)
    [sender]_write_salt = HKDF-Expand-Label(Secret, "salt", "", salt_length)

  Accept.header =
    RecordType.Accept [1] +
    AES_GCM.encrypt(AES_GCM.clientOneTimeKey, AES_GCM.clientOneTimeIv,
      NextSession.serverRandom [32] +
      NextSession.ECDHE.serverPublicKey [32]) [80]

  // Derive next secrets
  NextSession.early_secret = HKDF-Extract(0, CurrentSession.PSK);
  NextSession.handshake_secret = HKDF-Extract(Derive-Secret(NextSession.early_secret, "derived", ""), NextSession.ECDHE.sharedKey);
  NextSession.master_secret = HKDF-Extract(Derive-Secret(NextSession.handshake_secret, "derived", ""), 0);

  NextSession.transcript_hash = Transcript-Hash(Connect/Update + Accept.header);

  //NextSession.client_traffic_secret = HKDF-Expand-Label(NextSession.master_secret, "c ap traffic", NextSession.transcript_hash, Hash.length)
  NextSession.server_traffic_secret = HKDF-Expand-Label(NextSession.master_secret, "s ap traffic", NextSession.transcript_hash, Hash.length)
  //NextSession.session_master_secret = HKDF-Expand-Label(NextSession.master_secret, "session", NextSession.transcript_hash, Hash.length)

  NextSession.server_write_key = HKDF-Expand-Label(NextSession.server_traffic_secret, "key", "", key_length);
  NextSession.server_write_iv = HKDF-Expand-Label(NextSession.server_traffic_secret, "iv", "", iv_length);
  NextSession.server_write_salt = HKDF-Expand-Label(NextSession.server_traffic_secret, "salt", "", salt_length);

  //NextSession.client_write_key = HKDF-Expand-Label(NextSession.client_traffic_secret, "key", "", key_length);
  //NextSession.client_write_iv = HKDF-Expand-Label(NextSession.client_traffic_secret, "iv", "", iv_length);
  //NextSession.client_write_salt = HKDF-Expand-Label(NextSession.client_traffic_secret, "salt", "", salt_length);

  NextSession.session_timestamp_raw = Date.now();
  NextSession.session_timestamp = htonl(Uint32Array.of(Math.floor(NextSession.session_timestamp_raw / 1000)));

  SessionIDPayload =
    NextSession.session_timestamp [4] +
    NextSession.master_secret [32] +
    NextSession.transcript_hash [32];
  
  NextSession.SessionID = AES_GCM.encrypt(sessionIdAESKey, sessionIdAesIv,
    SessionIDPayload) [84];

  Accept.body =
    AES_GCM.encrypt(NextSession.server_write_key, NextSession.server_write_iv,
      NextSession.SessionID [84] +
      ECDSA.integrityJSONSignature [64]
    ) [160]

  Accept [241] = Accept.header [81] + Accept.body [160]

  // response
  res.headers = {
    "x-status": res.status,
    "x-scheme": req.url.protocol.replace(/:$/, ''),
    "x-authority": req.url.host,
    "x-path": req.url.pathname + req.url.search, // Note: req.url.hash is removed
    "content-type": "application/octet-stream",
    //"x-request-id": requestId, // TODO: revisit later
    "x-request-timestamp": req.headers["x-timestamp"],
    "x-timestamp": Date.now(),
    "x-digest": "sha256-" + base64(SHA256.digest(res.body)),
    "x-integrity": "x-status,x-scheme,x-authority,x-path,header-list,x-request-timestamp,x-timestamp,x-digest;" +
      "hmac-sha256-" + base64(HMAC_SHA256(NextSession.server_write_salt, headers.join("\n") + "\n"))
  }
  res.body = Accept

  // at client (main document; encoded)
  session_early_lifetime = 300 * 1000 (msec)
  session_lifetime = 600 * 1000 (msec)
  NextSession = { session_timestamp_raw: Date.now() }; // TODO: number or htonl(Date.now()/1000)?

  // Validate headers
  res.headers['x-status'] === res.status;
  res.headers['content-type'] === 'application/octet-stream';
  res.headers['x-scheme'] === req.url.protocol.replace(/:$/, ''); // TODO: validate req.url
  res.headers['x-authority'] === req.url.host;
  res.headers['x-path'] === req.url.pathname + req.url.search; // Note: req.url.hash is removed
  res.headers['x-request-timestamp'] === req.headers['x-timestamp'];
  res.headers['x-timestamp'] within acceptable_timestamp_range // check timestamp with estimated clock difference
  res.headers['x-digest'] === 'sha256-' + base64(SHA256.digest(res.body));

  // decrypt res.body
  // decrypt Accept.header
  [ NextSession.serverRandom, NextSession.ECDHE.serverPublicKey ] =
    AES_GCM.decrypt(aesKey = AES_GCM.clientOneTimeKey, iv = AES_GCM.clientOneTimeIv, res.body.subarray(1, Accept.header.byteLength));

  // Derive Secrets
  NextSession.ECDHE.sharedKey = ECDH.deriveKey(NextSession.ECDHE.clientPrivateKey, NextSession.ECDHE.serverPublicKey);
  // For Connect
  CurrentSession.PSK = HKDF-Expand-Label(CurrentSession.connect_early_secret, "connect", "", Hash.length) // pseudo-PSK
  // For Update
  CurrentSession.PSK = HKDF-Expand-Label(CurrentSession.session_master_secret, "update", "", Hash.length)

  NextSession.early_secret = HKDF-Extract(0, CurrentSession.PSK);
  NextSession.handshake_secret = HKDF-Extract(Derive-Secret(NextSession.early_secret, "derived", ""), NextSession.ECDHE.sharedKey);
  NextSession.master_secret = HKDF-Extract(Derive-Secret(NextSession.handshake_secret, "derived", ""), 0);

  NextSession.transcript_hash = Transcript-Hash(Connect/Update + Accept.header);

  NextSession.client_traffic_secret = HKDF-Expand-Label(NextSession.master_secret, "c ap traffic", NextSession.transcript_hash, Hash.length)
  NextSession.server_traffic_secret = HKDF-Expand-Label(NextSession.master_secret, "s ap traffic", NextSession.transcript_hash, Hash.length)
  NextSession.session_master_secret = HKDF-Expand-Label(NextSession.master_secret, "session", NextSession.transcript_hash, Hash.length)

  NextSession.server_write_key = HKDF-Expand-Label(NextSession.server_traffic_secret, "key", "", key_length);
  NextSession.server_write_iv = HKDF-Expand-Label(NextSession.server_traffic_secret, "iv", "", iv_length);
  NextSession.server_write_salt = HKDF-Expand-Label(NextSession.server_traffic_secret, "salt", "", salt_length);

  NextSession.client_write_key = HKDF-Expand-Label(NextSession.client_traffic_secret, "key", "", key_length);
  NextSession.client_write_iv = HKDF-Expand-Label(NextSession.client_traffic_secret, "iv", "", iv_length);
  NextSession.client_write_salt = HKDF-Expand-Label(NextSession.client_traffic_secret, "salt", "", salt_length);

  // decrypt Accept.body
  [ NextSession.SessionID, ECDSA.integrityJSONSignature ] =
    AES_GCM.decrypt(aesKey = NextSession.server_write_key, iv = NextSession.server_write_iv, res.body.subarray(Accept.header.byteLength));

  // Validate x-integrity (delayed)
  // parse headers
  [ headerNamesCSV, 'hmac-sha256-' + headerSignatureBase64 ] = res.headers['x-integrity'].split(';')
  headerNames = headerNamesCSV.split(',');
  headerSignature = atob(headerSignatureBase64);
  headers = headerNames.map((headerName) => headerName + ': ' + req.headers[headerName] + '\n').join('') + '\n';
  // verify hmac
  HMAC_SHA256(NextSession.server_write_salt, headers) === headerSignature;

  // Register NextSession (TODO: at this timing?)
  Sessions.push(NextSession);
  CurrentSession = NextSession; // update current session with the next session

  // On Connect
    // verify integrityJSON signature
    integrityJSON = decrypt(fetch('integrity.json', {
      headers: {
        "x-method": "GET",
        "x-scheme": "https",
        "x-authority": req.url.host,
        "x-path": entryPageURL.pathname + "integrity.json",
        "x-session-id": base64(CurrentSession.SessionID),
        //"x-request-id": requestId, // TODO: revisit later
        "x-timestamp": Date.now(),
        "x-integrity": "x-method,x-scheme,x-authority,x-path,header-list,x-session-id,x-timestamp;" +
          "hmac-sha256-" + base64(HMAC_SHA256(CurrentSession.client_write_salt, headers.join("\n") + "\n"))
      }
    }));
    ECDSA.verify(ECDSA.serverPublicKey, ECDSA.integrityJSONSignature, integrityJSON) === true;

    // register Service Worker
    navigator.serviceWorker.register('hook.min.js');
    // verify Service Worker script
    fetch('hook.min.js', { integrity: hookMinJsScript.integrity }).ok

    // store integrityJSON
    caches.open(version).put(INTEGRITY_PSEUDO_URL, integrityJSON);

    if (caches.open(version).match(CACHE_STATUS_PSEUDO_URL)) {
      // skip fetching and loading cache-bundle.json
    }
    else {
      // load cache bundle
      cacheBundle = decrypt(fetch('cache-bundle.json', {
        headers: {
          "x-method": "GET",
          "x-scheme": "https",
          "x-authority": req.url.host,
          "x-path": entryPageURL.pathname + "cache-bundle.json",
          "x-session-id": base64(CurrentSession.SessionID),
          //"x-request-id": requestId, // TODO: revisit later
          "x-timestamp": Date.now(),
          "x-integrity": "x-method,x-scheme,x-authority,x-path,header-list,x-session-id,x-timestamp;" +
            "hmac-sha256-" + base64(HMAC_SHA256(CurrentSession.client_write_salt, headers.join("\n") + "\n"))
        }
      }));
      'sha256-' + Base64(SHA_256.digest(cacheBundle)) === integrityJSON['cache-bundle.json']; // verify
      caches.open(version).put(cacheBundle.entries);
    }

    // store Sessions
    postMessage(['plugin', 'Integrity:enqueue', Sessions]); // transfer session information to the Service Worker

    // at Service Worker before loading plugins
    hook.parameters.messageQueues['Integrity:enqueue'] = [ event ]; // enqueue the message event with Sessions

    // at client
    // reload the entry page
    
    // at Service Worker in processing the entry page
    data = hook.parameters.messageQueues['Integrity:enqueue'][0].data;
    Sessions = data[2];

    // at client (main document; decoded)
    postMessage(['plugin', 'Integrity:enqueue'], [port]); // request Sessions object from the Service Worker
    Sessions = data[2];

    // At Service Worker
    setTimeout(() => {
      for (let session of Sessions) {
        if (session.session_timestamp + session_lifetime < Date.now()) {
          // Discard invalidated Sessions;
        }
      }
      if (Sessions[Sessions.length - 1].session_timestamp + session_early_lifetime < Date.now()) {
        Update Sessions;
      }
    }, sessionCheckInterval)

t2ym added a commit that referenced this issue Sep 9, 2019
…; Avoid multiple simultaneous fetching of cache-bundle.json
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Sep 9, 2019
t2ym added a commit that referenced this issue Oct 1, 2019
…on for all Validation Service/Console requests (Note: Validation Service/Coneole is still a skeleton and non-functional)
t2ym added a commit that referenced this issue Oct 1, 2019
t2ym added a commit that referenced this issue Oct 1, 2019
…ks of leaking ClientIntegrity from process memory dumps
t2ym added a commit that referenced this issue Oct 2, 2019
t2ym added a commit that referenced this issue Oct 2, 2019
t2ym added a commit that referenced this issue Jan 14, 2020
…an infinite loop if a hacked Service Worker tries to replace the entry page
t2ym added a commit that referenced this issue Jan 14, 2020
…Hang up with an infinite loop if a hacked Service Worker tries to replace the entry page
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
@t2ym t2ym unpinned this issue Sep 24, 2020
@t2ym
Copy link
Owner Author

t2ym commented Jan 25, 2021

Issue: Inconsistent browserHash with URL search parameters

Root Cause

  • document.location is missing in the excluded volatile object list

Fix (merged at 0.4.0-alpha.49)

diff --git a/plugins/integrity-js/integrity.js b/plugins/integrity-js/integrity.js
index 7d60764d..a6884197 100644
--- a/plugins/integrity-js/integrity.js
+++ b/plugins/integrity-js/integrity.js
@@ -1516,6 +1516,7 @@
         '\.performance:object\.navigation:object|' + // navigation history
         '\.screen:object\.orientation:object|' + // screen device
         '\.location:object|' + // location object
+        '\.document:object\.location:object|' + // document.location object
         '\.localStorage:object|' + // browser storage
         '\.sessionStorage:object).*$'); // browser storage
       const volatileBooleans = [

t2ym added a commit that referenced this issue Jan 25, 2021
t2ym added a commit that referenced this issue Jan 25, 2021
…o the excluded volatile object list for _traverse()
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