-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from pubky/feat/basic-auth
Feat/basic auth
- Loading branch information
Showing
31 changed files
with
4,173 additions
and
1 deletion.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub const PUBKY_AUTHN: &[u8; 11] = b"PUBKY:AUTHN"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.