radicle-dev / radicle-upstream

Desktop client for Radicle.
Other
616 stars 51 forks source link

keymanagement: passphrase #868

Closed xla closed 4 years ago

xla commented 4 years ago

Key management: passphrase

We want to secure the key(s) generated for, but owned by the user as effective as possible. Especially while the application is running and users defer authority to sign with their keys to Upstream via the proxy. Fortunately the radice-keystore encrypts its content with the help of a password. Currently the missing link is to let the user provide the password of the keystore and ensure that it needs to be reentered to unlock the keystore beyond sessions and potentially after timeouts, much like gpg agents work.

Requirements

Flow

Key creation

During the creation of the identity - also Onboarding - the user is prompted to provide a passphrase, this we can use like:

App restart

API

Rough and high-level description of the API concerning the management of the passphrase protected keystore.

Unauthorized

To indicate to the UI that we expect an unsealed keystore we can return an appropriate status codes for all endpoints which require access to a Signer. Along with it we send the location of the endpoint to access to unseal

> GET /any-endpoint-with-signer HTTP/1.1
> User-Agent: electron/lul
> Host: http://localhost:8080
> Accept: */*
< HTTP/1.1 401
< date: Thu, 03 Sep 2020 15:58:25 GMT
< expires: -1
< cache-control: private, max-age=0
< location: https://localhost:8080/v1/session/unseal

Unsealing

> POST /session/unseal HTTP/1.1
> User-Agent: electron/lul
> Host: http://localhost:8080
> Accept: */*
{ "passphrase": "0p3ns0urc3c01n" }
*
< HTTP/1.1 204
< date: Thu, 03 Sep 2020 15:58:25 GMT
< set-cookie: <proxy_secret_key>=<generated_secret>; Max-Age=6000

The Max-Age attribute is included in the example to show that down the road or as an extension to this proposal we can require for the passphrase to be entered after an interval during a running session.

UX

Every time the UI encounters a 401 for one of the endpoints it should lock the interface completely to display a non-escapable modal asking for the passphrase.

Open questions

xla commented 4 years ago

@radicle-dev/engineering As this is security sensitive I'd like get as much feedback as possible. Even if there is need to expand and hash out more detail for peepz to assess.

CodeSandwich commented 4 years ago

Seems pretty sound. I wonder how hard would it be to perform a man-in-the-middle attack and impersonate proxy?

MeBrei commented 4 years ago

Great description!

App restart

  • check for existing key(store)
  • seal API access with appropriate error codes
  • user reenters passphrase to unseal keystore

UX

Every time the UI encounters a 401 for one of the endpoints it should lock the interface completely to display a non-escapable modal asking for the passphrase.

FintanH commented 4 years ago

create a secret valid for the current session

For my own education, what is this meant to do/achieve?

CodeSandwich commented 4 years ago

AFAIU it's a random key generated and valid for a single connection session, which is a proof of a successful authentication. This key can be used all the time for authentication during a session instead of a more sensitive user password. If it's stolen, the attacker won't gain that much. It also changes regularly making the channel harder to crack.

xla commented 4 years ago

Seems pretty sound. I wonder how hard would it be to perform a man-in-the-middle attack and impersonate proxy?

@CodeSandwich Fair question, generally when thread modelling for the application we have to assume there is not that can be done when your local machine is compromised as then scenarios like injected keyloggers are on the table. Some attacks can be mitigated when migrating to HTTPS for the ui <> proxy communication - which is an open question of how we can achieve that.

xla commented 4 years ago

@MeBrei

  • Can the keystore handle multiple pass phrases? ie I forget my password -> I create a new Identity -> I remember my old password.

It does not afaik. Recovery is a rabbit hole in itself and should be explored separately. For now we need to assume, if a user lost their passphrase they lost access to the encrypted key.

  • Just for clarification: The ui will determine if it should show the reenter-passphrase or create-identity screen based on an api call that checks if the key(store) exists or not?

Yes and no. It will not directly check if a keystore exists, just any 401 will indicate that a re-auth is required and we need the user to enter the passphrase. For the identity creation part we can inspect the session state as we already have that functionality in place.

  • There should also be an opportunity to create a new identity.

Indeed, we haven't looked yet at "logout", "start fresh". This would require some work from @radicle-dev/product.

xla commented 4 years ago

For my own education, what is this meant to do/achieve?

@FintanH More or less what @CodeSandwich mentioned. This is to full-fill the requirement of

  • an unsealed keystore should only be valid for a single instance of a client

as only the client which unsealed the keystore knows the secret another client accessing the proxy API has to provide the passphrase again and therefore can't make use of API endpoints without knowledge of the passphrase. A thread to mitigate here would be, the user authed through the UI and unsealed the proxy, another user land process without elevated rights accesses the proxy API and performs operations which require signing.

kim commented 4 years ago

can we set up HTTPS between UI and proxy

Yeah, so, that's a bit of a blocker here, isn't it? Not only would the proxy certificate chain have to be anchored in Chrome's CA roots, but to pass the TLS handshake the proxy will need to be able to sign using the key advertised in its certificate -- and then how do you secure that key?

To take a step back: we just fork the proxy process in the electron main, which then binds to localhost:8080. Any other unprivileged process can intercept that traffic, talk to the proxy, or even impersonate as it if it manages to bind to that port before the forked process does. That problem doesn't go away by using other ways of IPC, unless electron had a cross-platform way of namespacing them to the process (not the user), which I don't think it has.

What this means is that, if we send a password over that socket, and a "secret" value back, we could just as well store the secret key unencrypted, and spare ourselves the trouble.

I'm not very familiar with electron, but I believe the best way we can deal with this is to make the main.js prompt for the passphrase before forking the proxy process, and then writing it to its stdin. It also must exit if proxy doesn't come up (eg. because something else is already bound to 8080). If that passphrase prompt uses chromium IPC, it is at least hard to predict which fd will be used.

tl;dr The idea of seal/unseal is not actually sound given the channel being insecure

kim commented 4 years ago

Oh wait, but then we have unsealed the proxy, so everyone can perform privileged operations :facepalm:

Can we somehow secure the channel by which the "session token" is transmitted?

CodeSandwich commented 4 years ago

Can Electron easily pass a parameter to its child when forking a process? This could be a good security root.

xla commented 4 years ago

Can we somehow secure the channel by which the "session token" is transmitted?

@kim In the main electron main thread we have the entire nodjs stdlib at our disposal, so we can exchange the secret through other ways and communicate it back to the render process. What secure ways can we think of to exchange the secret between two process?

cloudhead commented 4 years ago

How can an unpriviledged process intercept HTTP traffic?

What is the threat model? If it's "a rogue process was installed and is running as your user", then I'm not sure there's a way to be safe against that.. it could patch the app binary with a keylogger.

The only way to be really safe is to either prompt for the passphrase via pinentry/keychain or perhaps to install upstream as a system package..

kim commented 4 years ago

How can an unpriviledged process intercept HTTP traffic?

You only need cap_net_raw iirc, and my awesome-block-explorer node module legitimately needs that, don't you agree?

it could patch the app binary with a keylogger

Modifying the app binary and elevating its privileges is harder to pull off unnoticed. But yes it's true that this is hard to defend against.

I think @CodeSandwich is on the right track: the main thread can generate a random encryption key, and pass that to the forked proxy (via stdin). Afaiu, this won't affect any renderer windows, so TLS is still not a possibility. Also, IPC is just domain sockets, so that's not secure either (unless the upstream process runs under its own user).

However, the main thread can pass the public part of the random key to BrowserWindow via additionalArguments, and forget about the secret part immediately. Now, the dialog just needs to encrypt the passphrase entry in javascript before sending it to the proxy. We have now secured the passphrase against eavesdropping.

Alternatively, the main thread may remember the public key, too, and use some other means to get at the passphrase (eg. the system keychain, or an HSM), and encrypt it before passing down to proxy.

kim commented 4 years ago

What is the threat model

I would think that the passphrase is actually more valuable than the key material itself, because betcha you're using the same for your node wallet.

kim commented 4 years ago

The only way to be really safe is to either prompt for the passphrase via pinentry/keychain

Only if it is the proxy itself which triggers that.

xla commented 4 years ago

This has been great input, will update the original issue with the accumulated changes to improve the flow and secure the actual exchange between the processes.

However, the main thread can pass the public part of the random key to BrowserWindow via additionalArguments, and forget about the secret part immediately. Now, the dialog just needs to encrypt the passphrase entry in javascript before sending it to the proxy. We have now secured the passphrase against eavesdropping.

@kim For clarification I gonna expand on the flow here to see that I got it right:

For the javascript part I'd recommend to tap into the native WebCrypto API and as we need an asymmetric crypto algorithm the only candidate is RSA-OAEP - while not being the most efficient for decryption and not post-quantum I don't see strong reasons to not accept it for this use-case.

kim commented 4 years ago

Fell into the same trap as before: once we have “unlocked” the proxy, anyone who can talk to localhost can make it sign things on the local user’s behalf. I don’t find this particularly appealing.

I think the options are:

  1. Make all request from the UI go through the electron main (over IPC), which talks to the proxy over TLS, and holds the session token as per the original proposal.

    The certificate could be generated on the fly with a key passed on startup, as above. The certificate validation in node would be overridden to accept certificates signed by the key just issued.

    Passphrase entry would be controlled by main, which should either show a native dialog, go through the system keychain, or employ the encryption trick described above with an ephemeral key (the proxy itself wouldn’t be involved here).

  2. Turn the proxy into a native node module.

    This has the benefit of eliminating the additional hop, but needs to solve the secure passphrase entry issue, too.

  3. Handle all authentication in the proxy.

    The benefit would be that it may actually not be so bad to be able to use the proxy’s API from elsewhere, and it would just display a system dialog whenever authentication is required. The downside is that this is potentially more laborious to solve cross-platform (compared to a browser window spawned from node). It may also be confusing as that modal won’t lock the main UI.

I kind of like 3., thinking of the proxy not as a daemon, but a headless application. It might be that 1. is quicker to get going, though.

kim commented 4 years ago

Just to clarify: the session cookie is as good as the passphrase, so it can’t be transmitted (repeatedly!) over a cleartext channel.

juliendonck commented 4 years ago

Design is ready for this when y'all are ready to pick this one up. figma

xla commented 4 years ago

Superseded by #997 #998 #999