This document describes the architecture of WebSign, Cyph’s proprietary “secure web application” technology. WebSign was originally presented during our talks at Black Hat 2016 and DEF CON 24 (deck, video), and is now protected by US Patent 9,906,369.
A core technique involved is what we refer to as “HPKP Suicide”, an abuse1 of a web security standard called HTTP Public Key Pinning that essentially lets us “trick” the browser into behaving as though a server is persistently offline after its first interaction with that server.
General summary of HPKP Suicide’s steps (and the talk goes into more detail if needed):
- Server generates TLS key pair and stores it on a volatile RAM filesystem (e.g. tmpfs).
- Server requests a new TLS certificate for that key pair from a certificate authority or requests to rekey its existing certificate.
- After obtaining the new cert, server updates its web server configuration to include an HPKP header with a hash matching the new public key.
- Server starts serving with the new config/cert/key.
- Server waits for as small an interval as possible before proceeding to the next step. (e.g. Let’s Encrypt’s rate limit makes an 8.4-hour interval possible, and DigiCert is allowing us a 12-hour interval.)
- Server destroys its current TLS key pair (making it practically unrecoverable due to it having been stored only in RAM during its lifetime).
- Server goes back to step #1.
After each iteration of these steps, any HPKP-supporting browsers that had hit the server at any point during the interval from step #5 will now treat the server as though it’s literally offline (AJAX requests will fail, attempts to navigate to the server’s hostname will bump into an unskippable error page, etc.) — which the server will have no power to undo without access to the private key it deleted in step #6. In other words, we’ve now (approximately) achieved the needed TOFU property.
Note: As explained further below, a fallback mechanism that’s almost as good is used to support non-HPKP-supporting browsers, which at present includes every browser other than Firefox.
With this, we can now pin whatever logic we’d like using a web standard that facilitates offline web applications (often used for games that don’t depend on access to a backend API 100% of the time): AppCache (and/or its successor, ServiceWorker). Because the server is pseudo-offline, the browser will always fall back to the logic contained within the AppCache cache rather than pull untrusted code from the server.
In the case of WebSign, we take advantage of this to pin our code signing framework within the browser. Here’s a high-level overview of how these pieces all fit together:
Specific mechanics of our code signing implementation are as follows.
Package generation/signing components:
- Build environment (machine where code is compiled and deployments are initiated).
- Air Gapped Signing Environment (air gapped microcontroller, on which SPHINCS and RSA release signing keys were generated and continue to be stored — functionally similar to an HSM):
Package generation/signing steps:
- Build environment swaps out all subresource references in application’s index.html with markup similar to SRI (for a WebSign-specific implementation of the same concept).
- Differences between WebSign-SRI and the SRI standard:
- Rather than just JS and CSS, WebSign-SRI supports any arbitrary subresource; for this to work, non-JS/CSS subresources referenced anywhere throughout the code (e.g. images) are first converted into text files containing base64 data URIs.
- In the event that a subresource fails to validate during the initial package load, rather than just silently discarding that subresource, WebSign-SRI will abort loading the package entirely.
- Rather than being restricted to newer versions of Chrome and Firefox, WebSign-SRI works in any browser that isn’t ancient (although more recent browser versions with native SHA-512 support get a small performance boost).
- Differences between WebSign-SRI and the SRI standard:
- Build environment is connected to signing environment via temporary unidirectional fiber network.
- Build environment sends index.html to signing environment — along with some metadata, e.g. an expiration date and the allowed hostname for running the package (which would be cyph.ws in the case of Cyph’s production deployment).
- After manual review in a terminal pager and explicit confirmation, signing environment signs the package.
- Unidirectional network direction is physically reversed (cables swapped).
- Signing environment sends signed package back to build environment.
- Fiber network is disconnected.
- Build environment publishes signed package and all subresources to github.com/cyph/cdn, which our CDN pulls updates from.
WebSign client logic:
- Downloads and validates the WebSign bootstrap itself against a hash whitelist from the signed package metadata. This is meant to provide a form of TOFU even in browsers where HPKP is unavailable, albeit much more fragile and subject to things like cache eviction attacks, but also has the side effect of protecting users during the initial download from naive tampering by things like malware and harmful TLS-intercepting antivirus software. Failures abort the package load process and display:
- With this degraded level of TOFU, rather than outright blocking a tampered version of the WebSign bootstrap from ever hitting the browser, we instead are able to warn the user when a tampered bootstrap has been downloaded and queued for execution the next time they navigate to the page.
- Chooses the correct CDN node based on api.cyph.com/continent (e.g. eu.cdn.cyph.com).
- Downloads package from CDN and validates against public keys.
- If the package fails to validate — and an older still-valid version of the package isn’t cached locally — the process will be aborted and a message will be displayed indicating that the application failed to load.
- Executes HTML from package, with subresources being processed by WebSign-SRI.
While this all works very well for Cyph given that Cyph was from the start architected and developed against WebSign with its usage being a hard requirement, there are a number of considerations and caveats to keep in mind when evaluating WebSign for use in any other (particularly non-greenfield) software:
- Single-page applications only.
- The URL fragment must be used for routing views (e.g. app.com/#foo/bar). However, this requirement can be skipped when restricting support to browsers with ServiceWorker capability.
- WebSigned applications that use Content Security Policy (which any WebSigned application should do) must do so through the “CSP Meta-Hardening” technique demoed in our BH/DC talk.
- Broadly speaking, all subresources should be self-contained within a WebSigned project, not fetched from remote origins like CDNs (otherwise WebSign adds no value). This includes ensuring that any third-party libraries used don’t have hard dependencies on remote subresources, and finding alternatives to those libraries when they do.
- Due to various traces necessarily left in the client (pinned keys, AppCache, etc.), WebSign isn’t appropriate for applications with threat models geared toward a strong guarantee of deniability of use (e.g. SecureDrop understandably treats this with a higher priority than data confidentiality).