mirror of
https://github.com/aljazceru/pubky-core.git
synced 2026-01-07 08:14:20 +01:00
feat(common): add AuthToken with scopes
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -336,9 +336,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.1"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
|
||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
@@ -1401,10 +1401,10 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkarr"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8"
|
||||
version = "2.2.0"
|
||||
source = "git+https://github.com/Pubky/pkarr?branch=v3#17975121c809d97dcad907fbb2ffc782e994d270"
|
||||
dependencies = [
|
||||
"base32",
|
||||
"bytes",
|
||||
"document-features",
|
||||
"dyn-clone",
|
||||
@@ -1416,13 +1416,13 @@ dependencies = [
|
||||
"mainline",
|
||||
"rand",
|
||||
"self_cell",
|
||||
"serde",
|
||||
"simple-dns",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"z32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1753,9 +1753,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.204"
|
||||
version = "1.0.209"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
|
||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -1781,9 +1781,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.204"
|
||||
version = "1.0.209"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2616,12 +2616,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "z32"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
|
||||
@@ -4,6 +4,10 @@ members = [ "pubky","pubky-*"]
|
||||
# See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
pkarr = { git = "https://github.com/Pubky/pkarr", branch = "v3", package = "pkarr", features = ["async"] }
|
||||
serde = { version = "^1.0.209", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
@@ -10,13 +10,13 @@ base32 = "0.5.0"
|
||||
blake3 = "1.5.1"
|
||||
ed25519-dalek = "2.1.1"
|
||||
once_cell = "1.19.0"
|
||||
pkarr = "2.1.0"
|
||||
pkarr = { workspace = true }
|
||||
rand = "0.8.5"
|
||||
thiserror = "1.0.60"
|
||||
postcard = { version = "1.0.8", features = ["alloc"] }
|
||||
crypto_secretbox = { version = "0.1.1", features = ["std"] }
|
||||
|
||||
serde = { version = "1.0.204", features = ["derive"], optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = "0.3.69"
|
||||
@@ -26,7 +26,7 @@ postcard = "1.0.8"
|
||||
|
||||
[features]
|
||||
|
||||
serde = ["dep:serde", "ed25519-dalek/serde"]
|
||||
serde = ["dep:serde", "ed25519-dalek/serde", "pkarr/serde"]
|
||||
full = ['serde']
|
||||
|
||||
default = ['full']
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ed25519_dalek::ed25519::SignatureBytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
crypto::{random_hash, Keypair, PublicKey, Signature},
|
||||
@@ -12,9 +13,184 @@ use crate::{
|
||||
// 30 seconds
|
||||
const TIME_INTERVAL: u64 = 30 * 1_000_000;
|
||||
|
||||
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].
|
||||
///
|
||||
/// Version 0: Signer is implicitly the same as the [AuthToken::subject]
|
||||
version: u8,
|
||||
/// The Pubky of the party verifying the [AuthToken], for example a web server.
|
||||
audience: PublicKey,
|
||||
/// Timestamp
|
||||
timestamp: Timestamp,
|
||||
/// The [PublicKey] of the owner of the resources being accessed by this token.
|
||||
subject: PublicKey,
|
||||
/// Signature over the token.
|
||||
signature: Signature,
|
||||
// Variable length scopes
|
||||
scopes: Vec<String>,
|
||||
}
|
||||
|
||||
impl AuthToken {
|
||||
pub fn new(signer: &Keypair, audience: &PublicKey, scopes: Vec<String>) -> Self {
|
||||
let timestamp = Timestamp::now();
|
||||
|
||||
let signature = signer.sign(&AuthToken::signable(audience, ×tamp, &scopes));
|
||||
|
||||
Self {
|
||||
version: 0,
|
||||
subject: signer.public_key(),
|
||||
audience: audience.to_owned(),
|
||||
timestamp,
|
||||
scopes,
|
||||
signature,
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(audience: &PublicKey, bytes: &[u8]) -> Result<Self, Error> {
|
||||
if bytes[0] > CURRENT_VERSION {
|
||||
return Err(Error::UnknownVersion);
|
||||
}
|
||||
|
||||
let token: AuthToken = postcard::from_bytes(bytes)?;
|
||||
|
||||
let now = Timestamp::now();
|
||||
|
||||
match token.version {
|
||||
0 => {
|
||||
if &token.audience != audience {
|
||||
return Err(Error::InvalidAudience(
|
||||
audience.to_string(),
|
||||
token.audience.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Chcek timestamp;
|
||||
let diff = token.timestamp.difference(&now);
|
||||
if diff > TIMESTAMP_WINDOW {
|
||||
return Err(Error::TooFarInTheFuture);
|
||||
}
|
||||
if diff < -TIMESTAMP_WINDOW {
|
||||
return Err(Error::Expired);
|
||||
}
|
||||
|
||||
token
|
||||
.subject
|
||||
.verify(
|
||||
&AuthToken::signable(&token.audience, &token.timestamp, &token.scopes),
|
||||
&token.signature,
|
||||
)
|
||||
.map_err(|_| Error::InvalidSignature)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn signable(audience: &PublicKey, timestamp: &Timestamp, scopes: &Vec<String>) -> Vec<u8> {
|
||||
let serialized_scopes = &postcard::to_allocvec(&scopes).unwrap();
|
||||
|
||||
let mut signable = Vec::with_capacity(1 + 32 + 8 + serialized_scopes.len());
|
||||
|
||||
signable.extend_from_slice(&[CURRENT_VERSION]);
|
||||
signable.extend_from_slice(audience.as_bytes());
|
||||
signable.extend_from_slice(×tamp.to_bytes());
|
||||
signable.extend_from_slice(serialized_scopes);
|
||||
|
||||
signable
|
||||
}
|
||||
|
||||
/// A unique ID for this [AuthToken], which is a concatenation of
|
||||
/// [AuthToken::subject] and [AuthToken::timestamp].
|
||||
///
|
||||
/// Assuming that [AuthToken::timestamp] is unique for every [AuthToken::subject].
|
||||
pub fn id(&self) -> [u8; 40] {
|
||||
let mut id = [0u8; 40];
|
||||
id[0..32].copy_from_slice(&self.subject.to_bytes());
|
||||
id[32..].copy_from_slice(&self.timestamp.to_bytes());
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Result<Vec<u8>, postcard::Error> {
|
||||
postcard::to_allocvec(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthVerifier {
|
||||
audience: PublicKey,
|
||||
seen: Arc<Mutex<Vec<[u8; 40]>>>,
|
||||
}
|
||||
|
||||
impl AuthVerifier {
|
||||
pub fn new(audience: PublicKey) -> Self {
|
||||
Self {
|
||||
audience,
|
||||
seen: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(&self, bytes: &[u8]) -> Result<AuthToken, Error> {
|
||||
self.gc();
|
||||
|
||||
let token = AuthToken::verify(&self.audience, bytes)?;
|
||||
|
||||
// Err(AuthnSignatureError::AlreadyUsed) => return Err(AuthnSignatureError::AlreadyUsed),
|
||||
|
||||
let mut seen = self.seen.lock().unwrap();
|
||||
|
||||
let id = token.id();
|
||||
|
||||
match seen.binary_search_by(|element| element.cmp(&id)) {
|
||||
Ok(index) | Err(index) => {
|
||||
seen.insert(index, id);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
// === 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.seen.lock().unwrap();
|
||||
|
||||
match inner.binary_search_by(|element| element[0..8].cmp(&threshold)) {
|
||||
Ok(index) | Err(index) => {
|
||||
inner.drain(0..index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("Unknown version")]
|
||||
UnknownVersion,
|
||||
#[error("Invalid audience. Expected {0}, got {1}")]
|
||||
InvalidAudience(String, String),
|
||||
#[error("AuthToken has a timestamp that is more than 45 seconds in the future")]
|
||||
TooFarInTheFuture,
|
||||
#[error("AuthToken has a timestamp that is more than 45 seconds in the past")]
|
||||
Expired,
|
||||
#[error("Invalid Signature")]
|
||||
InvalidSignature,
|
||||
#[error(transparent)]
|
||||
Postcard(#[from] postcard::Error),
|
||||
}
|
||||
|
||||
impl AuthnSignature {
|
||||
pub fn new(signer: &Keypair, audience: &PublicKey, token: Option<&[u8]>) -> Self {
|
||||
let mut bytes = Vec::with_capacity(96);
|
||||
@@ -183,38 +359,51 @@ pub enum AuthnSignatureError {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::crypto::Keypair;
|
||||
use crate::{auth::TIMESTAMP_WINDOW, crypto::Keypair, timestamp::Timestamp};
|
||||
|
||||
use super::{AuthnSignature, AuthnVerifier};
|
||||
use super::{AuthToken, AuthVerifier, Error};
|
||||
|
||||
#[test]
|
||||
fn sign_verify() {
|
||||
let keypair = Keypair::random();
|
||||
let signer = keypair.public_key();
|
||||
let signer = Keypair::random();
|
||||
let audience = Keypair::random().public_key();
|
||||
let scopes = vec!["*:*".to_string()];
|
||||
|
||||
let verifier = AuthnVerifier::new(audience.clone());
|
||||
let verifier = AuthVerifier::new(audience.clone());
|
||||
|
||||
let authn_signature = AuthnSignature::generate(&keypair, &audience);
|
||||
let token = AuthToken::new(&signer, &audience, scopes.clone());
|
||||
|
||||
verifier
|
||||
.verify(authn_signature.as_bytes(), &signer)
|
||||
.unwrap();
|
||||
verifier.verify(&token.serialize().unwrap()).unwrap();
|
||||
|
||||
{
|
||||
// Invalid signable
|
||||
let mut invalid = authn_signature.as_bytes().to_vec();
|
||||
invalid[64..].copy_from_slice(&[0; 32]);
|
||||
assert_eq!(token.scopes, scopes)
|
||||
}
|
||||
|
||||
assert!(verifier.verify(&invalid, &signer).is_err())
|
||||
}
|
||||
#[test]
|
||||
fn expired() {
|
||||
let signer = Keypair::random();
|
||||
let audience = Keypair::random().public_key();
|
||||
let scopes = vec!["*:*".to_string()];
|
||||
|
||||
{
|
||||
// Invalid signer
|
||||
let mut invalid = authn_signature.as_bytes().to_vec();
|
||||
invalid[0..32].copy_from_slice(&[0; 32]);
|
||||
let verifier = AuthVerifier::new(audience.clone());
|
||||
|
||||
assert!(verifier.verify(&invalid, &signer).is_err())
|
||||
}
|
||||
let timestamp = (&Timestamp::now()) - (TIMESTAMP_WINDOW as u64);
|
||||
|
||||
let signable = AuthToken::signable(&audience, ×tamp, &scopes);
|
||||
let signature = signer.sign(&signable);
|
||||
|
||||
let token = AuthToken {
|
||||
version: 0,
|
||||
subject: signer.public_key(),
|
||||
audience,
|
||||
timestamp,
|
||||
signature,
|
||||
scopes,
|
||||
};
|
||||
|
||||
let serialized = token.serialize().unwrap();
|
||||
|
||||
let result = verifier.verify(&serialized);
|
||||
|
||||
assert_eq!(result, Err(Error::Expired));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ impl Timestamp {
|
||||
self.0.to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn difference(&self, rhs: &Timestamp) -> u64 {
|
||||
self.0.abs_diff(rhs.0)
|
||||
pub fn difference(&self, rhs: &Timestamp) -> i64 {
|
||||
(self.0 as i64) - (rhs.0 as i64)
|
||||
}
|
||||
|
||||
pub fn into_inner(&self) -> u64 {
|
||||
|
||||
@@ -8,17 +8,17 @@ anyhow = "1.0.82"
|
||||
axum = { version = "0.7.5", features = ["macros"] }
|
||||
axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] }
|
||||
base32 = "0.5.1"
|
||||
bytes = "1.6.1"
|
||||
bytes = "^1.7.1"
|
||||
clap = { version = "4.5.11", features = ["derive"] }
|
||||
dirs-next = "2.0.0"
|
||||
flume = "0.11.0"
|
||||
futures-util = "0.3.30"
|
||||
heed = "0.20.3"
|
||||
hex = "0.4.3"
|
||||
pkarr = { version = "2.1.0", features = ["async"] }
|
||||
pkarr = { workspace = true }
|
||||
postcard = { version = "1.0.8", features = ["alloc"] }
|
||||
pubky-common = { version = "0.1.0", path = "../pubky-common" }
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
toml = "0.8.19"
|
||||
tower-cookies = "0.10.0"
|
||||
|
||||
@@ -14,17 +14,17 @@ crate-type = ["cdylib", "rlib"]
|
||||
thiserror = "1.0.62"
|
||||
wasm-bindgen = "0.2.92"
|
||||
url = "2.5.2"
|
||||
bytes = "1.6.1"
|
||||
bytes = "^1.7.1"
|
||||
|
||||
pubky-common = { version = "0.1.0", path = "../pubky-common" }
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
pkarr = { version="2.1.0", features = ["async"] }
|
||||
pkarr = { workspace = true, features = ["async"] }
|
||||
reqwest = { version = "0.12.5", features = ["cookies"], default-features = false }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
pkarr = { version = "2.1.0", default-features = false }
|
||||
pkarr = { workspace = true, default-features = false }
|
||||
reqwest = { version = "0.12.5", default-features = false }
|
||||
|
||||
js-sys = "0.3.69"
|
||||
|
||||
Reference in New Issue
Block a user