Skip to content

Cargo mTLS registry authentication#3907

Open
matthague wants to merge 9 commits into
rust-lang:masterfrom
matthague:master
Open

Cargo mTLS registry authentication#3907
matthague wants to merge 9 commits into
rust-lang:masterfrom
matthague:master

Conversation

@matthague

@matthague matthague commented Jan 17, 2026

Copy link
Copy Markdown

This is an RFC aimed at allowing Cargo to present client certificates when forming HTTP connections and support mutual TLS authentication with registries.

It is aimed at forming consensus around the discussion in rust-lang/cargo#10641.

Rendered

@Noratrieb Noratrieb added the T-cargo Relevant to the Cargo team, which will review and decide on the RFC. label Jan 17, 2026
// Response kind: this was a TLS client identity request
"kind":"tls-identity",
// Base64 byte buffer containing the binary content of your client certificate (empty if unset)
"cert_blob":"aGVsbG8...gd29ybGQ=",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would supporting raw public keys be an option? That way a registry could just store the public key as a token in the database and doesn't need to parse a client certificate and do a whole bunch of checks to see if the certificate is valid, making it much more robust.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This RFC is intended to allow Cargo to present client certificates, not change how the certificates are processed or validated by the server.

In my use case, a registry is being reverse-proxied by a web server like Nginx, Apache, HAproxy, etc... to provide additional features like SSO, SCIM and LDAP in addition to client identity verification.

Using raw public keys wouldn’t support this reverse-proxy use case, since it would require changes to how the proxy validates the client certificates.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean having the option for the credential provider to provide raw public keys in case a registry wants to support this for robustness or any other reason. If you use client certificates to authenticate specific clients rather than just any client who gets a certificate from the CA, then whichever side channel is used to communicate the identity of the client from the reverse-proxy to the registry can pass a the raw public key too, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I was misunderstanding your previous comment. I think we could add an additional field that allows that, what did you have in mind?

// The format of your client certificate. With the current curl-based backend, supported formats are “PEM”, “DER”, and "P12" (empty for backend default)
"cert_type":"PEM",
// Base64 byte buffer containing the binary content of your private key (empty if unset)
"key_blob":"aGVsbG8...gd29ybGQ=",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already mentioned this on Zulip and probably should be omitted in the first version, but it might be nice to support credential providers that can't export the private key but rather act as a signing oracle to support for example TPM or smartcard backed certificates. Rustls should support that through the SigningKey trait.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would definitely be a nice use case to support, and as you point out, it looks like rustls has support for this, and I also believe libcurl can be persuaded into doing this.

I don’t know enough about how these signing request flows work to propose a sensible protocol, so that’s mainly why I’ve omitted it here. I’d be happy to consider adding it to this RFC, but also agree that it’s a somewhat independent feature that could be added in a later version.

// Base64 byte buffer containing the binary content of your client certificate (empty if unset)
"cert_blob":"aGVsbG8...gd29ybGQ=",
// The format of your client certificate. With the current curl-based backend, supported formats are “PEM”, “DER”, and "P12" (empty for backend default)
"cert_type":"PEM",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need to support PKCS#12 format certificates? The credential provider could just convert it to a PEM certificate + separate private key, right? If you are using PKCS#12 to support private key encryption, the credential provider did have to parse the PKCS#12 file anyway to decrypt the private key as cargo doesn't know the password.

Also I don't think there should be a backend dependent default for the type. Either it should be automatically detected from the file header or it should be always explicitly specified.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we need to support PKCS#12, it was listed as a format that libcurl already supports so I listed it too.

If we’re fine explicitly specifying the format a PEM certificate + separate signing key would make sense. It can be on the credential provider to convert objects into that format.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for libcurl to load a password-protected P12 file you'll have to supply CURLOPT_KEYPASSWD as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3a16df3 now specifies that the certificates and keys are in PEM format

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A PKCS#8 key can also be password-protected. The need of password is not really a question of PEM vs DER vs P12. (We could just say we don't support encrypted private keys)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll add a section saying that this feature won’t support encrypted private keys, and that it’s the credential provider’s responsibility to take care of decryption if necessary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d1806de adds a section on certificate and key field formats, and specifies that encrypted private keys are not supported

Comment thread text/3907-mtls-registry-authentication.md Outdated
"kind":"tls-identity",
// Client certificate chain in PEM format (empty if unset)
"certificate":"-----BEGIN CERTIFICATE-----
[Base64 encoded client certificate data]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have to use the \n escape for new lines in json, you can't just have a literal newline.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ae27ce7 specifies that newlines have to be escaped

@Eh2406 Eh2406 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some first impressions from a first read.

"kind":"tls-identity",
// Registry information (see https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html#registry-information)
"registry":{"index-url":"sparse+https://registry-url/index/"},
// Additional command-line args (optional)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do these args represent? Where do they come from?


## Certificate and key formats

The `certificate` and `key` fields are expected to correspond to the same TLS client identity. If a credential provider is unable to supply a usable client identity, it may return empty fields.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When should it be returning "empty fields" vs "Err":{"kind":"not-found"}?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One data point from how the token path handles this today: Cargo's provider chain treats url-not-supported and not-found as fall-through and tries the next configured provider, and only errors if no provider produced a result. If "no identity" is represented by empty fields in an Ok response, a multi-provider configuration behaves differently for identities than for tokens: the chain stops at the first provider even though it had nothing.

I'd suggest not-found for "this provider has no identity for this registry/origin", url-not-supported for "this provider doesn't handle this registry at all", and treating empty certificate/key fields in an Ok response as a protocol error. That keeps fallback semantics identical across request kinds, which also matters for mixed deployments where one provider serves tokens and a different one serves TLS identities.


The `certificate` field contains the client certificate chain in PEM format, with newlines escaped using `\n`. If multiple certificates are present, they are expected to be concatenated PEM blocks.

The `key` field contains the private key corresponding to the client certificate, in PEM format, with newlines escaped using `\n`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every Cryptography expert I've ever talked to has strong opinions about PEM. The fact that it is widely used means that it's widely supported, but that doesn't necessarily mean we should start using it. I would want opinions from Rust Crypto before making this decision.

Authentication at the TLS level is different from the token-based methods for [Registry Authentication](https://doc.rust-lang.org/cargo/reference/registry-authentication.html) exposed by the [Credential Provider Protocol](https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html) since it takes place before the connection to the registry is established. It is not currently possible to write a [credential plugin](https://doc.rust-lang.org/cargo/reference/registry-authentication.html#credential-plugins) that enables this type of authentication with a registry, but an extension to that protocol would make this possible.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is our guidance on how registries should implement authentication? Should registries use MTLS? Should registries use tokens? Should registries use both? If the answer is, as it probably is, "It's complicated" then what are the factors that would drive that decision?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One operator data point: in my deployment they answer different questions and coexist. mTLS is the network-layer control: an identity-aware proxy (Teleport in my case, but Cloudflare Access or plain nginx ssl_verify_client are the same shape) decides whether the workload may reach the registry at all, before any HTTP request is made. The registry token remains the application-layer control: which repositories, read versus publish. Suggested guidance along those lines: mTLS authenticates the connection or workload, tokens authorize registry operations; a registry deployed behind an identity-aware proxy typically needs both, while a registry that terminates TLS itself and maps certificates to accounts may use mTLS alone. The deciding factor is whether the system checking the certificate and the system checking authorization are the same one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do identity-aware proxies forward the identity to the target application? If so this identity forwarding can take the place of the user provided token, right?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, potentially. I think there are two separate axes here: how identity reaches the registry, and which operation or artifact is being authorized.

There are three identity-integration models:

  1. Admission only: the proxy authenticates the client, but the registry does not consume the forwarded identity.
  2. Direct assertion: the registry validates the proxy's signed identity assertion and maps its subject, roles, or traits to registry principals and permissions.
  3. Token exchange: an auth service validates the forwarded identity and issues a registry-native token with the appropriate audience and scopes, roughly the pattern standardized by RFC 8693.

A pattern I've used and seen across projects is to allow ordinary package downloads without application-layer credentials because the registry endpoint is already protected by an identity-aware access layer. Those reads are anonymous to the registry, but they are not anonymously reachable.

Publish, yank, owner-management, and administrative operations still require application-level authorization. That could come from a user-supplied registry token, a directly consumed proxy assertion, or a registry token derived from that assertion.

Some reads also require application authorization. For example, software with export-control or other data-classification restrictions might only be readable by a particular team. If multiple classifications exist behind one registry endpoint, the registry must consume a trusted identity and enforce repository- or artifact-level read policy; admission through the outer proxy alone is not granular enough.

Teleport supports identity forwarding by sending a signed Teleport-Jwt-Assertion on upstream requests, including the user's roles and traits. It can also rewrite Authorization to Bearer {{internal.jwt}}, although that overrides any client-supplied Authorization header. Teleport JWT docs and header-rewrite docs.

So yes, forwarded identity can replace the Cargo-provided token when the registry understands it. But it does not remove the need for application authorization: whether another credential is needed depends on the operation, the artifact's classification, and whether the registry can map the forwarded identity to sufficiently granular policy.

@ehuss ehuss moved this to RFC needs review in Cargo status tracker Jan 20, 2026
@cazlo

cazlo commented Jul 11, 2026

Copy link
Copy Markdown

I have a concrete production use case for this RFC: a private Cargo registry served through Teleport Application Access. Teleport's tbot agent writes a short-lived PEM client certificate and key to disk and renews them automatically before expiry. The registry continues to use a token for repository-level authorization, so mTLS and the existing token flow need to coexist. This shape is not Teleport-specific. SPIFFE's spiffe-helper and Vault Agent's PKI templates produce the same rotating PEM certificate and key files, so the credential-provider design here fits the broader workload-identity ecosystem well: a small provider just reads the current files.

Short-lived identities expose one gap in the current draft: the identity is "used for subsequent communication to the same registry (within the current Cargo session)", but the response carries no expiry or refresh signal. A tbot-issued certificate can expire in the middle of a large fetch or a long-running invocation. Could the tls-identity response reuse the existing CacheControl model from the token get response ("cache":"never", "cache":"session", or "cache":"expires" with a sibling "expiration" unix timestamp)? That would make the refresh policy explicit and testable: Cargo uses the cached identity until expiration, and the first time it needs a new connection after that, it sends a fresh tls-identity request instead of handshaking with a stale certificate.

For prior art: I recently built a proof-of-concept implementation of the equivalent feature for Go's open proposal golang/go#30119 (golang/go#80371). It adds an origin-scoped GOAUTH=mtls method aimed at the same style of deployment, targeting a future Go release. Client certificates for native registry traffic are already supported by npm, Yarn, pnpm, pip, uv, Maven, Gradle, NuGet, Composer, Bundler, and Conan.

I'm happy to build a prototype credential provider against the v2 protocol and help test the rotation and scoping cases.


Credential providers will be able to provide client certificates and private keys to Cargo via new request and response messages.

Cargo will issue a tls-identity request when configuring an HTTP client for a registry, and the returned identity will be used for subsequent communication to the same registry (within the current Cargo session).

@cazlo cazlo Jul 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The same registry" can involve several HTTPS origins: the sparse index URL, the dl URL from the registry's config.json (often a CDN on a different host), and the API URL used for publish, yank, owner, and search. I think the RFC should define which origins are authorized to receive the identity,
because the default behavior of the current implementation surface would be broader than any of them: on current master (0a28f7930c), registry traffic goes through the shared http_async client, and every request handle sets follow_location(true), so libcurl follows redirects internally. A certificate configured on that handle is presented to every redirect hop, including cross-origin ones. libcurl's documented cross-host protections for Authorization headers do not cover TLS client certificates, which are selected at handshake time.

Note that Cargo's token behavior already faces this question: with auth-required, the Authorization header is sent to whatever origin the dl template resolves to. I think the identity design should be stricter than that precedent, since a certificate is presented at handshake time to every connection made by the handle rather than attached to individual requests.

There is a second isolation question in the same place: all easy handles share one curl multi and its connection cache. With two registries using different identities in one invocation, connection reuse and HTTP/2 multiplexing must never match a connection that was established under a different client certificate. curl has had this bug class twice: CVE-2021-22924 (connection reuse matched despite differing certificate-related config) and CVE-2022-27782 (TLS settings omitted from the reuse check entirely, fixed in 7.83.1). The second matters here because Cargo often links a system libcurl older than the fix, so it seems worth an explicit test rather than an assumption.

In the Go proof of concept linked above I scoped the certificate to one canonical HTTPS origin, used a distinct transport per identity so pooling cannot cross identity boundaries, and re-evaluated selection on every redirect hop. For Cargo the options seem to be: authorize only explicitly mapped origins, re-evaluate identity per hop by handling redirects in Cargo itself, or reject cross-origin redirects while an identity is loaded. Any of those can work; I mainly think the RFC text should pick one rather than leave it to the implementation.

View changes since the review

}}
```

## Certificate and key formats

@cazlo cazlo Jul 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Zulip thread the question came up whether HSM support via ssl_engine would lock Cargo into curl/OpenSSL. The same backend portability concern already applies to this response format on stock Windows builds today: Cargo's Windows libcurl is usually built with Schannel (see the comment in
src/cargo/util/network/http.rs), and libcurl documents that CURLOPT_SSLCERT_BLOB under Schannel requires PKCS#12, while CURLOPT_SSLKEY_BLOB is supported only by OpenSSL and wolfSSL. As drafted, the separate PEM certificate and key fields have no obvious implementation path on the default Windows toolchain.

I think the RFC should pick one of:
(a) Cargo converts the PEM pair in memory to whatever the backend needs (PEM to PKCS#12 for Schannel)
(b) an intentional, documented platform limitation with a clear fail-closed error, since silently continuing without client authentication would be the worst outcome
(c) a format field negotiated per backend behind the protocol.

Related: since a signing-oracle flow (the provider signs a digest and the key never leaves an HSM) is the likely future answer to the TPM/smartcard discussion above, it may be worth wording the response so that key is not structurally mandatory forever, for example "exactly one of key or a future signing capability", so protocol v2 can grow that without a breaking change.

View changes since the review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-cargo Relevant to the Cargo team, which will review and decide on the RFC.

Projects

Status: RFC needs review

Development

Successfully merging this pull request may close these issues.

9 participants