Protecting Core from malicious apps

This post comes out of a recent discussion with @ryan where we both felt uncomfortable from a developer UX perspective about having to provide the Core API password when starting the Portal for Development.

The conversation led me to spend some time thinking about our Portal-Core threat model. What problems are we trying to solve with Core API password, and are we succeeding?

TL;DR: We’re trying to keep malicious apps from gaining administrative access over Core and the credentials it controls, and, as you probably guessed by the length of this post, we’re not succeeding!

Background

Blockstack Core API is an Python application that runs with user-level permissions. It provides a HTTP REST API that accepts requests on localhost. Through this API, clients can access a user’s storage and bitcoin wallet.

Blockstack Portal is a single page JavaScript application that runs in the a user’s web browser. It is served via a web proxy on localhost.

Objective

Only the Portal should be allowed to have “administrative access” to a user’s Core API.

We want 3rd party apps besides the Portal to be required to request a permission-restricted session token, approved by the user via the Portal and issued by the Core API. 3rd party applications must use that token to make Core API requests that require authentication.

Reasoning

Core API holds the user’s storage provider credentials and consequently has administrative read-write access the user’s storage. This means that any API client with administrative to Core can arbitrarily share, modify or delete items in the user’s personal storage.

In our current (v0.6) deployment method, Core API also has full access to the private keys of the user’s Bitcoin wallet with the ability to generate and send Bitcoin transactions at well. API clients with administrative access to Core can spend the user’s bitcoin without their knowledge or approval.

Current approach

We currently use a shared secret approach to restricting administrative access to the Core API to the Portal. The user (or process) responsible for starting up the Core API generates an API password and provides it to Blockstack Core when starting the API endpoint. That user or process shares that secret with the Portal by embedding it in the Portal’s JavaScript file.

Portal provides the API password in the Authorization header of requests to the Core API. Core API makes a decision whether or not to grant access to certain API calls based on this password.

Additionally, Core encrypts the user’s bitcoin wallet with a password that must be provided at start up.

Problems with current approach

  1. Any application running on the same machine can obtain the API password by connecting to the Portal web proxy port, extracting the API password from the source code and gain administrative access to the Core API.

  2. Any application running as the same user can obtain the Core API password and Core wallet password by inspecting the environment variables of the Core API python process.

On macOS, one can list environment variables of a process by running:
ps eww <process-id>

On Linux, the same can be accomplished by reading:
/proc/<process-id>/environ

Discussion

Problem 1 - any app on localhost can obtain Core API password from Portal source code

Source code to the Portal web app is by necessity readable by any client that connects to the Portal web proxy port. This is how we’re able to make it available to any web browser running on the user’s computer. There’s no mechanism by which we can authenticate the Portal code to the Core API node. Instead, we should be authenticating the user of the Portal code.

The way for us to authenticate the user to the Core API to do that is for the user to provide some information - a shared secret, a signed message, etc - in the context of a browser session.

Possible solutions

I have a few ideas for how to solve this that I’ll share once I finish organizing them. In the meantime, what are yours?

Problem 2 - any app running as same user can obtain Core API password

Our threat model includes other applications running as the user. Potential threats include both apps in the user’s browser and native apps running with the same permissions as the user.

Preventing unauthorized applications from gaining administrative-level access to a user’s Core Node means not only preventing circumvention of the API’s access controls, but also preventing unauthorized applications from gaining access to the secrets that Core protects such as Bitcoin private keys and storage provider access tokens.

Core protects a user’s wallet on disk by encrypting it with a password. This password is required by Core when starting an API endpoint. It can be provided to Core by one of 3 methods:

  1. Setting an environment variable
  2. Interactive input by the user
  3. As a command line parameter

No matter how the password is provided, Core sets environment variables with both the API password and the Wallet password. (see screenshot below)

All 3 password input methods fail to protect the user’s private keys if the malicious app is running as the same user as Core. This is because the bad actor process is able to directly read the wallet and API passwords from the Core process’s environment variables.

Possible Solutions

  • Run Core as different user. (There are risk and complexity in this)
  • Change Core to not store these passwords in environment variable. Not sure if this is possible.
1 Like

I think putting Core under a different user is a suitable solution for this kind of problem. You want to hide a secret in the filesystem from other processes, so the standard approach is to use an own user for that.

Nginx does something similar with TLS keys. They only need to be readable by the root user. Nginx then starts as root, reads the keys in and finally drops root privileges. Probably a solution for this problem too.

Run Core as different user. (There are risk and complexity in this)

This is pretty straight-forward. We just need to use a program-launcher like DJB’s setuidgid to force Core and Portal to run as a separate user (e.g. nobody, daemon, or a custom user/group we create in the installer (like what Bitcoin does)).

Change Core to not store these passwords in environment variable. Not sure if this is possible.

Any program running as the same user as the Core or Portal processes can attach to it with ptrace(2) and read its RAM (e.g. by attaching gdb to the running process). The input method or secret storage method will not matter. This is my motivation to run these processes as separate users.

Keep in mind that applications on macOS are installed differently than they are on Linux. Applications aren’t archives of files that stored in various commonly used places around the filesystem, but Bundles:

A bundle is a directory in the file system that groups executable code and related resources such as images and sounds together in one place.

Users don’t install applications by using a package manager or running an installer. They download a zip file or disk image containing the application bundle and drag it to wherever they want it to live. (Typically this is the ~/Applications folder) This process doesn’t run any “installer” code - it’s the simple moving of a directory.

While there is a concept of an installer in macOS, apps using this method are the exception rather than the norm. Most users will rarely encounter an installer.

While we certainly could run Core as a different user. I disagree that it is straight-forward.

It requires that we:

  • ask the user for elevated privileges (that they might not be able to provide).
  • write an installer
  • write an uninstaller - users couldn’t simply drag our app to trash like they do for most apps.
  • write a privileged helper that runs as root all of the time and starts automatically at boot so that we don’t need to prompt the user for elevated privileges each time we push an update (keybase does this).

Additionally, it would result in the user’s Core configuration and wallet being stored in a place in the file system they can’t read or back up instead of ~/Library/Application Support/Blockstack

We’d also have to handle multiple installations of Core on the same computer. Do we create a separate Core user/group for each family member that uses the family iMac?

While this is true on pure POSIX systems, this isn’t true on macOS. It uses a system of layered defense, of which POSIX is just one layer.

We can codesign our Blockstack.app bundle with -o restrict and opt into System Integrity Protection (SIP) which prevents attaching to its processes by all users including root.

Google Chrome takes this approach. (code here)

This is the standard approach on Linux. It isn’t the standard approach on Windows or macOS.

Both have their own mechanisms and best practices for storing user secrets:

On Windows, this is the Password Vault

On macOS, this is Keychain Services

The key characteristics of both of these are:

  • secrets are only available to the apps that stored them.
  • apps can store and receive secrets without asking for a privilege elevation (no prompting users to enter their password!)

Putting user secrets in places where the OS doesn’t expect, requires user interaction and creates problems for users trying to securely back up or migrate their data.

Looking at Chrome as an example again, it stores the encryption secret it uses to encrypt its “safe storage” in mac’s Keychain.

This lets Chrome securely “hide” sensitive information (such as our encrypted with a password Blockstack Portal keychain mnemonic!) written to disk despite the fact that the user’s other un-sandbox apps having read permission.


I think we can come up with a solution that offers both a user experience and better security.
I also don’t think in this instance we need to ask the user for elevated permissions. We should avoid doing so if possible.

I envision something like this:

We:

  • codesign Core with -o restrict, opting in to System Integrity Protection thereby preventing other processes from attaching to the process and viewing its memory.
  • store Core’s secrets in the appropriate OS-specific user secret store (PasswordVault or Keychain Services).
  • refrain from storing secrets in environment variables
  • opt-in to the macOS sandbox for both the Blockstack menu bar GUI process & Core. (see this Chromium design doc for info how Chrome uses it)
2 Likes

Following up with my ideas for Problem 2 from the first post in this topic.

We need a secure channel to pass the Core API password to Portal. Our current method, embedding the Core API password in the web app source code isn’t secure.

Ideas:

1. Pass Core API Password as a URL parameter on first load

We could store the Core API Password in local storage. If the user wanted to use a browser besides her default browser, she would have to manually enter the Core API password. We’d also have to provide a mechanism with which she can discover her Core API password.

The Core API password would get stored in the user’s browser history & possibly appear in Console.app log files.

We wouldn’t need to change how Core works.

2. User selects password.

We already have an on-boarding process in Portal where the user selects a password and connects his storage provider.

We could modify Core’s API so that we could start the API endpoint in a minimal functionality mode where admin-level password protected API calls would only be enabled after the Portal sets the API password with the same password the user enters.

This would be nice in that we wouldn’t have to change the user on-boarding process.

3. Load Portal in a WKWebView of our SPI-protected app

We could pass Core API password to Portal via evaluate​Java​Script(_:​completion​Handler:​).

This would give us additional design control over our look and feel and solve our issues with localhost in the browser address bar without having to require root privileges to modify /etc/hosts (tagging @guylepage3. It would also solve our problem with Chrome showing our localhost app as insecure.

We wouldn’t need to change how Core works.

Another consequence is (on macOS at least), the Portal wouldn’t run in the user’s browser anymore. I see this as an improvement to the status quo where the Portal is only useful in the user’s default browser and doesn’t work in other browsers.


Any approaches I’ve missed?

I have just patched the rc-0.14.2 branch to stop storing passwords in environment variables.

The reason we were doing this is because we needed a way to pass secrets across fork() and execv() (i.e. when forking and starting the API process, and when (re-)setting global variables based on environment variables). Instead of using environment variables (which are leak-prone), Core will instead dump the secrets to a temporary, unlinked file visible only to the Core user account, use lsof to make sure that only the Core process has the file open, and then pass the file descriptor to child processes that need it or to itself on re-execv().

1 Like

Cool. Will that be readable by other apps that are also running as the Core user account?

Not sure if this is useful: https://developer.apple.com/library/content/documentation/Security/Conceptual/SecureCodingGuide/Articles/ValidatingInput.html#//apple_ref/doc/uid/TP40007246-SW10

They talk about a few other secure ways of passing data between processes.

Thanks for the link! Yes, it is the case that no other process–even one running under the user’s account–will be able to get the secrets short of doing a ptrace(2). Blocking ptrace(2) will be OS-specific, but it can be done on Linux (for non-root users via prctl(2)). We’ll still have to figure out the best way to do this.

1 Like

We decided to take the pass the Core API password via the URL approach for the time being.

Developers working on Portal no longer need to provide a Core API password when running npm run dev. Instead, they’ll be prompted on first run to enter it in the portal as below:

End users (using the macOS app), will not see the prompt for Core API in their default browser since the password will be sent to their Portal the first time they load up the macOS app. However, they will see the prompt in other browsers or if they try to open their portal in incoginto or private browsing mode.

Here’s the relevant github issue:

To address this concept, have you considered following the same sort of security model that OAuth/Open ID Connect has presented? Instead of Blockstack (the dev team) saying “The Portal app will always have root/admin access to Core’s data”, as part of the user onboarding/setup of the Portal/Browser app, the user clicks an “Authorize” button that starts an OAuth handshake with the Core service, asking for “admin” scope. The user would then see an authorization screen (created by Core) asking if they authorize this request. This is the same sort of flow used when using a “sign in with Google” service, where as part of that process the authorization screen has a “this app is requesting these permissions…” and the user can approve or reject them.

The Portal app would then just need to be smart enough to detect if a user accepted the sign-in, but rejected the “admin” scope, and show a warning/notice of all the things it won’t be able to do now. This would open the door to other apps being able to request “admin” scope, and the users needing to be smart enough to approve/reject it, but it then puts the issue very front-and-center. Similar to how working with a root-unlocked Android device allows applications to request superuser access, the users need to be savvy enough to allow it or not.

The “admin” scope could also be sliced up into finer detail, as with Android permission levels, so that a user can choose how much they trust a given application with their Core data access.

@MidnightLightning We have decided to remove the core piece from the Architecture due to a number of operational issues, least of which on mobile. We are in the process of moving that code to JS and having it natively in the browser. That discussion is here FYI:

Thanks! That discussion talks about renaming it (to “blockstackd”?) but doesn’t seem to be getting rid of it?

@MidnightLightning Core in this refers to what I was calling the blockstack-api in the other thread.