Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: x509 extension registry #454

Closed
MarcoPolo opened this issue Sep 15, 2022 · 15 comments
Closed

proposal: x509 extension registry #454

MarcoPolo opened this issue Sep 15, 2022 · 15 comments

Comments

@MarcoPolo
Copy link
Contributor

MarcoPolo commented Sep 15, 2022

This proposal is in the similar vein as #450 but can be applied to our TLS security layer. All points in that issue apply here as well.

The general problem is that in some cases we want to pass data early. By doing this in the security handshake we can save some time and potentially round trips (see #446). Another example is passing in the list of supported protocols early. This is also something that protocol select #349 would make use of.

The way we'd include this early data is by attaching another extension to the x509 certificate. Similar to the libp2p public key extension we already do today. We could use the Object identifier number 1.3.6.1.4.1.53594.2.1 with the last 2 distinguishing it from the existing public key extension. The 53594 is allocated to libp2p by IANA. This data should be a varint prefixed (maybe multicodec) protobuf (e.g. `). The specific protobuf can be defined by this spec, but should be in line with the protobuf used in #450 so that we can reuse this definition in multiple places (and we get the same benefits whether we are on top of noise or tls). Even if some things in the registry only currently make sense within Noise (e.g. webtransport certhashes), the benefits of having one unified registry outweigh the costs of potentially unused fields.

Why not ALPN?

A solution to #446 is to use the ALPN to negotiate the muxer. This works well, but it means our ALPNs will be sent in clear text and can serve as a way to identify the TLS handshake as a libp2p handshake.

ALPN also works well for solving the problem specific to #446, but doesn't generalize to our other problems. ALPN is meant to negotiate a protocol rather than exchange data.

Using this registry will allow us to unify this logic with Noise. Thus presenting a unified abstraction on top of our two security layers.

@MarcoPolo
Copy link
Contributor Author

@julian88110 take a look at this. This proposal would mean that we should prefer to send the early muxer info in this registry rather than the ALPN.

@julian88110
Copy link
Contributor

julian88110 commented Sep 15, 2022

Hi Marco,
thanks for the new proposal. If I read this correctly, the new registration is specific to TLS, not a unified TLS/Noise solution, right? As security regarding muxer type is not a serious concern, (we already sending them in the clear for noise early data), any other benefits we gain on this approach?

@MarcoPolo
Copy link
Contributor Author

MarcoPolo commented Sep 16, 2022

the new registration is specific to TLS, not a unified TLS/Noise solution, right?

This is meant to be the TLS part of the solution. The Noise part is #450. In both cases we are answering "how do we share data in the security layer?".

(we already sending them in the clear for noise early data)

I don't think we should use the payload in the first message of the XX noise handshake unless we need to. I think for early muxer negotiation we should use the 2nd and 3rd message. Even though we don't consider supported muxers private information, we should still avoid passing data that can identify the connection as libp2p in plaintext. Some more discussion here: #453 (comment)

As security regarding muxer type is not a serious concern, any other benefits we gain on this approach?

A unified way to send early encrypted data between TLS and noise. We can then add our extensions (e.g. send a list of supported protocols) to this registry and get it working on both TLS and Noise with minimal changes.

w.r.t #446 this allows us to not commit ourselves to using the ALPN and thus not identifying the connection as a libp2p connection in plaintext.

@julian88110

@marten-seemann
Copy link
Contributor

Thank you for this excellent writeup @MarcoPolo!

To add a bit more context here: The better solution would be to a proper TLS extension, not an extension to the x509 certificate. We'd then be able to specify which handshake messages to attach data to. Unfortunately, most TLS implementations don't allow easy access to TLS extensions. They use TLS extensions internally, but don't expose them to the application layer. By (ab-) using the certificate, we gain easy access to the information.

We could use the Object identifier number 1.3.6.1.4.1.53594.2.1 with the last 2 distinguishing it from the existing public key extension. The 53594 is allocated to libp2p by IANA. This data should be a varint prefixed (maybe multicodec) protobuf (e.g. `). The specific protobuf can be defined by this spec, but should be in line with the protobuf used in #450 so that we can reuse this definition in multiple places (and we get the same benefits whether we are on top of noise or tls).

Not sure if we need any prefixing. We can just define 1.3.6.1.4.1.53594.2.1 to mean "the protobuf we specified here". If we ever want to switch to a different, non backwards-compatible encoding, we can use 1.3.6.1.4.1.53594.3.1.

A solution to #446 is to use the ALPN to negotiate the muxer. This works well, but it means our ALPNs will be sent in clear text and can serve as a way to identify the TLS handshake as a libp2p handshake.

ALPN also works well for solving the problem specific to #446, but doesn't generalize to our other problems. ALPN is meant to negotiate a protocol rather than exchange data.

I have mixed feelings about approach. It feels wrong to roll our own solution given that there's a widely accepted and implemented RFC for this problem: ALPN. Despite that, I'm leaning ever so slightly towards using the proposed extension registry. There's certainly the advantage of having this information encrypted (without relying on ECH, which might or might not be implement by the standard library). Being consistent with Noise is nice as well.

@julian88110
Copy link
Contributor

Agreed, this is a clever idea. The one concern I have is using a certificate extension for protocol selection purposes, that is not a straightforward function alignment. We run the risk of turning a certificate extension into a general purpose carrier.

@MarcoPolo
Copy link
Contributor Author

We run the risk of turning a certificate extension into a general purpose carrier.

Just to be clear, what's the issue with doing that?

@julian88110
Copy link
Contributor

Thanks to everyone taking time to review and discuss this. As we synced briefly today, here are some thoughts on the options of using TLS extension instead. Given the limited time and exposure to the code base, I am sure I might make some inaccurate assumptions, please correct me if I do. Hoping to provoke more thoughts that can drive this open issue to close soon.

  • Option A :
    TLS extension the formal way, where we get an IANA allocation and formally adopted, this might be a very remote shot, so I would not go into more details unless we want to pursue this path.

  • Option B:
    Customize TLS implementation / lib we use.
    This is feasible in some cases, for go, we have more control, for other languages, for example c++, rust, we need to revise the library and maintain the customized library going forward. I see this is a big commitment.
    And for web browser based nodes, I am not sure I see a clear path for using TLS extensions, the nodes in the web may have very limited ability to customize TLS behavior.
    Any other options ?

Thanks!

@MarcoPolo
Copy link
Contributor Author

MarcoPolo commented Sep 20, 2022

I wonder if we can do something simpler here. In TLS 1.3 the server can send application data in the second handshake message (which is what we're doing in Noise). We can have the server send this encoded registry in the second handshake message.

The hard part here is that the server has to know that the client can handle this application data. Using ALPN could help, but could make it easy to identify libp2p connections. We could set the ALPN to some commonly used values to signify this version (it would be a hack; but it would mean our initial TLS handshake is indistinguishable from any other common TLS connection).

@marten-seemann
Copy link
Contributor

marten-seemann commented Sep 20, 2022

TLS Extensions

@julian88110

TLS extension the formal way, where we get an IANA allocation and formally adopted, this might be a very remote shot

I don’t think we need formal adoption to get a TLS extension code point. Registration should be pretty straightforward, https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml says:

The role of the designated expert is described in [RFC8447]. The designated expert [RFC8126] ensures that the specification is
publicly available. It is sufficient to have an Internet-Draft (that is posted and never published as an RFC) or a document from another standards body, industry consortium, university site, etc. The expert may provide more in-depth reviews, but their approval should not be taken as an endorsement of the extension.

That said, it seems like the only way to attach extensions to the handshake in an encrypted way is Encrypted Extensions. Apparently there's no way for the client to send encrypted extensions (other than ECH at some point in the future).

0.5-RTT data

I wonder if we can do something simpler here. In TLS 1.3 the server can send application data in the second handshake message (which is what we're doing in Noise). We can have the server send this encoded registry in the second handshake message.

This is what's commonly referred to as 0.5-RTT data, right? Technically, this wouldn't be data sent in TLS, but this would already be part of the encrypted byte stream that TLS provides. I don't think that crypto/tls allows us to do that though (QUIC would, as we could easily add an API to quic-go).

The hard part here is that the server has to know that the client can handle this application data.

For the client, this would be indistinguishable from normal application data:
Of course, this causes problems as well:

  • the client expects to jump into a multistream handshake
  • there's no reliable of telling if the server sent any data or not (i.e. unclear upgrade path)

Note that this is getting very close to what Protocol Select is trying to do, where both sides send a list of supported protocols and we decide for one by intersecting these lists. It would be really nice to avoid this.

Using ALPN could help, but could make it easy to identify libp2p connections. We could set the ALPN to some commonly used values to signify this version (it would be a hack; but it would mean our initial TLS handshake is indistinguishable from any other common TLS connection).

I'm really happy we didn't use this hack (setting ALPN to HTTP) when we first rolled out libp2p TLS, otherwise optimizations like this one wouldn't be possible now.

@marten-seemann
Copy link
Contributor

I propose thinking about this problem from the other side.

QUIC

Let’s first analyze QUIC, which is (once we have proper Happy Eyeballs-style dialing) our most common and most performant transport (>90% of connections). Would we want to use an x509 extension or a TLS extension with QUIC? No! TLS allows us to use 0.5-RTT data, so the server can just open streams and send data right after receiving the first packet from the client. In particular, the server could open the Identify stream right away and send its list of supported protocols.
Putting anything into the handshake would be counterproductive because 1. it increases the size of the server’s first flight (which is limited by the 3x amplification limit), 2. it increases the likelihood of on of the server’s handshake packets getting lost, and 3. the extensions would introduce HoL blocking among each other.
So it seems like we don’t have any problem to solve in our most common case. This is good news.

TLS

There are 2 options:

  1. Put the muxers into ALPN or
  2. Generalize the NoiseExtension and send it in a x509 or TLS extension.

Having the muxers in ALPN would allow the server to choose the muxer right after receiving the client’s first flight. It could then start opening streams and send application data in 0.5-RTT data, exactly like QUIC does.

Putting muxer information in an x509 or TLS extension would cost us an entire roundtrip, since the server would have to wait until completion of the handshake. We can work around this by putting application data into the x509 extension (like the list of Identify protocols), but it would definitely be nicer to just use a regular libp2p stream.

It’s not clear if crypto/tls supports 0.5-RTT data, and if it’s worth the effort forking the standard library to add this feature. The nice thing is that this can be added in the future and is completely backwards-compatible.

Noise

Noise XX doesn’t allow sending of 0.5-RTT data, so the round trip we’re saving with QUIC’s 0.5 RTT data is already lost. We wouldn’t gain anything by attaching the NoiseExtensions to the 1st and 2nd flight, so we send it encrypted in the 2nd and 3rd flight of the handshake.

This is not ideal, but all we can hope to achieve with XX. We might want to consider offering a different handshake pattern in cases where we’re using inlined keys. That would allow us to send 0-RTT and 0.5-RTT data in those cases.

What about censorship resistance?

First of all, are we the only ones concerned about sending values (SNI, ALPN) unencrypted in the ClientHello? Clearly we aren’t, the IETF has been working on ECH (Encrypted ClientHello) for a few years now. It doesn’t seem like a crazy idea to live with the current situation for a little bit longer, until the RFC is published and implementations have caught up.

For immediate solutions, WebTransport and WebSocket Secure. Both protocols use an HTTPS connection for the WebSocket / WebTransport upgrade request, so they’ll naturally (and correctly!) use the HTTP / H3 ALPN. There’s a slight cost of using WebSocket / Webtransport instead of TCP / QUIC, but that’s a small price to pay if you’re on a censored internet connection. And only until ECH is widely deployed.

@julian88110
Copy link
Contributor

@marten-seemann thanks for the nice summary and comprehensive review.
I believe after considering all the factors, including library support, backward compatibility, future maintenance commitment, and more importantly protocol integrity and function separation, ALPN is the less evil choice, at least for the current situation. what are everyone's thoughts on this?

@MarcoPolo
Copy link
Contributor Author

After a synchronous discussion with @julian88110 and @marten-seemann here's our shared understanding:

Instead of thinking "How can we make tcp+tls look like Noise" maybe we should consider "How can we make tcp+tls look like QUIC". QUIC is the most common transport right now between go-libp2p nodes, and will be even more common in the future.

With QUIC the server can send encrypted data to an unauthenticated client after 2nd handshake message. It can open new streams and run user protocols right away (with the caveat that the client is not yet authenticated). In this case, we don't need to define a new "Extension Registry" that defines a new format of sharing data to the client. We can use all our existing abstractions and methods of sending data (with the caveat, again, that this data is sent to an unauthenticated peer).

We can make tcp+tls behave similarly if we negotiate which muxer to use. If the server knows which muxer to use, it can go ahead and open a new stream after the 2nd handshake message and open new streams and run user protocols right away (with the same caveat the the client is not yet authenticated).

The benefit, again, is that existing user protocols don't need to change a whole lot. They just need to say they're okay sending this data to an unauthenticated party, but otherwise open and use a stream like normal. Contrast this to an "Extension Registry" where each protocol would have to register a code point and use this protobuf in order to make use of this early encrypted yet unauthenticated data.

The easiest and standard way of defining which muxer the server should use in tcp+tls is to have the client negotiate this with ALPN. This is the intent behind ALPN so we aren't doing anything non-standard.

The current recommendation then is to negotiate the muxer of ALPN and use the existing stream abstractions to send early-encrypted-yet-unauthenticated data. And to not implement a x509 extension registry or a TLS extension for this extension registry.

To quickly summarize this issue and its points:

  • "ALPN doesn't generalize to our other problems"
    • We should prefer to re-use existing abstractions rather than make a new one via the extension registry. By negotiating the muxer in the ALPN, the server can go ahead and open streams after the second handshake message.
  • "An extension regstiry will allow us to unify this logic with Noise"
    • What we really mean here is "unify this logic with Noise XX + tcp". This may not apply if we used a zero-rtt IK handshake.
    • At the expense of not unifying this logic with the more common (and faster) QUIC.
    • Out of scope to this issue, but may be worth considering if we should attempt to make this early open stream semantics work in Noise XX + tcp.
  • "Protocol select protocol-select/: Add Protocol Select specification #349 would make use of this"
    • We can do this with early unauthenticated streams which are sent in after the second handshake message from the server.

I hope this is a good summary of our discussion on a good overview of how we ended up with the decision to negotiate the muxer via ALPN. Please let me know if I'm missing something.

@mxinden
Copy link
Member

mxinden commented Sep 21, 2022

The current recommendation then is to negotiate the muxer of ALPN and use the existing stream abstractions to send early-encrypted-yet-unauthenticated data. And to not implement a x509 extension registry or a TLS extension for this extension registry.

Reasoning and plan makes sense to me. Thanks @MarcoPolo for the summary and thanks everyone for exploring this in depth.

@BigLep
Copy link
Contributor

BigLep commented Sep 25, 2022

@MarcoPolo : I didn't read in depth, but in quickly skimming, it wasn't clear to me what the next steps are for this issue. Can you specify them if they aren't already stated?

@MarcoPolo
Copy link
Contributor Author

This issue was original created to propose an extension registry for x509 certs. After discussion, we don't think we want this. So I'm closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants