Skip to content

Commit

Permalink
Merge pull request #20 from pubky/feat/basic-auth
Browse files Browse the repository at this point in the history
Feat/basic auth
  • Loading branch information
Nuhvi authored Jul 22, 2024
2 parents ce14d06 + 5a6c7ae commit 0109ab4
Show file tree
Hide file tree
Showing 31 changed files with 4,173 additions and 1 deletion.
2,289 changes: 2,289 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = []
members = [ "pubky","pubky-*"]

# See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352
resolver = "2"
17 changes: 17 additions & 0 deletions pubky-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "pubky-common"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
base32 = "0.5.0"
blake3 = "1.5.1"
ed25519-dalek = "2.1.1"
once_cell = "1.19.0"
pkarr = "2.1.0"
rand = "0.8.5"
thiserror = "1.0.60"
postcard = { version = "1.0.8", features = ["alloc"] }
serde = { version = "1.0.204", features = ["derive"] }
220 changes: 220 additions & 0 deletions pubky-common/src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
//! Client-server Authentication using signed timesteps
use std::sync::{Arc, Mutex};

use ed25519_dalek::ed25519::SignatureBytes;

use crate::{
crypto::{random_hash, Keypair, PublicKey, Signature},
timestamp::Timestamp,
};

// 30 seconds
const TIME_INTERVAL: u64 = 30 * 1_000_000;

#[derive(Debug, PartialEq)]
pub struct AuthnSignature(Box<[u8]>);

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[11..19].copy_from_slice(time_step_bytes);
arr[19..51].copy_from_slice(signer.as_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::crypto::Keypair;

use super::{AuthnSignature, AuthnVerifier};

#[test]
fn sign_verify() {
let keypair = Keypair::random();
let signer = keypair.public_key();
let audience = Keypair::random().public_key();

let verifier = AuthnVerifier::new(audience.clone());

let authn_signature = AuthnSignature::generate(&keypair, &audience);

verifier
.verify(authn_signature.as_bytes(), &signer)
.unwrap();

{
// Invalid signable
let mut invalid = authn_signature.as_bytes().to_vec();
invalid[64..].copy_from_slice(&[0; 32]);

assert!(!verifier.verify(&invalid, &signer).is_ok())
}

{
// Invalid signer
let mut invalid = authn_signature.as_bytes().to_vec();
invalid[0..32].copy_from_slice(&[0; 32]);

assert!(!verifier.verify(&invalid, &signer).is_ok())
}
}
}
25 changes: 25 additions & 0 deletions pubky-common/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use rand::prelude::Rng;

pub use pkarr::{Keypair, PublicKey};

pub use ed25519_dalek::Signature;

pub type Hash = blake3::Hash;

pub use blake3::hash;

pub fn random_hash() -> Hash {
let mut rng = rand::thread_rng();
Hash::from_bytes(rng.gen())
}

pub fn random_bytes<const N: usize>() -> [u8; N] {
let mut rng = rand::thread_rng();
let mut arr = [0u8; N];

#[allow(clippy::needless_range_loop)]
for i in 0..N {
arr[i] = rng.gen();
}
arr
}
5 changes: 5 additions & 0 deletions pubky-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod auth;
pub mod crypto;
pub mod namespaces;
pub mod session;
pub mod timestamp;
1 change: 1 addition & 0 deletions pubky-common/src/namespaces.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const PUBKY_AUTHN: &[u8; 11] = b"PUBKY:AUTHN";
84 changes: 84 additions & 0 deletions pubky-common/src/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use postcard::{from_bytes, to_allocvec};
use serde::{Deserialize, Serialize};

extern crate alloc;
use alloc::vec::Vec;

use crate::timestamp::Timestamp;

// TODO: add IP address?
// TODO: use https://crates.io/crates/user-agent-parser to parse the session
// and get more informations from the user-agent.
#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Session {
pub version: usize,
pub created_at: u64,
/// User specified name, defaults to the user-agent.
pub name: String,
pub user_agent: String,
}

impl Session {
pub fn new() -> Self {
Self {
created_at: Timestamp::now().into_inner(),
..Default::default()
}
}

// === Setters ===

pub fn set_user_agent(&mut self, user_agent: String) -> &mut Self {
self.user_agent = user_agent;

if self.name.is_empty() {
self.name.clone_from(&self.user_agent)
}

self
}

// === Public Methods ===

pub fn serialize(&self) -> Vec<u8> {
to_allocvec(self).expect("Session::serialize")
}

pub fn deserialize(bytes: &[u8]) -> Result<Self> {
if bytes[0] > 0 {
return Err(Error::UnknownVersion);
}

Ok(from_bytes(bytes)?)
}
}

pub type Result<T> = core::result::Result<T, Error>;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Unknown version")]
UnknownVersion,
#[error(transparent)]
Postcard(#[from] postcard::Error),
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn serialize() {
let mut session = Session::default();

session.user_agent = "foo".to_string();

let serialized = session.serialize();

assert_eq!(serialized, [0, 0, 0, 3, 102, 111, 111,]);

let deseiralized = Session::deserialize(&serialized).unwrap();

assert_eq!(deseiralized, session)
}
}
Loading

0 comments on commit 0109ab4

Please sign in to comment.