feat: mvp auth protocol
This commit is contained in:
parent
f3bd9685ba
commit
f6e20a6d4f
3 changed files with 162 additions and 0 deletions
145
crates/distribution/src/auth.rs
Normal file
145
crates/distribution/src/auth.rs
Normal 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!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use ed25519_dalek::{Signer, Verifier};
|
use ed25519_dalek::{Signer, Verifier};
|
||||||
|
|
||||||
|
use crate::auth::{SignedRequest, SignedRequestPayload};
|
||||||
use crate::types::{DirectoryEntry, DirectoryEntryPayload, NodeId, Signature};
|
use crate::types::{DirectoryEntry, DirectoryEntryPayload, NodeId, Signature};
|
||||||
|
|
||||||
// ─── Keypair ────────────────────────────────────────────────────────────────
|
// ─── Keypair ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -41,6 +42,13 @@ impl Keypair {
|
||||||
Signature(sig.to_bytes())
|
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`.
|
/// Sign a directory entry payload, returning a complete `DirectoryEntry`.
|
||||||
pub fn sign_directory_entry(
|
pub fn sign_directory_entry(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -74,6 +82,14 @@ pub fn verify(node_id: &NodeId, msg: &[u8], sig: &Signature) -> bool {
|
||||||
vk.verify(msg, &signature).is_ok()
|
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`.
|
/// Verify a `DirectoryEntry`'s signature against its embedded `node_id`.
|
||||||
pub fn verify_directory_entry(entry: &DirectoryEntry) -> bool {
|
pub fn verify_directory_entry(entry: &DirectoryEntry) -> bool {
|
||||||
let payload = entry.payload();
|
let payload = entry.payload();
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@ pub mod snapshot;
|
||||||
pub mod driver;
|
pub mod driver;
|
||||||
#[cfg(feature = "iroh")]
|
#[cfg(feature = "iroh")]
|
||||||
pub mod iroh_driver;
|
pub mod iroh_driver;
|
||||||
|
pub mod auth;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue