feat(homeserver): update signup and signin to use new AuthToken

This commit is contained in:
nazeh
2024-08-28 19:55:29 +03:00
parent f3469d34a4
commit 98910b40de
6 changed files with 45 additions and 196 deletions

View File

@@ -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<Self, Error> {
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<Mutex<Vec<[u8; 40]>>>,
// 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));
}
}

View File

@@ -5,7 +5,6 @@ use axum::{
http::StatusCode,
response::IntoResponse,
};
use pubky_common::auth::AuthnSignatureError;
pub type Result<T, E = Error> = core::result::Result<T, E>;
@@ -71,8 +70,8 @@ impl From<PathRejection> for Error {
// === Pubky specific errors ===
impl From<AuthnSignatureError> for Error {
fn from(error: AuthnSignatureError) -> Self {
impl From<pubky_common::auth::Error> for Error {
fn from(error: pubky_common::auth::Error) -> Self {
Self::new(StatusCode::BAD_REQUEST, Some(error))
}
}

View File

@@ -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))

View File

@@ -25,13 +25,12 @@ pub async fn signup(
State(state): State<AppState>,
user_agent: Option<TypedHeader<UserAgent>>,
cookies: Cookies,
pubky: Pubky,
uri: Uri,
body: Bytes,
) -> Result<impl IntoResponse> {
// 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<AppState>,
user_agent: Option<TypedHeader<UserAgent>>,
cookies: Cookies,
pubky: Pubky,
uri: Uri,
body: Bytes,
) -> Result<impl IntoResponse> {
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

View File

@@ -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(),
};

View File

@@ -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?;