From 1c15ad0f81f7f9194cb9cf97c17e391fa1262a2c Mon Sep 17 00:00:00 2001 From: Ryan Butler Date: Wed, 19 Jun 2024 21:34:25 -0400 Subject: [PATCH] replicate: use bearer token instead of did (#111) The previous DID stuff in the replicate crates were unecessary and too application specific. Its more useful to use a general bearer token, and leave how to validate that token and what its contents are up to the application. A did-signed authentication attestation could just be a base64 signed message, and use that as the bearer token. --- .../client/examples/example-client.rs | 17 ++-- crates/replicate/client/src/instance.rs | 69 +++------------ crates/replicate/client/src/lib.rs | 63 +++++++++++++ crates/replicate/client/src/manager.rs | 53 +++-------- crates/replicate/common/src/did.rs | 88 ------------------- crates/replicate/common/src/lib.rs | 1 - 6 files changed, 89 insertions(+), 202 deletions(-) delete mode 100644 crates/replicate/common/src/did.rs diff --git a/crates/replicate/client/examples/example-client.rs b/crates/replicate/client/examples/example-client.rs index eefc628..56ad508 100644 --- a/crates/replicate/client/examples/example-client.rs +++ b/crates/replicate/client/examples/example-client.rs @@ -1,10 +1,7 @@ use clap::Parser; use color_eyre::{eyre::WrapErr, Result}; use replicate_client::{instance::Instance, manager::Manager}; -use replicate_common::{ - data_model::State, - did::{AuthenticationAttestation, Did, DidPrivateKey}, -}; +use replicate_common::data_model::State; use tracing::info; use tracing_subscriber::{filter::LevelFilter, EnvFilter}; use url::Url; @@ -14,8 +11,9 @@ use url::Url; pub struct Args { #[clap(long)] url: Url, + /// Optional bearer token to use for authenticating. #[clap(long)] - username: String, + token: Option, } #[tokio::main] @@ -34,12 +32,7 @@ async fn main() -> Result<()> { let args = Args::parse(); - let did = Did(args.username); - let did_private_key = DidPrivateKey; - - let auth_attest = AuthenticationAttestation::new(did, &did_private_key); - - let mut manager = Manager::connect(args.url, &auth_attest) + let mut manager = Manager::connect(args.url, args.token.as_deref()) .await .wrap_err("failed to connect to manager")?; info!("Connected to manager!"); @@ -55,7 +48,7 @@ async fn main() -> Result<()> { .wrap_err("failed to get instance url")?; info!("Got instance {instance_id} at: {instance_url}"); - let mut instance = Instance::connect(instance_url, auth_attest) + let mut instance = Instance::connect(instance_url, args.token.as_deref()) .await .wrap_err("failed to connect to instance")?; info!("Connected to instance!"); diff --git a/crates/replicate/client/src/instance.rs b/crates/replicate/client/src/instance.rs index 7dbb639..3d5dc03 100644 --- a/crates/replicate/client/src/instance.rs +++ b/crates/replicate/client/src/instance.rs @@ -31,12 +31,6 @@ //! disconnects and reconnects. The client must prove ownership of this unique //! identifier. //! -//! The most logical implementation strategy to accomplish this is to solve it the same -//! way that the nexus protocol does - every user is identified by a -//! [Decentralized Identifier][DID][^1], and they sign a message with the DID's associated -//! private key, proving that they are who they say they are. This is done via the -//! [`AuthenticationAttestation`] argument when connecting to the instance. -//! //! # Entities //! Each entity has state, and the instance has many entities. An entity is identified //! with an id, which the server assigns in response to a request to spawn an entity @@ -79,18 +73,14 @@ //! [did:web]: https://w3c-ccg.github.io/did-method-web/ //! [ABA]: https://en.wikipedia.org/wiki/ABA_problem -use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; use eyre::{bail, ensure, Result, WrapErr}; use futures::{SinkExt, StreamExt}; -use replicate_common::{ - data_model::{DataModel, Entity, LocalChanges, RemoteChanges, State}, - did::AuthenticationAttestation, +use replicate_common::data_model::{ + DataModel, Entity, LocalChanges, RemoteChanges, State, }; -use tracing::warn; use url::Url; -use wtransport::{endpoint::ConnectOptions, ClientConfig, Endpoint}; -use crate::CertHashDecodeErr; +use crate::{connect_to_url, Ascii}; use replicate_common::messages::instance::{Clientbound as Cb, Serverbound as Sb}; type RpcFramed = replicate_common::Framed; @@ -118,13 +108,14 @@ impl Instance { /// # Arguments /// - `url`: Url of the manager api. For example, `https://foobar.com/my/manager` /// or `192.168.1.1:1337/uwu/some_manager`. - /// - `auth_attest`: Used to provide to the server proof of our identity, based on - /// our DID. - pub async fn connect( - url: Url, - auth_attest: AuthenticationAttestation, - ) -> Result { - let conn = connect_to_url(&url, auth_attest) + /// - `bearer_token`: optional, must be ascii otherwise we will panic. + pub async fn connect(url: Url, bearer_token: Option<&str>) -> Result { + let bearer_token = bearer_token.map(|s| { + // Technically, bearer tokens only permit a *subset* of ascii. But I + // didn't care enough to be that precise. + Ascii::try_from(s).expect("to be in-spec, bearer tokens must be ascii") + }); + let conn = connect_to_url(&url, bearer_token) .await .wrap_err("failed to connect to server")?; @@ -196,41 +187,3 @@ pub enum RecvState<'a> { /// Sequence number for state messages #[derive(Debug, Default)] pub struct StateSeq; - -async fn connect_to_url( - url: &Url, - auth_attest: AuthenticationAttestation, -) -> Result { - let cert_hash = if let Some(frag) = url.fragment() { - let cert_hash = BASE64_URL_SAFE_NO_PAD - .decode(frag) - .map_err(CertHashDecodeErr::from)?; - let len = cert_hash.len(); - let cert_hash: [u8; 32] = cert_hash - .try_into() - .map_err(|_| CertHashDecodeErr::InvalidLen(len))?; - Some(cert_hash) - } else { - None - }; - - let cfg = ClientConfig::builder().with_bind_default(); - let cfg = if let Some(_cert_hash) = cert_hash { - // TODO: Implement self signed certs properly: - // https://github.com/BiagioFesta/wtransport/issues/128 - warn!( - "`serverCertificateHashes` is not yet supported, turning off \ - cert validation." - ); - cfg.with_no_cert_validation() - } else { - cfg.with_native_certs() - } - .build(); - - let client = Endpoint::client(cfg)?; - let opts = ConnectOptions::builder(url) - .add_header("Authorization", format!("Bearer {}", auth_attest)) - .build(); - Ok(client.connect(opts).await?) -} diff --git a/crates/replicate/client/src/lib.rs b/crates/replicate/client/src/lib.rs index 1c3c77a..9d62009 100644 --- a/crates/replicate/client/src/lib.rs +++ b/crates/replicate/client/src/lib.rs @@ -1,4 +1,10 @@ +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine as _}; +use eyre::Result; +use tracing::warn; +use url::Url; + pub use replicate_common as common; +use wtransport::{endpoint::ConnectOptions, ClientConfig, Endpoint}; pub mod instance; pub mod manager; @@ -11,3 +17,60 @@ pub enum CertHashDecodeErr { #[error("expected length of 32, got length of {0}")] InvalidLen(usize), } + +/// A string that has been validated to be ascii. +struct Ascii<'a>(&'a str); + +impl<'a> TryFrom<&'a str> for Ascii<'a> { + type Error = (); + + fn try_from(value: &'a str) -> std::prelude::v1::Result { + if value.is_ascii() { + Ok(Self(value)) + } else { + Err(()) + } + } +} + +/// If there is a url fragment, it will be treated as a server certificate hash. +async fn connect_to_url( + url: &Url, + bearer_token: Option>, +) -> Result { + let cert_hash = if let Some(frag) = url.fragment() { + let cert_hash = BASE64_URL_SAFE_NO_PAD + .decode(frag) + .map_err(CertHashDecodeErr::from)?; + let len = cert_hash.len(); + let cert_hash: [u8; 32] = cert_hash + .try_into() + .map_err(|_| CertHashDecodeErr::InvalidLen(len))?; + Some(cert_hash) + } else { + None + }; + + let cfg = ClientConfig::builder().with_bind_default(); + let cfg = if let Some(_cert_hash) = cert_hash { + // TODO: Implement self signed certs properly: + // https://github.com/BiagioFesta/wtransport/issues/128 + warn!( + "`serverCertificateHashes` is not yet supported, turning off \ + cert validation." + ); + cfg.with_no_cert_validation() + } else { + cfg.with_native_certs() + } + .build(); + + let client = Endpoint::client(cfg)?; + let opts = ConnectOptions::builder(url); + let opts = if let Some(b) = bearer_token { + opts.add_header("Authorization", format!("Bearer {}", b.0)) + } else { + opts + }; + Ok(client.connect(opts.build()).await?) +} diff --git a/crates/replicate/client/src/manager.rs b/crates/replicate/client/src/manager.rs index a8f5b48..91fe96e 100644 --- a/crates/replicate/client/src/manager.rs +++ b/crates/replicate/client/src/manager.rs @@ -2,20 +2,17 @@ use std::fmt::Debug; -use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; use eyre::{bail, ensure, eyre, Context}; use futures::sink::SinkExt; use futures::stream::StreamExt; use replicate_common::{ - did::AuthenticationAttestation, messages::manager::{Clientbound as Cb, Serverbound as Sb}, InstanceId, }; -use tracing::warn; use url::Url; -use wtransport::{endpoint::ConnectOptions, ClientConfig, Endpoint}; -use crate::CertHashDecodeErr; +use crate::connect_to_url; +use crate::Ascii; type Result = eyre::Result; type Framed = replicate_common::Framed; @@ -37,44 +34,14 @@ impl Manager { /// # Arguments /// - `url`: Url of the manager api. For example, `https://foobar.com/my/manager` /// or `192.168.1.1:1337/uwu/some_manager`. - /// - `auth_attest`: Used to provide to the server proof of our identity, based on - /// our DID. - pub async fn connect( - url: Url, - auth_attest: &AuthenticationAttestation, - ) -> Result { - let cert_hash = if let Some(frag) = url.fragment() { - let cert_hash = BASE64_URL_SAFE_NO_PAD - .decode(frag) - .map_err(CertHashDecodeErr::from)?; - let len = cert_hash.len(); - let cert_hash: [u8; 32] = cert_hash - .try_into() - .map_err(|_| CertHashDecodeErr::InvalidLen(len))?; - Some(cert_hash) - } else { - None - }; - - let cfg = ClientConfig::builder().with_bind_default(); - let cfg = if let Some(_cert_hash) = cert_hash { - warn!( - "`serverCertificateHashes` is not yet supported, turning off \ - cert validation." - ); - // TODO: Use the cert hash as the root cert instead of no validation - cfg.with_no_cert_validation() - } else { - cfg.with_native_certs() - } - .build(); - - let client = Endpoint::client(cfg)?; - let opts = ConnectOptions::builder(&url) - .add_header("Authorization", format!("Bearer {}", auth_attest)) - .build(); - let conn = client - .connect(opts) + /// - `bearer_token`: optional, must be ascii otherwise we will panic. + pub async fn connect(url: Url, bearer_token: Option<&str>) -> Result { + let bearer_token = bearer_token.map(|s| { + // Technically, bearer tokens only permit a *subset* of ascii. But I + // didn't care enough to be that precise. + Ascii::try_from(s).expect("to be in-spec, bearer tokens must be ascii") + }); + let conn = connect_to_url(&url, bearer_token) .await .wrap_err("failed to connect to server")?; let bi = wtransport::stream::BiStream::join( diff --git a/crates/replicate/common/src/did.rs b/crates/replicate/common/src/did.rs deleted file mode 100644 index 97e90d3..0000000 --- a/crates/replicate/common/src/did.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::fmt::Display; - -use bytes::Bytes; - -/// The private key portion of the user's [`Did`]. -/// TODO: Make this use actual cryptography and Nexus. -pub struct DidPrivateKey; - -/// The ed25519 signature of a message. -/// TODO: Use actual cryptography instead of no-op. -#[derive(Debug)] -pub struct Signature; - -/// The Decentrailized Id, i.e. the account identifier. -/// TODO: Make this use actual cryptography and Nexus. -#[derive(Debug)] -pub struct Did(pub String); - -/// A message signed by the user's account. If you have an instance of this struct, -/// you can be sure that the signature is valid. -#[derive(Debug)] -pub struct SignedMessage { - did: Did, - _sig: Signature, - msg: Bytes, -} - -impl SignedMessage { - /// Creates a `SignedMessage`. - /// # Panics - /// Panics if the `did` and `private_key` don't match. - pub fn sign(msg: Bytes, did: Did, _private_key: &DidPrivateKey) -> Self { - // TODO: Actually do the cryptography - Self::verify(msg, did, Signature).expect( - "verification of generated signature failed, did private key match DID?", - ) - } - - pub fn msg(&self) -> &[u8] { - &self.msg - } - - pub fn did(&self) -> &Did { - &self.did - } - - pub fn verify(msg: Bytes, did: Did, signature: Signature) -> Result { - // TODO: Do actual cryptography by checking signature against message - Ok(Self { - did, - _sig: signature, - msg, - }) - } -} - -/// Provides evidence that the client actually owns the DID that they claim they own. -#[derive(Debug)] -pub struct AuthenticationAttestation(SignedMessage); - -impl AuthenticationAttestation { - pub fn new(did: Did, private_key: &DidPrivateKey) -> Self { - Self(SignedMessage::sign( - did.0.clone().into_bytes().into(), - did, - private_key, - )) - } - - /// Promotes a message to an authentication attestation, by checking that the - /// message is genuine. - pub fn verify( - msg: Bytes, - sig: Signature, - ) -> Result> { - let did = std::str::from_utf8(&msg)?; - let did = Did(did.to_owned()); - let signed = SignedMessage::verify(msg, did, sig) - .map_err(|_| "failed to verify message signature")?; - Ok(AuthenticationAttestation(signed)) - } -} - -impl Display for AuthenticationAttestation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.did.0) - } -} diff --git a/crates/replicate/common/src/lib.rs b/crates/replicate/common/src/lib.rs index e9de383..f6f0a92 100644 --- a/crates/replicate/common/src/lib.rs +++ b/crates/replicate/common/src/lib.rs @@ -1,5 +1,4 @@ pub mod data_model; -pub mod did; mod framed; pub mod messages;