t2ym / thin-hook

Thin Hook Preprocessor
Other
4 stars 1 forks source link

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

Open t2ym opened 5 years ago

t2ym commented 5 years ago

Prototyping optional double encryption for integrity

Design Principles

Current Status

    // 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);
    }
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
client edge workers backend workers TLS body encrypted req/sec
h2load nginx (http2) 4 express (http/1.1) 4 yes yes 1,370
client edge workers backend workers TLS body encrypted req/sec
h2load nginx (http2) 4 express (http/1.1) 4 yes yes 1,960
$ 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 commented 5 years ago

Protocol Design (Tentative) in Pseudo-Code

Design Issues

Change Log

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 commented 3 years ago

Issue: Inconsistent browserHash with URL search parameters

Root Cause

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 = [