-
Notifications
You must be signed in to change notification settings - Fork 279
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
libp2p TLS 1.3 Handshake #151
Changes from all commits
18a009f
f35dae6
0437cb7
565f205
15e57e7
2f901ae
1a5e6eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Design considerations for the libp2p TLS Handshake | ||
|
||
## Requirements | ||
|
||
There are two main requirements that prevent us from using the straightforward way to run a TLS handshake (which would be to simply use the host key to create a self-signed certificate). | ||
|
||
1. We want to use different key types: RSA, ECDSA, and Ed25519, Secp256k1 (and maybe more in the future?). | ||
2. We want to be able to send the key type along with the key (see https://github.com/libp2p/specs/issues/111). | ||
|
||
The first point is problematic in practice, because Go currently only supports RSA and ECDSA certificates. Support for Ed25519 was planned for Go 1.12, but was deferred recently, and the Go team is now evaluating interest in this in order to prioritze their work, so this might or might not happen in Go 1.13. I'm not aware of any plans for Secp256k1 at the moment. | ||
The second requirement implies that we might want add some additional (free-form) information to the handshake, and we need to find a field to stuff that into. | ||
|
||
The handshake protocol described here: | ||
* supports arbitrary keys, independent from what the signature algorithms implemented by the TLS library used | ||
* defines a way how future versions of this protocol might be negotiated without requiring any out-of-band information and additional roundtrips | ||
|
||
|
||
## Design Choices | ||
|
||
### TLS 1.3 - What about older versions? | ||
|
||
The handshake protocol requires TLS 1.3 support. This means that the handshake between two peers that have never communicated before will typically complete in just a single roundtrip. With older TLS versions, a handshake typically takes two roundtrips. By not specifying support for older TLS versions, we increase performance and simplify the protocol. | ||
|
||
|
||
### Why we're not using the host key for the certificate | ||
|
||
The current proposal uses a self-signed certificate to carry the host's public key in the libp2p Public Key Extension. The key used to generate the self-signed certificate has no relationship with the host key. This key can be generated for every single connection, or can be generated at boot time. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO another reason for not using the host key directly is to achieve Perfect Forward Secrecy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TLS 1.3 uses ephemeral Diffie-Hellman for the key exchange mechanism, so it's always PFS, no matter what kind of certificate you use. |
||
|
||
One optimisation that was considered when designing the protocol was to use the libp2p host key to generate the certificate in the case of RSA and ECDSA keys (which we can assume to be supported signature schemes by all peers). That would have allowed us to strip the host key and the signature from the key extension, in order to | ||
|
||
1. reduce the size of the certificate and | ||
2. reduce the number of signature verifications the peer has to perform from 2 to 1. | ||
|
||
The protocol does not include this optimisation, because | ||
|
||
1. assuming that the peer uses an ECDSA key for generating the self-signed certificate, this only saves about ~150 bytes if the host key is an ECDSA key as well, and it even slightly increases the size of the certificate in case of a RSA host key. Furthermore, for ECDSA keys, the size of all handshake messages combined is less than 900 bytes, so having a slightly larger certificate won't require us to send more (TCP / QUIC) packets. | ||
2. For a client, the number of signature verifications shouldn't pose a problem, since it controls the rate of its dials. Only for servers this might be a problem, since a malicious client could force a server to waste resources on signature verification. However, this is not a particularly interesting DoS vector, since the client's certificate is sent in its second flight (after receiving the ServerHello and the server's certificate), so it requires the attacker to actually perform most of the TLS handshake, including encrypting the certificate chain with a key that's tied to that handshake. | ||
|
||
|
||
### Versioning - How we could roll out a new version of this protocol in the future | ||
|
||
An earlier version of this document included a version negotiation mechanism. While it is a desireable property to be able to change things in the future, it also adds a lot of complexity. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about the protocol string in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes but we may want to speak TLS before multistream. That is, we should allow peers to advertise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That works for TCP, but not for QUIC. In QUIC, the first packet sent by the client already contains the ClientHello. |
||
|
||
To keep things simple, the current proposal does not include a version negotiation mechanism. A future version of this protocol might: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's a bit of a hole here, let's at least make it compatible with negotiation in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, that's what (2) below provides. We can use the SNI field as it's sent in the first packet. |
||
|
||
1. Change the format in which the keys are transmitted. A x509 extension has an ID (the Objected Identifier, OID), so we can use a new OID if we want to change the way we encode information. x509 certificates allow use to include multiple extensions, so we can even send the old and the new version during a transition period. In the handshake protocol defined here, peers are required to skip over extensions that they don't understand. | ||
2. For more involved changes, a new version might (ab)use the SNI field in the ClientHello to announce support for new versions. To allow for this to work, the current version requires clients to send anything in the SNI field and server to completely ignore this field, no matter what its contents are. | ||
marten-seemann marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# libp2p TLS Handshake | ||
|
||
## Introduction | ||
|
||
This document describes how [TLS 1.3](https://tools.ietf.org/html/rfc8446) is used to secure libp2p connections. Endpoints authenticate to their peers by encoding their public key into a x509 certificate extension. The protocol described here allows peers to use arbitrary key types, not constrained to those for which signing of a x509 certificates is specified. | ||
|
||
|
||
## Handshake Protocol | ||
|
||
The libp2p handshake uses TLS 1.3 (and higher). Endpoints MUST NOT negotiate lower TLS versions. | ||
|
||
During the handshake, peers authenticate each other’s identity as described in [Peer Authentication](#peer-authentication). Endpoints MUST verify the peer's identity. Specifically, this means that servers MUST require client authentication during the TLS handshake, and MUST abort a connection attempt if the client fails to provide the requested authentication information. | ||
|
||
|
||
## Peer Authentication | ||
|
||
In order to be able use arbitrary key types, peers don’t use their host key to sign the x509 certificate they send during the handshake. Instead, the host key is encoded into the [libp2p Public Key Extension](#libp2p-public-key-extension), which is carried in a self-signed certificate. The key used to generate and sign this certificate SHOULD NOT be related to the host's key. Endpoints MAY generate a new key and certificate for every connection attempt, or they MAY reuse the same key and certificate for multiple connections. Endpoints MUST choose a key that will allow the peer to verify the certificate (i.e. choose a signature algorithm that the peer supports), and SHOULD use a key type which allows for efficient signature computation and which reduces the combined size of the certificate and the signature. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we do this only because Go doesn't support some protocols in it's TLS implementation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's one reason, the other is #111. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think generating a new key per session should be a MUST, otherwise Perfect Forward Secrecy might be compromised. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me expand on my earlier comment. In TLS 1.3, the client send a key_share extension in the ClientHello. This key share is the client's ephemeral DH key (i.e. it is a fresh value for every connection). The server sends its part of the ephemeral DH key share in the key_share extension in the ServerHello. By the way, you might have heard of eTLS 1.3. It's supposed to be used by businesses who want middleboxes to be able to decode their traffic (and who don't care about their customer's privacy). It removes PFS from the protocol by having the server send a static key share in the ServerHello. The sad thing is, there's no immediate way to detect a server that's misbehaving in that way, other than running heuristics over multiple connections. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This precludes the optimization we talked about where a peer MAY derive the certificate key from the host key and the verifier MAY skip verifying the signature in the extension if the key matches the certificate key. I guess SHOULD allows implementations to do what they want, but we may want to say that implementations MUST NOT reject certificates that use the host key. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, SHOULD means that you're not supposed to do it, unless you think you have good reasons for it. I've described some measurements I did in the Design Considerations, section Why we're not using the host key for the certificate, which led me to conclude that this optimization is not worth it. Do you agree? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It probably isn't but there's no reason to preclude it. Basically, I just don't want some over-zealous dev to reject these keys. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, that's why it's a SHOULD. SHOULD implies that you can't rely on the peer following the recommendation, so I don't think any additional text is needed. |
||
|
||
Endpoints MUST NOT send a certificate chain that contains more than one certificate. The certificate MUST have NotBefore and NotAfter fields set such that the certificate is valid at the time it is received by the peer. When receiving the certificate chain, an endpoint MUST check these conditions and abort the connection attempt if the presented certificate is not yet valid or if it is expired. | ||
|
||
The certificate MUST contain the [libp2p Public Key Extension](#libp2p-public-key-extension). If this extension is missing, endpoints MUST abort the connection attempt. The certificate MAY contain other extensions, implementations MUST ignore extensions with unknown OIDs. | ||
|
||
Note for clients: Since clients complete the TLS handshake immediately after sending the certificate (and the TLS ClientFinished message), the handshake will appear as having succeeded before the server had the chance to verify the certificate. In this state, the client can already send application data. If certificate verification fails on the server side, the server will close the connection without processing any data that the client sent. | ||
|
||
|
||
|
||
### libp2p Public Key Extension | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the exact string name of the extension to use when generating the certificate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the question. You can find the OID further down in the document. |
||
|
||
In order to prove ownership of its host key, an endpoint sends two values: | ||
- the public host key | ||
- a signature performed using the private host key | ||
|
||
The public host key allows the peer to calculate the peer ID of the peer it is connecting to. Clients MUST verify that the peer ID derived from the certificate matches the peer ID they intended to connect to, and MUST abort the connection if there is a mismatch. | ||
|
||
The peer signs the concatenation of the string "libp2p-tls-handshake:" and the public key that it used to generate the certificate carrying the libp2p Public Key Extension using its private host key. This signature provides cryptographic proof that the peer was in possession of the private host key at the time the certificate was signed. Peers MUST verify the signature, and abort the connection attempt if signature verification fails. | ||
|
||
The public host key and the signature are ANS.1-encoded into the SignedKey data structure, which is carried in the libp2p Public Key Extension. The libp2p Public Key Extension is a x509 extension with the Object Identier 1.3.6.1.4.1.53594.1.1. | ||
|
||
```asn1 | ||
SignedKey ::= SEQUENCE { | ||
publicKey BIT STRING, | ||
signature BIT STRING | ||
marten-seemann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
``` | ||
|
||
The publicKey field of SignedKey contains the public host key of the endpoint, encoded using the following protobuf. | ||
|
||
```protobuf | ||
enum KeyType { | ||
RSA = 0; | ||
Ed25519 = 1; | ||
Secp256k1 = 2; | ||
ECDSA = 3; | ||
} | ||
|
||
message PublicKey { | ||
required KeyType Type = 1; | ||
required bytes Data = 2; | ||
} | ||
``` | ||
|
||
TODO: PublicKey.Data looks underspecified. Define precisely how to marshal the key. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some discussion here: https://github.com/libp2p/specs/pull/100/files. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a bunch of specs that only live in PRs now, but don't seem to be blocked by anything major. We should get them merged some time, so we can properly refer to them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mostly blocked on reviews, IIRC. But yeah, we should just get MVPs merged and mark them as drafts. |
||
|
||
|
||
## Future Extensibility | ||
|
||
Future versions of this handshake protocol MAY use the Server Name Indication in the ClientHello as defined in [RFC 6066, section 3](https://tools.ietf.org/html/rfc6066) to announce their support for other versions. In order to keep this flexibility for future versions, clients that only support the version of the handshake defined in this document MUST NOT send any value in the Server Name Indication. Servers that only this version MUST ignore this field, specifically, they MUST NOT check if it was empty. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should fork the required library and use Ed25519 using agl's libraries.
ECDSA kinda works but.. secp256k1 should be avoided anyways, due to side channel concerns. RSA should not even be considered for any current use cases, although supporting it for whatever strange future scenarios sounds harmless..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really, we might wan to rephrase this in terms of ecosystem support. That is, TLS is a rather complicated protocol and we don't want to force libp2p implementations to add support for new key types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered that, but concluded that's it's not a good solution. It would mean pulling in a forked x509 package, and in order to use that one, we'd also have to fork tls just to change the import path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a general remark, in my opinion whether or not a library exists in the ecosystem should only be a very minor consideration when writing specs. You want your specs to do the right thing, not the thing that can be implemented the most easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which is why I didn't answer your question "Any estimate how long would it take to get this fixed in rustls?" (#151 (comment))
To me how long something takes to implement is not relevant when discussing specs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filo has a branch at golang/go#25355 I'm surprised this takes google so long given agl's role in Ed25519 being accepted by standards bodies.