Securing cargo publishing credentials

There is an accepted RFC to allow development of external tools to protect these credentials:

It'd be great to get this implemented and start experimenting with credential helpers that utilize system keychains/FIDO tooling.

EDIT: Oh, it is even already implemented with example system keychain wrappers :tada:. So as long as you use nightly to publish it is possible to start experimenting with using it, and hopefully if a few people do that can provide evidence on what needs changing to get it stabilized.

I've seen this RFC, and I'm afraid it does nothing. There is no mechanism to establish a chain of trust between Cargo and the helper, so anyone can call the helper from anywhere to make it tell what is the Cargo token (for example, on macOS it will tie the key to bash, not cargo, so everything that runs bash can see the password).

My assumption is that you would mark your registry credential as "user action required" in the keychain, so that it would prompt the user to unlock it on each use. That does mean there is still a window for a worm to watch for you publishing and do its own publish during that window when you are expecting a credential request, but there are many ways a permanently running worm could sneak its way into that process (e.g. replace the .rs files on disk between you starting cargo publish and it packaging them up). This should be enough to protect against automated background publishing.

But that "user action" would be user-space implementation, which other user-space programs can hack into (e.g. patch a jump instruction in the helper's executable to skip the prompt). OTOH macOS Keychain is tied to kernel code signing verification, and can't be bypassed from user space. Any manipulation of the helper or cargo would invalidate its signature, and that would block access to the keychain.

1 Like

On Linux the keychain implementation often uses a program stored in /usr that is started by PAM when logging in and given the user password to decypt it's database. This program can display a prompt to the user. It is not possible to change this program without root permission, nor is it possible to convince PAM to give another program the user's password without root permission. At least in case of KDE Wallet the user by default has to give their password again when any program (including the cargo helper) want to access any stored credential1. Gnoome keyring also doesn't let the program requesting the credential show the password prompt2.

@Kornel, I would love to talk with you and anyone else about how to improve the credential-process design. Perhaps it would make sense to start a new topic (I'm not too familiar with Discourse on how to fork topics)? I'm using your library on macOS, so I assume you're pretty familiar with it.

The credential-process design as-is isn't intended to be super-hardened. To me, security is a spectrum, and this initial design was intended to be "don't store unencrypted tokens on the filesystem", which protects against attacks which have the ability to read files. I'd say it also goes further to protect against unrestricted attackers until you try to publish.

My impressions is that a next step in hardening it is to sign the executables, which we don't have the ability to do, yet. I'm somewhat familiar with signing on macOS and Windows, but I have no idea how that works on Linux.

AFAIK, the macOS keychain will be tied to cargo (not bash), and depends on whether or not you click on the "Always Allow" button in the popup. If you don't click that, then it should ask for a password every time.

The 1password integration always requires a password, and is limited by the security design of 1password (which AFAIK is not well geared to protect against when an attacker has unrestricted access).

I'm least familiar with the GNOME secret stuff, as I am not a Linux user, and I don't know if anyone actually uses that. I included an implementation as an experiment.

I'd be happy to have some clear identification of what the next steps in improving the security would be. I assume it would be something like signing executables and 2FA, neither of which we have the capacity to do right now. But if there are other steps, I'd like to know about them.

3 Likes

Your new topic is started. :+1:

2 Likes

The proposed RFC shows this example:

[registry]
credential-process = ["bash", "-c", "pass tokens/crates-io | head -n 1"]

I assume that for macOS usage would look something like:

[registry]
credential-process = ["cargo-credential-macos-keychain", "get"]

then the keychain entry would belong to the cargo-credential-macos-keychain executable, and not cargo.

The problem is that anyone can call cargo-credential-macos-keychain get and it just prints the password in clear text.

Fortunately, the fix for it is easy. Don't make it a real executable. Move keychain code to cargo executable itself. This way the keychain entry will belong to cargo, and cargo won't need to expose it directly.

You've mentioned there would be a prompt. Currently there isn't one. I'm guessing you mean something like "Do you want to publish the crate [y/n]"?

You'd have to ensure that this prompt can't be answered via terminal, since an attacker can control stdin/tty and can answer it automatically. You may need to open a GUI window and disable accessibility for that window (so that attacker can't send clicks to it either).

Some prompt would be a good idea, but purely client-side prompt depends on client-side security, and if the threat here is a worm that infected client's machine, then there's no guarantee that client-side prompt will hold.

In this case prompting could be strongly enforced by the registry asking for TOTP code or U2F attestation. Luckily, Firefox's U2F implementation is in Rust and is very easy to use.

Regarding code signing on macOS.

Strictly speaking, signing is not required to use the Keychain, but highly recommended. If there's no signature, then macOS prompts user "Do you want to allow cargo to access the Keychain?" and IIRC it will remember hash of the executable.

Unfortunately, without a signature it will have to ask again when the executable changes, and there's no way to distinguish whether it changed due to upgrade or malware, and users will quickly get used to clicking Allow every time.

If cargo was signed with Developer Id certificate, then the Keychain entry would be automatically given to the executable based on the Developer Id, without asking.

Hence those executions shouldn't yield the password, they should only sign one particular instance of the crate and return that signature, or token to publish a crate with exactly that content. If each of those executions requires interaction with my hardware token, then a bad actor calling it does not give it anything. Quite the opposite, if any of those prompts ever shows up out of the blue I'll avoid ever executing anything remotely security critical on my hardware again. Similar advantage is gained with a software-based token or a keyring running in a different process group that the user can't access, only that there it is easier to impersonate the process and steal the password for the token, or show incorrect details on the operation I'm supposedly consenting to.

This would put the design closer to the ssh-agent. It also never hands out the cryptographic material itself but only performs each of the steps required for key agreement with the server during connection setup. This reduces the surface dramatically. It also makes it hard to worm since the user can configure it such that each access only signs one publish action. In theory we'd like to see the requesting process somehow uniquely identifying itself—i.e. that the agent can show 'process id xyz (cargo)', or even '(cargo—signed by)'—but that's a different attack scenario where the attack must race cargo and simultaneous block the original request to remain undetected (see incineration scenario).

2 Likes

Does this require all development of these keychain connectors to happen in Cargos repo? Or is there some way they can still be community maintained?

It can be pulled in as a crate. The only important bit is that cargo runtime process calls Keychain on its behalf, not via std::process::Command.

It would be great if crates.io supported hardware 2FA like U2F server-side.

Unfortunately, without server-side support U2F is not useful. I've looked into using hardware keys for cargo-crev, and found that it's pointless for purely client-side tools. This is because the U2F protocol only answers a challenge, and there is no reliable way to make it sign something with a specific private key, or protect a secret like crates-io token (it could be done with other type of HSM, but it's a much higher bar than requiring a common 2FA key).

This means that client-side there is no way to enforce use of the key. You can't even use it to establish chain of trust between cargo process and the helper process, because any process can use the hardware without limitation, and each end can be spoofed. This reduces the hardware token to be just a button, not cryptographically stronger than a mouse button.

Besides, the RFC has already defined the helpers as tools that expose tokens in clear text to anybody who calls them. There's no room to introduce proper HSM-backed signatures in helpers within scope of this RFC.

Hmm, I was just doing some testing and it isn't quite working like I was expecting. The macOS Keychain is explicitly allowing the process unrestricted access to any entry it creates, which is not how I remember it working. I may have been confused because if the process hash changes, then it always prompts for the password until you click "Always Allow", but otherwise doesn't prompt (and I was probably rebuilding the executable several times). Unfortunately, I don't see a way to add a password and say "and don't trust me to read it", unless you know of any?

On macOS, would it make sense to create a new Keychain instead of using the default one and setting prompt_user to true to force a prompt every time?

I encourage you to try the current implementation on nightly and see how it works. The documentation is here and the code is here. For macOS, the config would be:

[registry]
credential-process = "cargo:macos-keychain"

You also need to remove any current token configs if you have any. This can be done by temporarily moving ~/.cargo/credentials out of the way, or using the new cargo logout command which will remove the token config.

Then, you need to login. This can be done with cargo login -Z credential-process and just type in some random text for the token (we're just testing here).

You can then try publishing something (create a new package with cargo new foo or whatever, something that won't actually publish). Run cargo publish -Z credential-process.

We could move the keychain code into cargo itself. I'm not sure if that helps much, because if an attacker has unrestricted access to execute programs, they can just run cargo. Would that actually improve the security much to move it in-process?

The 1password helper always asks for a password on the console (managed by op). I was thinking the other keychain managers always displayed a secure GUI prompt, but I realize I was mistaken about that.

It seem to me the root problem is that crates.io allows publishing arbitrary crates from a single bearer token that has a very long lifetime. Of course that's not secure. I'll quote Schneier on this, in the context of bank transaction because the attack model and scope of compromise seems similar:

Of course it’s a broken model. We have to stop trying to authenticate the person; instead, we need to authenticate the transaction [..] from Hacking Two-Factor Authentication - Schneier on Security

And even if we keep only the symmetric authentication, due to implementation complexity or w/e, the secret authenticating to crates.io need not allow publishing of crates in and of itself. It might be limited to publishing of crates signed with a pre-configured key. And Signing alone does not require an interactive protocol and even if it did, no on publishes via the website so we could implement this locally. Then the signing process can be performed offline and be done with a hardware token or any other method that the developer deems secure on their local machine. That is, if we stick only to the wording of the RFC and somehow want it to simultaneously solve all security problems of publishing.

But really 2FA is mostly relevant for securing password-based authentication. (Which disguises how broken password methods/bearer token authentication on HTTP web is.) Why does crates only support symmetric token for access? Might we suppose that all secure developer machines have ssh, and establish a new method of publishing crates via such a connection? No more symmetric token, less risk of silent and persistent compromise. This would give strong authentication and integrate with well-established key-management methods.

It would secure the token against simple grab-and-run attacks, because the token wouldn't leave the cargo process. Attacker couldn't just patch/replace cargo executable to make it leak the token either, because that would invalidate signature/hash that macOS associated with its Keychain.

It wouldn't prevent running of cargo publish unauthorized. To secure against this attack some kind of secure prompt is necessary. Keychain access prompt is not it: it's only to allow executables without a valid signature, so it's more of a security risk than a protection (it's an equivalent of warning against self-signed certificates in TLS). I think 2FA would be the best for this.

I don't quite see it that way. The intent here isn't to protect against an attacker that has unrestricted access. The intent is to improve over the current state where the token is unencrypted on disk. I think if someone has full control over your system, the number of ways they can attack the package registry are a bit greater than just adding 2FA can protect against.

I'm trying to work in the constraints we have to improve the security somewhat. I'd prefer to talk about what we can do to improve things now, rather than what we wish we could do (but can't).

Technically the helper is better than a token sitting in a file, but only barely. It doesn't protect the token from being stolen by a malicious build.rs or a proc macro. If you changed the helper from being a bin to being a library crate running in the Cargo process, it would.

How is having direct cargo integration more secure than a helper? In either case a malicious program could ask it to do something bad. (directly publish a malicious crate in case of cargo or expose the secret in case of a helper) The only way to prevent this is by requiring the user to securely enter a password. This works just as well for a helper as for direct cargo integration.