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

Add protocol level authentication #1957

Closed
wants to merge 8 commits into from
Closed

Add protocol level authentication #1957

wants to merge 8 commits into from

Conversation

dignifiedquire
Copy link
Contributor

@dignifiedquire dignifiedquire commented Jan 16, 2024

Brings back the general concept of authenticating requests.

This is a protocol level breaking change, which is why all ALPN version numbers have been increased.

Currently there are two authenticators implemented

  • NoAuthenticator: does nothing
  • IdAuthenticator: sends a single UUID for all requests, which allows attribution of incoming traffic to this id. By default this authenticator is use in the iroh cli, and the uuid can be set in the config or on the console using IROH_UUID="my-uuid"

Open Questions

  • Should gossip authenticate each message, or can we reduce this?

@dignifiedquire dignifiedquire self-assigned this Jan 16, 2024
pub fn start(
connection: quinn::Connection,
request: GetRequest,
auth: Authenticator,
Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to be fancy you could request impl DynAuthenticator and then implement it for Option<Authenticator> or maybe for Option<dyn DynAuthenticator> (not entirely sure what works and what doesn't without trying it out). It would give a more natural API here as a user would probably guess to pass None instead of having to find NoAuthenticator and importing it.

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 doesn't quite work unfortunately, tried it

pub const REJECTED_CODE: u32 = 10;

#[derive(Debug, Clone)]
pub struct Authenticator(Arc<dyn DynAuthenticator>);
Copy link
Contributor

Choose a reason for hiding this comment

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

Good to have this dyn by default. This does not warrant a type parameter.


type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;

pub trait DynAuthenticator: Sync + Send + std::fmt::Debug + 'static {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to have doc comments here to describe the flow. So for each request the request fn is called, which may or may not produce a token. And then response is called with the request and the token from above, and tells us whether to proceed. What is the purpose of the split compared to just having request -> ok/not ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added some docs


#[derive(Debug, Clone)]
pub struct Request {
pub id: u64,
Copy link
Member

Choose a reason for hiding this comment

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

What is the purpose of this id?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added a comment


pub trait DynAuthenticator: Sync + Send + std::fmt::Debug + 'static {
fn request(&self, request: Request) -> BoxFuture<Result<Option<Token>>>;
fn respond(&self, request: Request, token: &Option<Token>) -> BoxFuture<Result<AcceptOutcome>>;
Copy link
Member

@Frando Frando Jan 17, 2024

Choose a reason for hiding this comment

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

To verify my understanding, would that be correct doc comments?

/// Optionally add a token to outgoing requests.
///
/// This is called for each outgoing request created by this node. 
/// When returning a `Token`, it will be added to the request payload.
fn request(&self, request: Request) -> BoxFuture<Result<Option<Token>>>;

/// Authenticate incoming requests.
/// 
/// This is called for each incoming request, right after decoding but before any processing takes place. 
/// Processing and responding only continues if this method returns `AcceptOutcome::Accept`. 
/// Otherwise the request will be declined.
fn respond(&self, request: Request, token: &Option<Token>) -> BoxFuture<Result<AcceptOutcome>>;

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, exactly, thank you for writing the docs for me :)


type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;

pub trait DynAuthenticator: Sync + Send + std::fmt::Debug + 'static {
Copy link
Member

@Frando Frando Jan 17, 2024

Choose a reason for hiding this comment

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

Should we add the NodeId of the source/destination? We always have this available I think, and I would guess that custom authenticators might e.g. want to do allow-list based authentication?

So

fn request(&self, request: Request, dest: NodeId) -> BoxFuture<Result<Option<Token>>>;
fn respond(&self, request: Request, src: NodeId, token: &Option<Token>) -> BoxFuture<Result<AcceptOutcome>>;

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 would say lets not add it for now, we can easily add it later down the line if we see the need for it, as it doesn't change the wire format.


type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;

pub trait DynAuthenticator: Sync + Send + std::fmt::Debug + 'static {
Copy link
Member

@Frando Frando Jan 17, 2024

Choose a reason for hiding this comment

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

I found the namings not to be super intuitive. Maybe explicit method names would make this easier to parse, e.g.

fn create_token(&self, request: Request, dest: NodeId) -> BoxFuture<Result<Option<Token>>>;
fn accept_request(&self, request: Request, src: NodeId, token: &Option<Token>) -> BoxFuture<Result<AcceptOutcome>>;

but with proper docs the current naming might work too. Not super sure yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed, let me know what you think of the new names

}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Token {
Copy link
Member

@Frando Frando Jan 17, 2024

Choose a reason for hiding this comment

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

If we want to be able to evolve this format without breaking the wire format, maybe wrap it in an enum with a single variant for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

my assumption was that if we need to change the token we need to bump the protocol version anyway

@dignifiedquire dignifiedquire marked this pull request as ready for review January 18, 2024 09:27
@dignifiedquire dignifiedquire changed the title [WIP] Add protocol level authentication Add protocol level authentication Jan 18, 2024
@dignifiedquire
Copy link
Contributor Author

Needs more design

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

Successfully merging this pull request may close these issues.

4 participants