feat: mvp auth protocol

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-02-15 20:49:12 +07:00
parent f3bd9685ba
commit f6e20a6d4f
3 changed files with 162 additions and 0 deletions

View file

@ -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<String, String>,
},
Get {
path: String,
},
Delete {
path: String,
},
List {
prefix: Option<String>,
},
}
// ─── 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<NodeId>,
}
// ─── 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!()
}
}

View file

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

View file

@ -14,3 +14,4 @@ pub mod snapshot;
pub mod driver;
#[cfg(feature = "iroh")]
pub mod iroh_driver;
pub mod auth;