hasgeek / funnel

Website for hasgeek.com
https://hasgeek.com/
GNU Affero General Public License v3.0
47 stars 52 forks source link

Signed database records #1169

Open jace opened 3 years ago

jace commented 3 years ago

Membership records (#401) are designed to keep records for the platform's needs first. For instance, if someone has been removed from an organization and files a request with customer support, the revoked membership record serves as proof for the claim.

There is no corresponding measure by which the platform can prove the record was created by legitimate user action -- essentially a cryptographically signed database record. This issue documents the possible approaches and their concerns.

Handling the secret

Since Funnel is a web app, there is no straightforward mechanism for client-side storage of a secret key that is shared across login sessions. Solving this may add unnecessary (a) cognitive overhead for the user in the UI, and (b) maintenance overhead relative to the security needs of current functionality. This option is therefore off the table. (Update: see first comment, this is actually viable and corresponds to option 3 below.)

Server-side storage is an option but has to be protected from misuse. The secret can be encrypted using the user's password. To use it, the password will be required again. The requires_sudo decorator from #893 is suitable and can be extended to decrypt the secret and store in some form of cache for the timeout period.

  1. Server-side cache is straightforward but unsafe. The server has unrestricted access to the cache and the cache may expunge contents ahead of the desired time. This option is off the table.

  2. A HTTPS-only cookie will be protected from JavaScript, and can be restricted to a path so that it is only submitted to endpoints that handle signed records. The cookie will need to be signed with the server's secret key (as usual) to protect from tampering.

  3. Using browser LocalStorage will offload key management entirely to the client. It absolves the server of responsibility as the server does not need to see the decrypted key at all: it can send the client the password-encrypted key and let the client handle decryption. (An insecure client remains a risk.)

Signing records

Cryptographic signatures require a binary-stable representation of data. Any format -- JSON, YAML or TOML -- can be used as long as the implementation guarantees identical output for identical input today and in the future. This implementation must be shared between client and server if using client signatures. JSON with JWT may be appropriate.

The server must switch from receiving HTTP POST forms to signed payloads (for eg, JWT) and retrieving data from it.

Signed record storage

The existing database architecture -- of storing data in SQL columns -- should continue to be used. However, the signature will require three additional columns: the signature (or the entire signed payload, but requiring more storage), a foreign key reference to the signing key, and a version number for the database structure.

To verify data against the signature, the app should be able to reproduce the payload from SQL, producing a binary blob that matches the signature. However, since database structure migrations do happen, this will require keeping revisions and their representations as an app feature, and not just in the migrations system (Alembic).

If a migration removes a column, the payload cannot be reconstructed. The original must be preserved.

All incoming data should be verified if signed, but proof of a signed record is not a standard requirement when sending data to the client. It is available should the user seek to verify. Having the original payload allows offloading verification to the user.

Key verification

The server must also verify that the payload from the client is using the user's secret key. Since the server does not have the secret key in option 3 (client-side storage), the key must be part of a public-private key pair, with the server storing the public key unencrypted. PKC adds overheads that are not present in option 2 using trusted server-side code.

Lost key replacement

Should the user forget their password, the signing key will be irrevocably lost. In this case the existing key record should be archived and a new record created. Archival is needed for verifying past signatures. This creates a new problem: the server could replace the user's key, sign records with it, and then archive it while preserving the existing key, thereby faking those records. If keys are archived when the password is lost, then there is no way to verify the key as legitimate.

Additionally, since public keys must be revealed to audit a record, this will have the side effect of revealing user account status to the world. This is a similar risk to how WhatsApp reveals the fact of a new phone when it issues a notice about the security code having changed, and it can only be mitigated by limiting access to the public key.

Implementation levels

The concerns described so far represent three levels of advancement in handling the problem:

  1. Trusted server code and trusted server admin: as at present. Nothing is signed.

  2. Trusted server code, but untrusted server admin: this level assumes the code is safe (and can be audited from its Git history), and only protects against an admin who has access to the server, as they cannot fake a record by a user without knowing their password. Symmetric encryption is (mostly?) sufficient.

  3. Untrusted server code and admin: Moving signatures entirely into the client will remove the requirement to trust the server, but will require a fairly robust PKC implementation, particularly around the "lost key" vulnerability.

Note: This issue ticket is only concerned with signing records for audit. It is not about encrypted data records.

jace commented 3 years ago

There is a Web Cryptography API that offers reasonable security for encryption keys in the client, but the keys cannot be retrieved and cannot be synced across clients. This effectively binds an encryption key to a UserSession record – the client makes a key pair and submits the public key – which is not bad.

Browser support also seems to be fairly good (with supported algorithms in this undated report).

jace commented 3 years ago

Web Cryptography API reading material:

  1. Using the API to generate a public/private key pair: https://getstream.io/blog/web-crypto-api-chat/
  2. JWK for JSON Web Key: https://datatracker.ietf.org/doc/html/rfc7517
  3. Guide to related technologies: https://pomcor.com/2017/06/02/keys-in-browser/ (cache version if inaccessible)
  4. Some more from Stack Exchange: https://crypto.stackexchange.com/a/52488/80562
jace commented 3 years ago

The new ideas in #1097 and the existing OAuth login options don't use a password, so they remove the opportunity for using password to encrypt a key. However, this encryption is unnecessary if the key is entirely client-side.