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

libp2p TLS 1.3 Handshake #151

Merged
merged 7 commits into from
Apr 7, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions tls/design considerations.md
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.
Copy link

@burdges burdges Mar 28, 2019

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..

Copy link
Member

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.

Copy link
Contributor Author

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.

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link

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.

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 to 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 perfomance and simplify the protocol.
Copy link
Member

Choose a reason for hiding this comment

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

Most importantly, it would make us vulnerable to downgrade attacks.

marten-seemann marked this conversation as resolved.
Show resolved Hide resolved


### 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.
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

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

What about the protocol string in multistream-select? That's a version negotiation mechanism.

Copy link
Member

Choose a reason for hiding this comment

The 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 /ip4/.../tcp/443/tls/ipfs/Qm... directly. This should help disguise IPFS traffic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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 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
69 changes: 69 additions & 0 deletions tls/tls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# libp2p TLS Handshake

## Introduction

This document describes how [TLS 1.3](https://tools.ietf.org/html/rfc8446) is used to secure libp2p connections. Endpoints authenticates 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.
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved


## 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 identy. Specifically, this means that servers MUST require clients authentication during the TLS handshake, and MUST abort a connection attempt if the client fails to provide the requested authentication information.
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved


## 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.
Copy link

@magik6k magik6k Feb 28, 2019

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's one reason, the other is #111.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Since TLS 1.3 decouples key exchange mechanism from the signature algorithms, TLS 1.3 handshakes are always forward secure, no matter what kind of certificate you use.

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.

Copy link
Member

Choose a reason for hiding this comment

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

The key used to generate and sign this certificate SHOULD NOT be related to the host's key.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 key corresponding to its host key
Copy link
Member

Choose a reason for hiding this comment

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

So my main issue is this sentence, I don't understand the difference between public key and host key.

- a signature performed using the host key

The public 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 it there is a mismatch.

The peer signs its public key using the its host key. This signature provides cryptographic proof that the peer was in possession of the private key at the time the certificate was signed. Peers MUST verify the signature, and abort the connection attempt if signature verification fails.
Copy link
Member

Choose a reason for hiding this comment

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

Q: Which public key is signed?
Q: I feel like it should somehow be bound to TLS session we are establishing. Otherwise, this proof could be "stolen" and reused by someone else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the public key used to generate the certificate. I'll update the PR to clarify that.
Why do you want to bind it to the session that we're establishing? What we're doing here is basically creating a certificate chain, with the host key at the root. Just that we're not using x509 certificates, due to the limitations described earlier.

Copy link
Contributor

Choose a reason for hiding this comment

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

I initially thought that the publicKey field of SignedKey was referring to the host key. I think it's this sentence which is a bit ambiguous:

The peer signs its public key using the its host key.

Since you were just talking about the public key for the peer ID in the prior paragraph, I assumed that's what "public key" referred to here.

It's clearer down below, where it says "The publicKey field of SignedKey contains the public key of the endpoint", although it wouldn't hurt to spell out that the endpoint key is the one used to generate the certificate.

Since it is the endpoint key being signed, if you wanted to have the proof be bound to the session as @Kubuxu suggests, you could just generate a new key and self-signed certificate for each session. Is that correct @marten-seemann?

Copy link
Member

Choose a reason for hiding this comment

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

I'm still not clear about which keys, public keys and certificates are being sent over and in which fields.

I don't see cleanly that the chain confirming ownership of key used in TLS handshake is bound to a PeerID.
I have few ideas regarding possible attacks, and possible reuse in case TLS key is leaked but first I need to understand this part fully.

Copy link
Member

Choose a reason for hiding this comment

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

I'm reading it again so bear with me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the confusion here. You sign the public key that was used to generate the certificate with your host private key.

The reason is that the key used to generate the certificate is used by TLS during the handshake to sign the CertificateVerify message. By signing the public key of the certificate we get a chain of verified signatures.

Using the host's public key would be a bad idea. An attacker could just copy the contents of the libp2p Public Key Extension and use that to impersonate this peer. We really need an operation that proves that the peer actually meant to use the key used by TLS.


The public 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.XXX.YYY.

TODO: Nothing will break if we just use an arbitrary value for XXX. However, if we want to do things correctly, [OID](https://en.wikipedia.org/wiki/Object_identifier) PENs should be registered with [IANA](https://pen.iana.org/pen/PenApplication.page).
marten-seemann marked this conversation as resolved.
Show resolved Hide resolved

```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 key of the endpoint, encoded using the following protobuf.
Copy link
Member

Choose a reason for hiding this comment

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

That seems redundant with #100


```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.
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.