lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit ac24ee227c4e82fd8eb1e18c252bb9fce348afff
parent c3508928e31e68ea720a9f470b3fb1ee0b21d313
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 00:57:33 -0700

authority: enforce contract draft authorization

- add actor, signer, contract, and draft authorization checks
- validate draft contract existence, kind, role, pubkey, and signed event identity
- cover listing, order, mismatch, and unknown-contract cases
- validate with cargo fmt, check, and tests for radroots_authority

Diffstat:
Acrates/authority/src/authorization.rs | 307+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/authority/src/error.rs | 40++++++++++++++++++++++++++++++++++++++++
Mcrates/authority/src/lib.rs | 5+++++
3 files changed, 352 insertions(+), 0 deletions(-)

diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs @@ -0,0 +1,307 @@ +#![forbid(unsafe_code)] + +use crate::{RadrootsActorContext, RadrootsAuthorityError, RadrootsEventSigner}; +use radroots_events::contract::{RadrootsEventContract, event_contract}; +use radroots_events::draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}; + +#[cfg(not(feature = "std"))] +use alloc::borrow::ToOwned; +#[cfg(feature = "std")] +use std::borrow::ToOwned; + +pub fn authorize_actor_for_contract( + actor: &RadrootsActorContext, + contract: &RadrootsEventContract, +) -> Result<(), RadrootsAuthorityError> { + if actor.satisfies(contract.author_role) { + Ok(()) + } else { + Err(RadrootsAuthorityError::ActorRoleUnsatisfied { + contract_id: contract.id.to_owned(), + required_role: contract.author_role, + }) + } +} + +pub fn authorize_actor_for_draft( + actor: &RadrootsActorContext, + draft: &RadrootsFrozenEventDraft, +) -> Result<&'static RadrootsEventContract, RadrootsAuthorityError> { + let contract = event_contract(draft.contract_id.as_str()).ok_or_else(|| { + RadrootsAuthorityError::UnknownContract { + contract_id: draft.contract_id.clone(), + } + })?; + if contract.kind != draft.kind { + return Err(RadrootsAuthorityError::DraftKindMismatch { + contract_id: draft.contract_id.clone(), + expected_kind: contract.kind, + actual_kind: draft.kind, + }); + } + authorize_actor_for_contract(actor, contract)?; + if actor.pubkey.as_str() != draft.expected_pubkey.as_str() { + return Err(RadrootsAuthorityError::ActorPubkeyMismatch { + expected_pubkey: draft.expected_pubkey.clone(), + actor_pubkey: actor.pubkey.as_str().to_owned(), + }); + } + Ok(contract) +} + +pub fn authorize_signer_for_draft<S>( + signer: &S, + draft: &RadrootsFrozenEventDraft, +) -> Result<(), RadrootsAuthorityError> +where + S: RadrootsEventSigner + ?Sized, +{ + if signer.pubkey().as_str() == draft.expected_pubkey.as_str() { + Ok(()) + } else { + Err(RadrootsAuthorityError::SignerPubkeyMismatch { + expected_pubkey: draft.expected_pubkey.clone(), + signer_pubkey: signer.pubkey().as_str().to_owned(), + }) + } +} + +pub fn sign_authorized_draft<S>( + actor: &RadrootsActorContext, + signer: &S, + draft: &RadrootsFrozenEventDraft, +) -> Result<RadrootsSignedNostrEvent, RadrootsAuthorityError> +where + S: RadrootsEventSigner + ?Sized, +{ + authorize_actor_for_draft(actor, draft)?; + authorize_signer_for_draft(signer, draft)?; + let signed_event = signer.sign_frozen_draft(draft)?; + if signed_event.pubkey.as_str() != draft.expected_pubkey.as_str() { + return Err(RadrootsAuthorityError::SignedEventPubkeyMismatch { + expected_pubkey: draft.expected_pubkey.clone(), + actual_pubkey: signed_event.pubkey, + }); + } + if signed_event.id.as_str() != draft.expected_event_id.as_str() { + return Err(RadrootsAuthorityError::SignedEventIdMismatch { + expected_event_id: draft.expected_event_id.clone(), + actual_event_id: signed_event.id, + }); + } + Ok(signed_event) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RadrootsSignerError; + use radroots_events::contract::{RadrootsActorRole, event_contract}; + use radroots_events::ids::RadrootsPublicKey; + use radroots_events::kinds::{KIND_LISTING, KIND_ORDER_REQUEST, KIND_POST}; + + fn hex_64(character: char) -> String { + std::iter::repeat_n(character, 64).collect() + } + + fn hex_128(character: char) -> String { + std::iter::repeat_n(character, 128).collect() + } + + fn seller_actor(pubkey: &str) -> RadrootsActorContext { + RadrootsActorContext::with_roles(pubkey, [RadrootsActorRole::Seller]).expect("seller") + } + + fn buyer_actor(pubkey: &str) -> RadrootsActorContext { + RadrootsActorContext::with_roles(pubkey, [RadrootsActorRole::Buyer]).expect("buyer") + } + + fn listing_draft(pubkey: &str) -> RadrootsFrozenEventDraft { + RadrootsFrozenEventDraft::new( + "radroots.listing.published.v1", + KIND_LISTING, + 1_700_000_000, + vec![vec!["d".to_owned(), "listing-a".to_owned()]], + "{}", + pubkey, + ) + .expect("listing draft") + } + + struct StaticSigner { + pubkey: RadrootsPublicKey, + event_id: Option<String>, + } + + impl StaticSigner { + fn new(pubkey: &str) -> Self { + Self { + pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), + event_id: None, + } + } + + fn with_event_id(pubkey: &str, event_id: String) -> Self { + Self { + pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), + event_id: Some(event_id), + } + } + } + + impl RadrootsEventSigner for StaticSigner { + fn pubkey(&self) -> &RadrootsPublicKey { + &self.pubkey + } + + fn sign_frozen_draft( + &self, + draft: &RadrootsFrozenEventDraft, + ) -> Result<RadrootsSignedNostrEvent, RadrootsSignerError> { + RadrootsSignedNostrEvent::new( + self.event_id + .as_deref() + .unwrap_or(draft.expected_event_id.as_str()), + self.pubkey.as_str(), + draft.created_at, + draft.kind, + draft.tags.clone(), + draft.content.as_str(), + hex_128('f'), + "{}", + ) + .map_err(|error| RadrootsSignerError::SigningFailed { + message: error.to_string(), + }) + } + } + + #[test] + fn buyer_and_seller_contract_roles_match_current_contracts() { + let listing = event_contract("radroots.listing.published.v1").expect("listing contract"); + let order_request = event_contract("radroots.order.request.v1").expect("order contract"); + let seller = seller_actor(hex_64('a').as_str()); + let buyer = buyer_actor(hex_64('b').as_str()); + + assert_eq!(listing.author_role, RadrootsActorRole::Seller); + assert!(authorize_actor_for_contract(&seller, listing).is_ok()); + assert!(matches!( + authorize_actor_for_contract(&buyer, listing), + Err(RadrootsAuthorityError::ActorRoleUnsatisfied { .. }) + )); + assert!(authorize_actor_for_contract(&buyer, order_request).is_ok()); + assert!(matches!( + authorize_actor_for_contract(&seller, order_request), + Err(RadrootsAuthorityError::ActorRoleUnsatisfied { .. }) + )); + } + + #[test] + fn actor_pubkey_mismatch_fails() { + let draft = listing_draft(hex_64('a').as_str()); + let actor = seller_actor(hex_64('b').as_str()); + + assert!(matches!( + authorize_actor_for_draft(&actor, &draft), + Err(RadrootsAuthorityError::ActorPubkeyMismatch { .. }) + )); + } + + #[test] + fn signer_pubkey_mismatch_fails() { + let draft = listing_draft(hex_64('a').as_str()); + let signer = StaticSigner::new(hex_64('b').as_str()); + + assert!(matches!( + authorize_signer_for_draft(&signer, &draft), + Err(RadrootsAuthorityError::SignerPubkeyMismatch { .. }) + )); + } + + #[test] + fn unknown_contract_and_kind_mismatch_fail() { + let actor = seller_actor(hex_64('a').as_str()); + let unknown = RadrootsFrozenEventDraft { + contract_id: "radroots.unknown.v1".to_owned(), + contract_registry_version: 1, + kind: KIND_LISTING, + created_at: 1_700_000_000, + tags: Vec::new(), + content: "{}".to_owned(), + expected_pubkey: hex_64('a'), + expected_event_id: hex_64('e'), + }; + assert!(matches!( + authorize_actor_for_draft(&actor, &unknown), + Err(RadrootsAuthorityError::UnknownContract { .. }) + )); + + let wrong_kind = RadrootsFrozenEventDraft { + contract_id: "radroots.listing.published.v1".to_owned(), + contract_registry_version: 1, + kind: KIND_POST, + created_at: 1_700_000_000, + tags: Vec::new(), + content: "{}".to_owned(), + expected_pubkey: hex_64('a'), + expected_event_id: hex_64('e'), + }; + assert!(matches!( + authorize_actor_for_draft(&actor, &wrong_kind), + Err(RadrootsAuthorityError::DraftKindMismatch { + expected_kind: KIND_LISTING, + actual_kind: KIND_POST, + .. + }) + )); + } + + #[test] + fn signed_event_id_mismatch_fails() { + let pubkey = hex_64('a'); + let draft = listing_draft(pubkey.as_str()); + let actor = seller_actor(pubkey.as_str()); + let signer = StaticSigner::with_event_id(pubkey.as_str(), hex_64('e')); + + assert!(matches!( + sign_authorized_draft(&actor, &signer, &draft), + Err(RadrootsAuthorityError::SignedEventIdMismatch { .. }) + )); + } + + #[test] + fn authorized_actor_and_signer_return_signed_event() { + let pubkey = hex_64('a'); + let draft = listing_draft(pubkey.as_str()); + let actor = seller_actor(pubkey.as_str()); + let signer = StaticSigner::new(pubkey.as_str()); + + let signed = sign_authorized_draft(&actor, &signer, &draft).expect("signed"); + + assert_eq!(signed.id, draft.expected_event_id); + assert_eq!(signed.pubkey, draft.expected_pubkey); + assert_eq!(signed.kind, KIND_LISTING); + } + + #[test] + fn order_request_draft_requires_buyer_role() { + let pubkey = hex_64('a'); + let draft = RadrootsFrozenEventDraft::new( + "radroots.order.request.v1", + KIND_ORDER_REQUEST, + 1_700_000_000, + Vec::new(), + "{}", + pubkey.as_str(), + ) + .expect("order draft"); + let buyer = buyer_actor(pubkey.as_str()); + let seller = seller_actor(pubkey.as_str()); + + assert!(authorize_actor_for_draft(&buyer, &draft).is_ok()); + assert!(matches!( + authorize_actor_for_draft(&seller, &draft), + Err(RadrootsAuthorityError::ActorRoleUnsatisfied { .. }) + )); + } +} diff --git a/crates/authority/src/error.rs b/crates/authority/src/error.rs @@ -15,6 +15,46 @@ pub enum RadrootsAuthorityError { #[error("invalid signer public key")] InvalidSignerPubkey, + #[error("unknown event contract `{contract_id}`")] + UnknownContract { contract_id: String }, + + #[error("event contract `{contract_id}` expects kind {expected_kind}, got {actual_kind}")] + DraftKindMismatch { + contract_id: String, + expected_kind: u32, + actual_kind: u32, + }, + + #[error("actor does not satisfy role {required_role:?} for contract `{contract_id}`")] + ActorRoleUnsatisfied { + contract_id: String, + required_role: radroots_events::contract::RadrootsActorRole, + }, + + #[error("actor pubkey mismatch: expected {expected_pubkey}, got {actor_pubkey}")] + ActorPubkeyMismatch { + expected_pubkey: String, + actor_pubkey: String, + }, + + #[error("signer pubkey mismatch: expected {expected_pubkey}, got {signer_pubkey}")] + SignerPubkeyMismatch { + expected_pubkey: String, + signer_pubkey: String, + }, + + #[error("signed event pubkey mismatch: expected {expected_pubkey}, got {actual_pubkey}")] + SignedEventPubkeyMismatch { + expected_pubkey: String, + actual_pubkey: String, + }, + + #[error("signed event id mismatch: expected {expected_event_id}, got {actual_event_id}")] + SignedEventIdMismatch { + expected_event_id: String, + actual_event_id: String, + }, + #[error("signer error: {0}")] Signer(#[from] RadrootsSignerError), } diff --git a/crates/authority/src/lib.rs b/crates/authority/src/lib.rs @@ -6,6 +6,7 @@ extern crate alloc; pub mod actor; +pub mod authorization; pub mod error; pub mod signer; @@ -13,5 +14,9 @@ pub use actor::{ RadrootsActorContext, RadrootsActorResolutionRequest, RadrootsActorSelector, RadrootsActorSource, role_satisfies, }; +pub use authorization::{ + authorize_actor_for_contract, authorize_actor_for_draft, authorize_signer_for_draft, + sign_authorized_draft, +}; pub use error::{RadrootsAuthorityError, RadrootsSignerError}; pub use signer::{RadrootsEventSigner, RadrootsSignerIdentity};