From f6e20a6d4f9a6e90d0ab9ee85f8253a6a909c2bc Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Sun, 15 Feb 2026 20:49:12 +0700 Subject: [PATCH] feat: mvp auth protocol --- crates/distribution/src/auth.rs | 145 ++++++++++++++++++++++++++++++ crates/distribution/src/crypto.rs | 16 ++++ crates/distribution/src/lib.rs | 1 + 3 files changed, 162 insertions(+) create mode 100644 crates/distribution/src/auth.rs diff --git a/crates/distribution/src/auth.rs b/crates/distribution/src/auth.rs new file mode 100644 index 0000000..46a7ba3 --- /dev/null +++ b/crates/distribution/src/auth.rs @@ -0,0 +1,145 @@ +use std::collections::HashSet; +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use crate::types::{NodeId, Signature}; + +// ─── ContentHash ──────────────────────────────────────────────────────────── + +/// A 32-byte blake3 digest used as content address for chunks and manifests. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ContentHash(pub [u8; 32]); + +impl fmt::Debug for ContentHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ContentHash(")?; + for b in &self.0[..4] { + write!(f, "{:02x}", b)?; + } + write!(f, "\u{2026})") + } +} + +// ─── DatastoreAction ──────────────────────────────────────────────────────── + +/// An action a client wants to perform on the datastore. +/// +/// Carried inside a `SignedRequestPayload` for browser-relay auth (Auth Path 2). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DatastoreAction { + Put { + path: String, + content_hash: ContentHash, + size_bytes: u64, + tags: std::collections::BTreeMap, + }, + Get { + path: String, + }, + Delete { + path: String, + }, + List { + prefix: Option, + }, +} + +// ─── SignedRequestPayload ─────────────────────────────────────────────────── + +/// The signable payload of a client request. +/// +/// Serialized canonically (serde_json) and signed by the client's ed25519 key. +/// Includes timestamp and nonce for replay protection. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedRequestPayload { + pub action: DatastoreAction, + /// Unix timestamp in seconds. + pub timestamp: u64, + /// 16 random bytes — prevents replay within the timestamp window. + pub nonce: [u8; 16], +} + +// ─── SignedRequest ────────────────────────────────────────────────────────── + +/// A signed request envelope for browser-relay auth (Auth Path 2). +/// +/// The relay forwards this opaquely — it cannot forge, modify, or replay it. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedRequest { + pub payload: SignedRequestPayload, + /// The client's ed25519 public key. + pub public_key: NodeId, + /// ed25519 signature over the canonical serialization of `payload`. + pub signature: Signature, +} + +// ─── AccessControlList ────────────────────────────────────────────────────── + +/// The datastore's access control list. +/// +/// Persisted as `acl.json` alongside the datastore's `storage_path`. +/// The owner always has implicit full access. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AccessControlList { + /// The datastore owner's public key — always has full access. + pub owner: NodeId, + /// Explicitly authorized client keys. + pub authorized_keys: HashSet, +} + +// ─── AuthzResult ──────────────────────────────────────────────────────────── + +/// The outcome of an authorization check. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthzResult { + Allowed, + Denied(DeniedReason), +} + +/// Why a request was denied. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DeniedReason { + /// The key is not in the ACL. + NotAuthorized, + /// The ed25519 signature is invalid. + InvalidSignature, + /// The request timestamp is outside the ±300s window. + RequestExpired, + /// The nonce has already been seen within the time window. + ReplayDetected, +} + +// ─── AuthzEngine ──────────────────────────────────────────────────────────── + +/// Authorization engine — checks requests against the ACL and replay state. +/// +/// Sits at the edge of the actor system (Auth Gate) and decides whether +/// to accept or reject external requests before they reach the actors. +#[derive(Debug)] +pub struct AuthzEngine { + pub acl: AccessControlList, + // Nonce tracking and other runtime state will be added during implementation. +} + +impl AuthzEngine { + /// Check whether a `NodeId` is authorized (connection-level, Auth Path 1). + pub fn check_node(&self, _node_id: &NodeId) -> AuthzResult { + todo!() + } + + /// Verify and authorize a signed request (Auth Path 2). + pub fn check_signed_request(&self, _request: &SignedRequest) -> AuthzResult { + todo!() + } + + /// Grant access to a `NodeId`. Owner-only operation. + pub fn grant(&mut self, _requester: &NodeId, _key: NodeId) -> Result<(), DeniedReason> { + todo!() + } + + /// Revoke access from a `NodeId`. Owner-only operation. + pub fn revoke(&mut self, _requester: &NodeId, _key: NodeId) -> Result<(), DeniedReason> { + todo!() + } +} diff --git a/crates/distribution/src/crypto.rs b/crates/distribution/src/crypto.rs index 43a1a42..2c781b2 100644 --- a/crates/distribution/src/crypto.rs +++ b/crates/distribution/src/crypto.rs @@ -1,5 +1,6 @@ use ed25519_dalek::{Signer, Verifier}; +use crate::auth::{SignedRequest, SignedRequestPayload}; use crate::types::{DirectoryEntry, DirectoryEntryPayload, NodeId, Signature}; // ─── Keypair ──────────────────────────────────────────────────────────────── @@ -41,6 +42,13 @@ impl Keypair { Signature(sig.to_bytes()) } + /// Sign a request payload, returning a complete `SignedRequest` envelope. + /// + /// Used by clients for Auth Path 2 (browser relay). + pub fn sign_request(&self, _payload: &SignedRequestPayload) -> SignedRequest { + todo!() + } + /// Sign a directory entry payload, returning a complete `DirectoryEntry`. pub fn sign_directory_entry( &self, @@ -74,6 +82,14 @@ pub fn verify(node_id: &NodeId, msg: &[u8], sig: &Signature) -> bool { vk.verify(msg, &signature).is_ok() } +/// Verify a `SignedRequest`'s signature against its embedded `public_key`. +/// +/// Checks only signature validity — does NOT check timestamp, nonce, or ACL. +/// Use `AuthzEngine::check_signed_request` for full verification. +pub fn verify_signed_request(_request: &SignedRequest) -> bool { + todo!() +} + /// Verify a `DirectoryEntry`'s signature against its embedded `node_id`. pub fn verify_directory_entry(entry: &DirectoryEntry) -> bool { let payload = entry.payload(); diff --git a/crates/distribution/src/lib.rs b/crates/distribution/src/lib.rs index 503bbf0..3df3e92 100644 --- a/crates/distribution/src/lib.rs +++ b/crates/distribution/src/lib.rs @@ -14,3 +14,4 @@ pub mod snapshot; pub mod driver; #[cfg(feature = "iroh")] pub mod iroh_driver; +pub mod auth;