Closed xla closed 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.
Seems pretty sound. I wonder how hard would it be to perform a man-in-the-middle attack and impersonate proxy?
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.
create a secret valid for the current session
For my own education, what is this meant to do/achieve?
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.
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.
@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.
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.
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
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?
Can Electron easily pass a parameter to its child when forking a process? This could be a good security root.
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?
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..
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
.
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.
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.
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
viaadditionalArguments
, and forget about the secret part immediately. Now, the dialog just needs to encrypt the passphrase entry in javascript before sending it to theproxy
. 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:
BrowserWindow
via additionalArguments
on spawnBrowserWindow
encrypts the passphrase for the passed public key
proxy
decrypts the passphrase with the private key received from the main processFor 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.
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:
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).
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.
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.
Just to clarify: the session cookie is as good as the passphrase, so it can’t be transmitted (repeatedly!) over a cleartext channel.
Design is ready for this when y'all are ready to pick this one up. figma
Superseded by #997 #998 #999
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:
Signer
to the current sessionApp restart
Signer
to the current sessionAPI
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 unsealUnsealing
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