From 98910b40dee80c3e426670ab2bbf1768613f56de Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 28 Aug 2024 19:55:29 +0300 Subject: [PATCH] feat(homeserver): update signup and signin to use new AuthToken --- pubky-common/src/auth.rs | 200 ++++------------------------ pubky-homeserver/src/error.rs | 5 +- pubky-homeserver/src/routes.rs | 4 +- pubky-homeserver/src/routes/auth.rs | 8 +- pubky-homeserver/src/server.rs | 6 +- pubky/src/shared/auth.rs | 18 +-- 6 files changed, 45 insertions(+), 196 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 6c476c6..8f95fbc 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -2,11 +2,10 @@ use std::sync::{Arc, Mutex}; -use ed25519_dalek::ed25519::SignatureBytes; use serde::{Deserialize, Serialize}; use crate::{ - crypto::{random_hash, Keypair, PublicKey, Signature}, + crypto::{Keypair, PublicKey, Signature}, timestamp::Timestamp, }; @@ -17,9 +16,6 @@ const CURRENT_VERSION: u8 = 0; // 45 seconds in the past or the future const TIMESTAMP_WINDOW: i64 = 45 * 1_000_000; -#[derive(Debug, PartialEq)] -pub struct AuthnSignature(Box<[u8]>); - #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct AuthToken { /// Version of the [AuthToken]. @@ -58,6 +54,11 @@ impl AuthToken { token } + /// Authenticate signer to an audience directly with [] capailities. + /// + /// + // pub fn authn(signer: &Keypair, audience: &PublicKey) -> Self {} + fn verify(audience: &PublicKey, bytes: &[u8]) -> Result { if bytes[0] > CURRENT_VERSION { return Err(Error::UnknownVersion); @@ -100,6 +101,10 @@ impl AuthToken { postcard::to_allocvec(self).unwrap() } + pub fn subject(&self) -> &PublicKey { + &self.subject + } + /// A unique ID for this [AuthToken], which is a concatenation of /// [AuthToken::subject] and [AuthToken::timestamp]. /// @@ -185,172 +190,6 @@ pub enum Error { AlreadyUsed, } -impl AuthnSignature { - pub fn new(signer: &Keypair, audience: &PublicKey, token: Option<&[u8]>) -> Self { - let mut bytes = Vec::with_capacity(96); - - let time: u64 = Timestamp::now().into(); - let time_step = time / TIME_INTERVAL; - - let token_hash = token.map_or(random_hash(), crate::crypto::hash); - - let signature = signer - .sign(&signable( - &time_step.to_be_bytes(), - &signer.public_key(), - audience, - token_hash.as_bytes(), - )) - .to_bytes(); - - bytes.extend_from_slice(&signature); - bytes.extend_from_slice(token_hash.as_bytes()); - - Self(bytes.into()) - } - - /// Sign a randomly generated nonce - pub fn generate(keypair: &Keypair, audience: &PublicKey) -> Self { - AuthnSignature::new(keypair, audience, None) - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } -} - -#[derive(Debug, Clone)] -pub struct AuthnVerifier { - audience: PublicKey, - inner: Arc>>, - // TODO: Support permisisons - // token_hashes: HashSet<[u8; 32]>, -} - -impl AuthnVerifier { - pub fn new(audience: PublicKey) -> Self { - Self { - audience, - inner: Arc::new(Mutex::new(Vec::new())), - } - } - - pub fn verify(&self, bytes: &[u8], signer: &PublicKey) -> Result<(), AuthnSignatureError> { - self.gc(); - - if bytes.len() != 96 { - return Err(AuthnSignatureError::InvalidLength(bytes.len())); - } - - let signature_bytes: SignatureBytes = bytes[0..64] - .try_into() - .expect("validate token length on instantiating"); - let signature = Signature::from(signature_bytes); - - let token_hash: [u8; 32] = bytes[64..].try_into().expect("should not be reachable"); - - let now = Timestamp::now().into_inner(); - let past = now - TIME_INTERVAL; - let future = now + TIME_INTERVAL; - - let result = verify_at(now, self, &signature, signer, &token_hash); - - match result { - Ok(_) => return Ok(()), - Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - _ => {} - } - - let result = verify_at(past, self, &signature, signer, &token_hash); - - match result { - Ok(_) => return Ok(()), - Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed), - _ => {} - } - - verify_at(future, self, &signature, signer, &token_hash) - } - - // === Private Methods === - - /// Remove all tokens older than two time intervals in the past. - fn gc(&self) { - let threshold = ((Timestamp::now().into_inner() / TIME_INTERVAL) - 2).to_be_bytes(); - - let mut inner = self.inner.lock().unwrap(); - - match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) { - Ok(index) | Err(index) => { - inner.drain(0..index); - } - } - } -} - -fn verify_at( - time: u64, - verifier: &AuthnVerifier, - signature: &Signature, - signer: &PublicKey, - token_hash: &[u8; 32], -) -> Result<(), AuthnSignatureError> { - let time_step = time / TIME_INTERVAL; - let time_step_bytes = time_step.to_be_bytes(); - - let result = signer.verify( - &signable(&time_step_bytes, signer, &verifier.audience, token_hash), - signature, - ); - - if result.is_ok() { - let mut inner = verifier.inner.lock().unwrap(); - - let mut candidate = [0_u8; 40]; - candidate[..8].copy_from_slice(&time_step_bytes); - candidate[8..].copy_from_slice(token_hash); - - match inner.binary_search_by(|element| element.cmp(&candidate)) { - Ok(index) | Err(index) => { - inner.insert(index, candidate); - } - }; - - return Ok(()); - } - - Err(AuthnSignatureError::InvalidSignature) -} - -fn signable( - time_step_bytes: &[u8; 8], - signer: &PublicKey, - audience: &PublicKey, - token_hash: &[u8; 32], -) -> [u8; 115] { - let mut arr = [0; 115]; - - arr[..11].copy_from_slice(crate::namespaces::PUBKY_AUTHN); - arr[19..51].copy_from_slice(signer.as_bytes()); - arr[11..19].copy_from_slice(time_step_bytes); - arr[51..83].copy_from_slice(audience.as_bytes()); - arr[83..].copy_from_slice(token_hash); - - arr -} - -#[derive(thiserror::Error, Debug)] -pub enum AuthnSignatureError { - #[error("AuthnSignature should be 96 bytes long, got {0} bytes instead")] - InvalidLength(usize), - - #[error("Invalid signature")] - InvalidSignature, - - #[error("Authn signature already used")] - AlreadyUsed, -} - #[cfg(test)] mod tests { use crate::{auth::TIMESTAMP_WINDOW, crypto::Keypair, timestamp::Timestamp}; @@ -427,4 +266,23 @@ mod tests { assert_eq!(result, Err(Error::Expired)); } + + #[test] + fn already_used() { + let signer = Keypair::random(); + let audience = Keypair::random().public_key(); + let scopes = vec!["*:*".to_string()]; + + let verifier = AuthVerifier::new(audience.clone()); + + let token = AuthToken::sign(&signer, &audience, scopes.clone()); + + let serialized = &token.serialize(); + + verifier.verify(serialized).unwrap(); + + assert_eq!(token.scopes, scopes); + + assert_eq!(verifier.verify(serialized), Err(Error::AlreadyUsed)); + } } diff --git a/pubky-homeserver/src/error.rs b/pubky-homeserver/src/error.rs index 5fad2f0..b6e5a14 100644 --- a/pubky-homeserver/src/error.rs +++ b/pubky-homeserver/src/error.rs @@ -5,7 +5,6 @@ use axum::{ http::StatusCode, response::IntoResponse, }; -use pubky_common::auth::AuthnSignatureError; pub type Result = core::result::Result; @@ -71,8 +70,8 @@ impl From for Error { // === Pubky specific errors === -impl From for Error { - fn from(error: AuthnSignatureError) -> Self { +impl From for Error { + fn from(error: pubky_common::auth::Error) -> Self { Self::new(StatusCode::BAD_REQUEST, Some(error)) } } diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 35615fd..085a5c5 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -18,9 +18,9 @@ mod root; fn base(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) - .route("/:pubky", put(auth::signup)) + .route("/signup", post(auth::signup)) + .route("/session", post(auth::signin)) .route("/:pubky/session", get(auth::session)) - .route("/:pubky/session", post(auth::signin)) .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index 72246f9..5f035e2 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -25,13 +25,12 @@ pub async fn signup( State(state): State, user_agent: Option>, cookies: Cookies, - pubky: Pubky, uri: Uri, body: Bytes, ) -> Result { // TODO: Verify invitation link. // TODO: add errors in case of already axisting user. - signin(State(state), user_agent, cookies, pubky, uri, body).await + signin(State(state), user_agent, cookies, uri, body).await } pub async fn session( @@ -90,13 +89,12 @@ pub async fn signin( State(state): State, user_agent: Option>, cookies: Cookies, - pubky: Pubky, uri: Uri, body: Bytes, ) -> Result { - let public_key = pubky.public_key(); + let token = state.verifier.verify(&body)?; - state.verifier.verify(&body, public_key)?; + let public_key = token.subject(); let mut wtxn = state.db.env.write_txn()?; let users: UsersTable = state diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index cdc352c..4ef528e 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,7 +1,7 @@ use std::{future::IntoFuture, net::SocketAddr}; use anyhow::{Error, Result}; -use pubky_common::auth::AuthnVerifier; +use pubky_common::auth::AuthVerifier; use tokio::{net::TcpListener, signal, task::JoinSet}; use tracing::{debug, info, warn}; @@ -21,7 +21,7 @@ pub struct Homeserver { #[derive(Clone, Debug)] pub(crate) struct AppState { - pub verifier: AuthnVerifier, + pub verifier: AuthVerifier, pub db: DB, pub pkarr_client: PkarrClientAsync, } @@ -46,7 +46,7 @@ impl Homeserver { .as_async(); let state = AppState { - verifier: AuthnVerifier::new(public_key.clone()), + verifier: AuthVerifier::new(public_key.clone()), db, pkarr_client: pkarr_client.clone(), }; diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index d4a6436..072461c 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,7 +1,7 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; -use pubky_common::{auth::AuthnSignature, session::Session}; +use pubky_common::{auth::AuthToken, session::Session}; use crate::{error::Result, PubkyClient}; @@ -19,21 +19,17 @@ impl PubkyClient { ) -> Result<()> { let homeserver = homeserver.to_string(); - let public_key = &keypair.public_key(); - let Endpoint { public_key: audience, mut url, } = self.resolve_endpoint(&homeserver).await?; - url.set_path(&format!("/{}", public_key)); + url.set_path(&format!("/signup")); - let body = AuthnSignature::generate(keypair, &audience) - .as_bytes() - .to_owned(); + let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); let response = self - .request(Method::PUT, url.clone()) + .request(Method::POST, url.clone()) .body(body) .send() .await?; @@ -91,11 +87,9 @@ impl PubkyClient { mut url, } = self.resolve_pubky_homeserver(&pubky).await?; - url.set_path(&format!("/{}/session", &pubky)); + url.set_path(&format!("/session")); - let body = AuthnSignature::generate(keypair, &audience) - .as_bytes() - .to_owned(); + let body = AuthToken::sign(keypair, &audience, vec![]).serialize(); let response = self.request(Method::POST, url).body(body).send().await?;