Even with 2FA, can’t a worm just replace/shadow cargo
and wait for the next action requiring 2FA to obtain a one-time code (or whatever other factor is required), then add the payload to the crate and publish at that point? Admittedly, it would greatly slow the propagation, perhaps to the point that it could be detected before too much damage is done, but I could see it going unnoticed for a while, depending on which crates it infected. Note that notifications would not help here, because the worm is waiting for the crate owner to publish the crate themself, so the notification would be expected.
That could work for TOTP, albeit with the severe limitations that you mentioned. But FIDO2/WebAuthn? No — that's inherently impossible to intercept.
Forgive me for not understanding exactly how hardware keys work, but outside of the sandbox of a browser, what protection is there against imitating cargo
? Inside a browser, you can incorporate the domain into the challenge and use HTTPS and the fact that the key can only be accessed through the interface that the browser provides to ensure that phishing is not possible. For a command line utility (especially one that is open source and possibly compiled from source by the user or their distribution), I don’t know how that would work.
FIDO2/WebAuthn relies on public key cryptography. Unless you're managing to get cargo's private key[1], there's no way to impersonate cargo in that capacity. You'd only be able to authenticate with the middle man, who would then be unable to pass the credential on to cargo itself.
-
once it's implemented, of course ↩︎
Cargo is a local binary though, it has nowhere to store a private key that could be used in a challenge that couldn’t just be stolen by a worm.
Also as I understand it if you setup a FIDO2/WebAuthn key for a domain in one browser, it will work just fine in another browser. The website itself will send the data that is combined with the key's private seed to generate the private key combined with some random data for the challenge and the browser combines this random data with the domain name to form a challenge that the security key will sign. A native program can pass any challenge it wants, including the one crates.io expects.
Some OSes do have an option to only allow programs signed with a specific to sign the challenge with a specific private key, but in that case the authenticator is built into the cpu (a TPM) and is not a separate security key.
Regardless of security of cargo
executable and U2F, it's possible to inject code into source files on disk, potentially just before Cargo packages them. But the important thing is that it does throttle spread of malware, because malware can't just run cargo publish
itself, it has to wait for user to approve a release.
The FIDO U2F overview has a decent explanation of how U2F keys work. You're correct in saying that a local cargo
binary acts as a browser in this situation, and can add payload to each crate that you publish, but it will still act as a massive speed-bump for propagation, since if you (e.g.) malware a machine belonging to a user with publish rights to 147 crates, you will only be able to publish tampered crates as and when they attempt to publish untampered crates from the infected machine, and not propagate to all 147 crates they have control of in one operation as soon as you infect their host.
Note that because you need to trigger all of the user's registered keys to avoid suspicion, and you need to include whatever the site's reason is for asking to use the security key in your requests to the key, you can't even wait for the user to update one crate and worm all their crates in one go, since they're likely to get suspicious if they expect a key to say "Authorize crates.io to: publish 2 crates: linkme, linkme-impl" and instead get "Authorize crates.io to: publish 147 crates: anyhow, argv, asmdb, async-trait, …". Instead, you have to wait for them to upload each crate in turn from an infected machine to stay undetected.
All the U2F keys I have seen have no UI but a light. And the reason text, if any, would be generated or at least processed by the same cargo
code that we are supposing has been infected, so there is no guarantee it is correct. The key does not itself have a secure connection to the server.
The only protection against bulk uploads that can actually happen via U2F (if I understand correctly) would be if crates.io issued a challenge for every individual package upload, thus making the user suspicious of multiple requests, but also making their finger tired if they want to upload lots of packages. That might in fact be the situation if U2F was naively added right now, though, since there is no provision for bulk uploads anyway (at least in the command line UI).
There's only one 2-factor key I've seen with a UI in the high-assurance domain on the token, the reader issued by my bank. That is not U2F though. And what part of the standardized protocol would be used here anyways? The only ctap operation is an HMAC signature of a client provided bag-of-bytes (aka. authenticatorGetAssertion
). This is leveraged by signing a cryptographic hash of the message into message authentication. But to make the UI useful to the user it is necessary that the contents of the display are bound to the contents of the message, somehow. That's not really feasible with the standardized CTAP operations, afaik.
You can have multiple U2F factors registered to an account - for example, I have both the embedded U2F key in my Android phone, and a YubiKey. While the YubiKey acts as you describe, with just a light, the Android embedded one also displays verification text - this is why I mentioned that you "need to trigger all of the user's registered keys to avoid suspicion", as if you did sort keys by type, you could ignore the Android phone and just ask for the YubiKey.
And the second part of the reason text lives in the signed data (it's called the "Transaction Content" in the current U2F spec), and is thus server-controlled and verified. An "uncompromised" setup would have the "Authorize crates.io to: " component of the text generated by cargo
, and the remainder coming from the server. A compromised cargo
could change the first part to "Authorize crates.io to: publish 2 crates: linkme, linkme-impl ", but you'd then have the key display "Authorize crates.io to: publish 2 crates: linkme, linkme-impl publish 147 crates: anyhow, argv, asmdb, async-trait, …", which is also unexpected and weird.
Yes, this depends on enough users being paranoid enough to have a key with the ability to display the text, and on those users reading the text on the key that displays it (not just tapping their Yubikey).
You could also go for the paranoid option in U2F setup for cargo publish
- U2F gets you a single-use key that appears on your crates.io account permanently, and that's usable only to publish one new version of the named crates. Then, the malware is also restricted by the fact that if it does grab an over-broad key, when I look at crates.io, I'll be informed of this fact.
It can use a credential helper, which can store the key any number of places, for example in an OS keychain or on an external hardware token (or in the case of a CI system, in an external Key Management Service), often with an accompanying AuthN/AuthZ policy: using the key may require user interaction such as responding to an OS dialog and/or entering a password/PIN.
Malware can subvert any of these mechanisms, but it does still guard against (non-RCE) local file disclosure attacks, which Cargo credentials are currently vulnerable to (although in practice LFD is often an RCE vector).
That's what "Transaction Content" in the FIDO UAF 1.2 specifications is for - it's something the key displays, and it's part of what's signed. If I tamper with transaction content on its way to the key, then I get a signature the server can't verify.
How does this map to hardware (under attack in the threat as discussed here), that's why I'm wondering about the CTAP details. Here we are considering client compromise except for the token itself. Iirc the UAF covers every but this part, assuming that the browser agent properly shields access to the token channel to provide it. Or as the specification puts it:
In the case of Transaction Confirmation only the transaction confirmation display component implementing WYSIWYS needs to be trusted,
Without further elaborating on guidance as to how that would be achieved, this is a requirement that the agent must figure out. So really the question would be: how does cargo achieve it, in the thread's security context where the computer itself is compromised, which is a very different threat model from the browser?
Windows is one of the few places that seem to do this well, when I use my security key for ssh it pops up a dialog:
(Likely this can be spoofed as well by showing your own dialog box and talking directly to the key, bypassing the system APIs, but assuming the first line in this message is from the actual transaction it at least makes it more effort than just changing a string output by the binary itself).
FIDO UAF 1.2 includes a CTAP component as part of the specification. The transaction content is an optional TLV in the TAG_UAFV1_SIGN_CMD message sent by the client to the authenticator; if present, the authenticator MUST include it in the data that's covered by the assertion (the signature), and it can optionally display the transaction content to the user - I've checked, and the Android implementation does display the transaction content from a request I send over Bluetooth.
If Microsoft have followed the FIDO U2F specifications when implementing this, then you cannot talk directly to the key from userspace, because you don't know the Key Handle you need to use to get the key to spit out a valid response.
In case of a "First Factor (1stF) Roaming Authenticator", the key handles are stored on the security key itself. Either using this feature or manually copying the key handle is required to allow publishing from multiple systems without enrolling every system on which you want to publish individually. When using this feature userspace can directly talk to the key even without the key handle.
This is a reason why I immediately revoke my cargo login token after release.
The new publish scopes do somewhat limit the effectiveness of such a worm, in that (if used) the worm would (ideally) only be able to publish a single crate (per instance of being run) rather than all crates under ownership of the same publisher.
It's not a full prevention, but a reasonable mitigation available for people to use. If the token is additionally only provided when the publish is expected (rather than ambiently), the window of attack shrinks more. If the pre-publish verify is run without access to the token, then the token could be entirely isolated from running any code with access to it.
The next step would imo be the building of a standard CI flow capable of doing such controlled publish access. (e.g. like cargo-dist manages everything for publishing bins, but for libs instead.) Doing so for GHA would be accessible to most (but not all) published crates. If you can make it more convenient than existing options in addition to more secure, it should easily be able to gain mindshare.
(If anyone is serious about building this out, I'd ask axo.dev if they'd be willing to integrate crates-io lib distribution as part of cargo-dist, and work with them to design & integrate it there, as well as potentially to improve token isolation in the bin dist workflows also. Having everything available through one access point is easily the most convenient. cc @Gankra, if you check this forum any.)