Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/buzz-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub use nip_fi::{
IssuerJwksConfig, IssuerKeySource, IssuerPolicy, IssuerPolicyError, IssuerRegistry,
JwksFetchError, JwksFetcher, JwksSourceContract, NipFiMode, NipFiStartupError,
ProductionJwksSource, RevalidationDependencies, SubjectClass, SubjectClassContract, TokenClass,
TransportContractId, VerifiedAssertion, VerifierError, CLIENT_ATTACHED_HEADER,
TransportContractId, VerifiedAssertion, VerifierError, VerifyAssertion, CLIENT_ATTACHED_HEADER,
NOSTR_PUBKEY_CLAIM, OAUTH_CLIENT_ID_CLAIM,
};

Expand All @@ -61,6 +61,8 @@ pub use access::MockAccessChecker;
#[cfg(any(test, feature = "test-utils"))]
pub use nip98_replay::AlwaysFreshReplayGuard;
#[cfg(any(test, feature = "test-utils"))]
pub use nip_fi::StaticIssuerKeySource;
#[cfg(any(test, feature = "test-utils"))]
pub use rate_limit::AlwaysAllowRateLimiter;

/// How the connection was authenticated.
Expand Down
25 changes: 25 additions & 0 deletions crates/buzz-auth/src/nip_fi/assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,31 @@ impl VerifiedAssertion {
pub const fn revalidation_dependencies(&self) -> &RevalidationDependencies {
&self.revalidation_dependencies
}

/// Test-only constructor: mint a minimal `VerifiedAssertion` for a given
/// `asserted_key`. All other fields are set to safe, arbitrary defaults.
///
/// Used in unit tests that need to supply a `VerifiedAssertion` with a
/// specific `asserted_key` without performing a real JWKS verification.
#[cfg(any(test, feature = "test-utils"))]
pub fn new_for_test(asserted_key: nostr::PublicKey) -> Self {
use chrono::Duration;
Self::seal(
"https://test.issuer.example".to_owned(),
"test-subject".to_owned(),
Some(asserted_key),
CanonicalCapabilities::from_pairs(vec![]),
vec![Utc::now() + Duration::seconds(3600)],
AssertionPolicyId::zero(),
TransportContractId::zero(),
RevalidationDependencies::new(
"test-kid".to_owned(),
1,
Utc::now() + Duration::seconds(3600),
"test.header.sig".to_owned(),
),
)
}
}

impl fmt::Debug for VerifiedAssertion {
Expand Down
12 changes: 12 additions & 0 deletions crates/buzz-auth/src/nip_fi/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ impl AssertionPolicyId {
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}

/// All-zeros sentinel for use in tests only.
#[cfg(any(test, feature = "test-utils"))]
pub fn zero() -> Self {
Self([0u8; 32])
}
}

impl fmt::Debug for AssertionPolicyId {
Expand Down Expand Up @@ -144,6 +150,12 @@ impl TransportContractId {
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}

/// All-zeros sentinel for use in tests only.
#[cfg(any(test, feature = "test-utils"))]
pub fn zero() -> Self {
Self([0u8; 32])
}
}

impl fmt::Debug for TransportContractId {
Expand Down
7 changes: 6 additions & 1 deletion crates/buzz-auth/src/nip_fi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ pub use jwks::{
ProductionJwksSource,
};
pub use startup::{validate_nip_fi_config, NipFiMode, NipFiStartupError};
pub use verifier::{AssertionKeySet, FederatedAssertionVerifier, IssuerKeySource, VerifierError};
pub use verifier::{
AssertionKeySet, FederatedAssertionVerifier, IssuerKeySource, VerifierError, VerifyAssertion,
};

#[cfg(any(test, feature = "test-utils"))]
pub use verifier::StaticIssuerKeySource;
46 changes: 39 additions & 7 deletions crates/buzz-auth/src/nip_fi/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ impl AssertionKeySet {
})
}

/// Test-utils / test-only constructor: same validation as the crate-private
/// `new`, exposed under the `test-utils` Cargo feature and `cfg(test)` so
/// integration tests in dependent crates (e.g., `buzz-relay`) can build
/// snapshots for `StaticIssuerKeySource` without requiring a live JWKS fetch.
#[cfg(any(test, feature = "test-utils"))]
pub fn new_for_test(
issuer: String,
generation: u64,
jwks: JwkSet,
hard_deadline: DateTime<Utc>,
) -> Option<Self> {
Self::new(issuer, generation, jwks, hard_deadline)
}

/// The exact `iss` this snapshot authenticates.
pub fn issuer(&self) -> &str {
&self.issuer
Expand Down Expand Up @@ -209,20 +223,20 @@ impl<S: IssuerKeySource> IssuerKeySource for std::sync::Arc<S> {
/// reconstruct the authority. An honest source returns only the snapshot bound
/// to the exact issuer requested, the invariant the real runtime source
/// guarantees.
#[cfg(test)]
#[cfg(any(test, feature = "test-utils"))]
#[derive(Clone, Default)]
pub(crate) struct StaticIssuerKeySource {
pub struct StaticIssuerKeySource {
snapshots: std::collections::HashMap<String, AssertionKeySet>,
/// When set, returned for every requested issuer regardless of its binding,
/// to exercise the verifier's defensive issuer re-check.
misbound: Option<AssertionKeySet>,
}

#[cfg(test)]
#[cfg(any(test, feature = "test-utils"))]
impl StaticIssuerKeySource {
/// Build an honest source from a set of snapshots, keyed by each snapshot's
/// issuer.
pub(crate) fn new(snapshots: impl IntoIterator<Item = AssertionKeySet>) -> Self {
pub fn new(snapshots: impl IntoIterator<Item = AssertionKeySet>) -> Self {
Self {
snapshots: snapshots
.into_iter()
Expand All @@ -235,18 +249,18 @@ impl StaticIssuerKeySource {
/// A hostile/buggy source that returns the given snapshot — bound to a
/// different issuer than requested — for every lookup, to exercise the
/// verifier's defensive issuer re-check.
pub(crate) fn misbinding(snapshot: AssertionKeySet) -> Self {
pub fn misbinding(snapshot: AssertionKeySet) -> Self {
Self {
snapshots: std::collections::HashMap::new(),
misbound: Some(snapshot),
}
}
}

#[cfg(test)]
#[cfg(any(test, feature = "test-utils"))]
impl sealed::Sealed for StaticIssuerKeySource {}

#[cfg(test)]
#[cfg(any(test, feature = "test-utils"))]
impl IssuerKeySource for StaticIssuerKeySource {
fn key_set(&self, issuer: &str) -> Option<AssertionKeySet> {
self.misbound
Expand All @@ -255,6 +269,24 @@ impl IssuerKeySource for StaticIssuerKeySource {
}
}

/// Object-safe wrapper for assertion verification, allowing type-erased storage
/// in `AppState` and test injection of `StaticIssuerKeySource`-backed verifiers.
///
/// `FederatedAssertionVerifier<S>` implements this for any `S: IssuerKeySource`.
/// The sealed `IssuerKeySource` trait still constrains who can build a real
/// verifier — this trait only erases the `S` type parameter at the storage boundary.
pub trait VerifyAssertion: Send + Sync {
/// Verify one compact JWS assertion. Semantics identical to
/// [`FederatedAssertionVerifier::verify`].
fn verify_assertion(&self, token: &str) -> Result<VerifiedAssertion, VerifierError>;
}

impl<S: IssuerKeySource + Send + Sync> VerifyAssertion for FederatedAssertionVerifier<S> {
fn verify_assertion(&self, token: &str) -> Result<VerifiedAssertion, VerifierError> {
self.verify(token)
}
}

/// The provider-neutral assertion verifier over a closed multi-issuer registry
/// and a trusted [`IssuerKeySource`].
#[derive(Debug, Clone)]
Expand Down
5 changes: 4 additions & 1 deletion crates/buzz-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ buzz-db = { workspace = true }
buzz-datastore-tracing = { workspace = true }
buzz-deletion = { workspace = true }
buzz-auth = { workspace = true }
jsonwebtoken = { workspace = true }
buzz-pubsub = { workspace = true }
buzz-audit = { workspace = true }
buzz-search = { workspace = true }
Expand All @@ -39,6 +40,7 @@ tower-http = { workspace = true }
nostr = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_urlencoded = "0.7"
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-opentelemetry = { workspace = true }
Expand Down Expand Up @@ -86,6 +88,7 @@ async-compression = { version = "0.4.42", features = ["tokio", "gzip"] }
dev = ["buzz-auth/dev"]

[dev-dependencies]
http-body-util = "0.1"
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"] }
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.75.1", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"] }
# Relay-driven mesh lifecycle smoke (examples/mesh_relay_lifecycle_smoke.rs):
Expand All @@ -94,7 +97,7 @@ mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag
buzz-test-client = { path = "../buzz-test-client" }
ed25519-dalek = "=3.0.0-rc.0"
buzz-core = { workspace = true, features = ["test-utils"] }
buzz-auth = { workspace = true, features = ["dev"] }
buzz-auth = { workspace = true, features = ["dev", "test-utils"] }
reqwest = { workspace = true }
tokio-tungstenite = { workspace = true }
futures = "0.3"
Expand Down
Loading
Loading