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

HTTP: Peer ID Authentication #564

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Changes from 10 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
194 changes: 194 additions & 0 deletions http/peer-id-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Peer ID Authentication over HTTP

| Lifecycle Stage | Maturity | Status | Latest Revision |
| --------------- | ------------- | ------ | --------------- |
| 1A | Working Draft | Active | r0, 2023-01-23 |

Authors: [@MarcoPolo]
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved

Interest Group: [@sukunrt], [@achingbrain]

## Introduction

This spec defines an authentication scheme of libp2p Peer IDs in accordance with
[RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110). The authentication
scheme is called `libp2p-PeerID`.

## Protocol Overview

## Parameters

| Param Name | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| hostname | The server name used in the TLS connection (SNI). |
| challenge-server | The random base64 encoded value the client generates to challenge the server to prove its identity |
| challenge-client | The random base64 encoded value the server generates to challenge the client to prove its identity |
| sig | A base64 encoded signature. |
| peer-id | The peer id of the node that set this parameter. Encoding defined by the [Peer ID spec]. |
| public-key | A base64 encoded value of peer's public key. The key itself is encoded per the [Peer ID spec]. |
| opaque | An base64 encoded opaque to the client blob generated by the server. If a client receives this it must return it. A server may use this to authenticate statelessly. For example, it could store the challenge-client and a expiry time. |

Params are encoded per [RFC 9110 auth-param's ABNF](https://datatracker.ietf.org/doc/html/rfc9110#name-collected-abnf). Generally it'll be something like: `hostname="example.com", challenge-server=base64EncodedVal`

## Signing

Signatures sign some set of parameters. The parameters are sorted
alphabetically, prepended with a varint length prefix, and concatenated together
to form the data to be signed. The signing algorithm is defined by the key type
used. Refer to the [Peer ID
spec] for
specifics on the signing algorithm. The set of parameters is prefixed with the auth scheme "libp2p-PeerID"

As an example, if we wanted to sign the parameters `hostname="example.com",
challenge-client=base64String` we would first structure the parameters as a byte
slice containing:
sukunrt marked this conversation as resolved.
Show resolved Hide resolved
```
libp2p-PeerID<varintprefix>challenge-client=<base64String><varintprefix>hostname="example.com"
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved
```

Then sign the resulting byte slice. See the test vectors below for a
examples.


## Base64 Encoding

The base64 encoding follows Base 64 Encoding with URL and Filename Safe Alphabet
from [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-5). The
reason this is not a multibase is to aid clients or servers who can not or
prefer not to import a multibase dependency.
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved

## Mutual Client and Server Peer ID

The following protocol allows both the client and server to authenticate each
other's peer id by having them each sign a challenge issued by the other. The
protocol operates as follows:

1. The client makes a POST request to the authentication URI.
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved
2. The server responds with status code 401 (Unauthorized) and set the header:
```
WWW-Authenticate: libp2p-PeerID challenge-client=<base64-encoded-challenge>, opaque=<base64-encoded-opaque-value>
```
The opaque parameter is opaque to client. The client MUST return the opaque
parameter back to the server. The server MAY use the opaque parameter to
encode state.
3. The client makes another POST request to the authentication URI and sets the header:
```
Authorization: libp2p-PeerID peer-id=<string-representation-of-client-peer-id>, opaque=<opaque-from-server>, challenge-server=<base64-encoded-challenge-server>, sig=<base64-signature-bytes>
```

The `sig` param represents a signature over the parameters:
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to include the certificate (or public key?) hash, if possible. That way, if the client has some way of validating it they can not rely on the CA system and instead validate 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.

This could be easy to do, but I'm not sure it would be used. The optimal solution here is to authenticate with some TLS exported key material (RFC 5705). That would bind the peer id to the TLS session and can happen with no additional round trips. If a peer can access the certificate they likely can access the key material exporter.

The only problem with this approach is that the key material exporters aren't yet exposed by browsers. But I expect that to change soon-ish as https://datatracker.ietf.org/doc/draft-ietf-httpbis-unprompted-auth/ gets published and implemented by browsers.

My thought here is to focus on the browser use case, and, if it would be used and useful, create a new spec for the exported key material scenario.

Copy link
Member

Choose a reason for hiding this comment

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

The optimal solution here is to authenticate with some TLS exported key material (RFC 5705). That would bind the peer id to the TLS session and can happen with no additional round trips. If a peer can access the certificate they likely can access the key material exporter.

I agree, but that's likely harder to get at.

My thought here is to focus on the browser use case, and, if it would be used and useful, create a new spec for the exported key material scenario.

What about making the format extensible? I.e., allow the server to sign multiple things that can be extended later?

Copy link
Member

Choose a reason for hiding this comment

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

I agree, but that's likely harder to get at.

Ah, I see. That's what that proposal is about. Nice!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about making the format extensible? I.e., allow the server to sign multiple things that can be extended later?

There is room in this spec to extend this later. A future version could for example define a new parameter that is passed by the client. Example: a SessionID parameter which is derived from keying material from the tls session just for this purpose, the server would check that it matches the expected SessionID it sees or fail the request.

Copy link
Member

Choose a reason for hiding this comment

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

This has to be covered by a signature. I.e., either:

  1. It's covered by the client signature and the server refuses to authenticate if it doesn't match
  2. It's covered by the server's signature and the client rejects the authentication if it doesn't match.

I want to make sure there's some sane upgrade path where we can add support for this later.

Copy link
Contributor Author

@MarcoPolo MarcoPolo Aug 21, 2024

Choose a reason for hiding this comment

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

Yes, sorry. The signature MUST include the SessionID as part of the parameters it signs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to be clear with the example for a future extension here:

  1. Each side derives a TLSSessionID using TLS' Keying Material exporters in a TBD way.
  2. Each side passes that as a parameter to the libp2p-PeerID auth scheme. And includes it as parameter in the signature.
  3. Each side MUST verify the provided TLSSessionID matches the expected value (as well as verifying the signature as before).
  4. Either side MUST fail the authentication if there is a mismatch.

Both sides need to sign and compare it.

- `hostname`
- `challenge-client` in its base64 encoded form.
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved
4. The server MUST verify the signature using the server name used in the TLS
session. The server MUST return 401 Unauthorized if the server fails to
validate the signature. If the signature is valid, the server has
authenticated the client's peer id. The server MUST respond with status code
200 (OK) and set the headers:
```
Authentication-Info: libp2p-PeerID peer-id=<server-peer-id-string>, sig=<base64-signature-bytes>
Authorization: libp2p-Bearer <base64-encoded-opaque-blob>
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved
```
The `sig` param represents a signature over the parameters:
- `hostname`
- `challenge-server` in its base64 encoded form.
- `client` the string representation of the client's Peer ID.
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved

The `libp2p-Bearer` token allows the client to make peer id authenticated
requests. The value is opaque to the client, and the server may use it to
store authentication state such as:
- The client's Peer ID.
- The `hostname` parameter.
- The token creation date (to allow tokens to expire).
5. The client MUST verify the signature. After verification the client has
authenticated the server's peer id. The client MUST send the `libp2p-Bearer`
token for Peer ID authenticated requests.

## libp2p Bearer token

The libp2p Bearer token is a token given to the client from the server that
allows the client (the bearer) to make Peer ID authenticated requests to the
server. Once the client receives this token after the Mutual Authentication
protocol, the client should save it and use it for future authenticated
requests.

The server SHOULD return a 401 Unauthorized and follow the above Mutual
authentication protocol when it wants the client to request a new libp2p Bearer
token.

## Authentication URI Endpoint

Because the client needs to make a request to authenticate the server, and the
client may not want to make the real request before authenticating the server,
the server MAY provide an authentication endpoint. This authentication endpoint
is like any other application protocol, and it shows up in `.well-known/libp2p/protocols`,
but it only does the authentication flow. The client and server SHOULD NOT send
any data besides what is defined in the above authentication flow. The protocol
id for the authentication endpoint is `/http-peer-id-auth/1.0.0`.


## Considerations for Implementations

* Implementations MUST only authenticate over a secured connection (i.e. TLS).
* Implementations SHOULD limit the maximum length of any variable length field.
MarcoPolo marked this conversation as resolved.
Show resolved Hide resolved

## Note on web PKI

Protection against man-in-the-middle (mitm) type attacks is through web PKI. If
the client is in an environment where web PKI can not be fully trusted (e.g. an
enterprise network with a custom enterprise root CA installed on the client),
then this authentication scheme can not protect the client from a mitm attack.

## Test Vectors

### Definitions used

- zero key: An ED25519 key initialized with zero bytes.
- zero peer id: A peer ID derived from the zero key.
- client key: An ED25519 key with the following marshalled key (refer to the [Peer ID spec] for how to unmarshal): `080112407e0830617c4a7de83925dfb2694556b12936c477a0e1feb2e148ec9da60fee7d1ed1e8fae2c4a144b8be8fd4b47bf3d3b34b871c3cacf6010f0e42d474fce27e`
- client peer ID: A peer id derived from the client key.

### Walkthrough

Included is a concrete example of running the protocol. The client uses the Peer ID defined above, and the server uses the zero key.



1. The clients sends the initial request.
2. The server responds with the header:
```
WWW-Authenticate: libp2p-PeerID challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, opaque=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
```
3. The client sends another request with the header:
```
Authorization: libp2p-PeerID peer-id=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq, opaque=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=, challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=, sig=F5OBYbbMXoIVJNWrW0UANi7rrbj4GCB6kcEceQjajLTMvC-_jpBF9MFlxiaNYXOEiPQqeo_S56YUSNinwl0ZCQ==
```
4. The server responds with the header:
```
Authentication-Info: libp2p-PeerID peer-id=12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN, sig=btLFqW200aDTQqpkKetJJje7V-iDknXygFqPsfiegNsboXeYDiQ6Rqcpezz1wfr8j9h83QkN9z78cAWzKzV_AQ==
Authorization: libp2p-Bearer <base64-encoded-bearer-token>
```


The following table lists out all parameters and intermediate values used in the walkthrough above.

| Parameter | value |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| hostname | example.com |
| challenge-client | `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` |
| challenge-server | `BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=` |
| client peer id | `12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq` |
| server's peer id | The zero key `12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN` |
| The server's opaque blob | Could be anything. In this example we'll use `CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=`. |
| What the client will sign (percent encoded) | `libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%16hostname=%22example.com%22` |
| The client's signature | `F5OBYbbMXoIVJNWrW0UANi7rrbj4GCB6kcEceQjajLTMvC-_jpBF9MFlxiaNYXOEiPQqeo_S56YUSNinwl0ZCQ==` |
| The client's Authorization header | `Authorization: libp2p-PeerID peer-id=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq, opaque=CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=, challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=, sig=MKoR8Shzr6VmQ675dErKh_gGGUsGaO8zXnZ8Cx8bIKiQlYBhqazUG8w4lG3_Wd5IfSz5P1HLfXtVb_fg_dsxDw==` |
| What the server will sign (percent encoded) | `libp2p-PeerID=challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=%3Bclient=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq%16hostname=%22example.com%22` |
| The server's signature | `btLFqW200aDTQqpkKetJJje7V-iDknXygFqPsfiegNsboXeYDiQ6Rqcpezz1wfr8j9h83QkN9z78cAWzKzV_AQ==` |
| The server's Authentication-Info header | `Authentication-Info: libp2p-PeerID peer-id=12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN, sig=m0OkSsO9YGcqfZ_XVTbiRwTtM4ds8434D9aod22Mmo3Wm0vBvxHOd71glC-uEez6g5gjA580KkGc9DOIvP47BQ==` |


[Peer ID spec]: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md

[@MarcoPolo]: https://github.com/MarcoPolo
[@sukunrt]: https://github.com/sukunrt
[@achingbrain]: https://github.com/achingbrain