mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
test(auth): publish hosted O4 conformance selection
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
This commit is contained in:
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -356,6 +356,7 @@ jobs:
|
||||
-p buzz-relay \
|
||||
-p buzz-test-client \
|
||||
--lib \
|
||||
--test nip_fi_runtime_conformance \
|
||||
--test e2e_event_reminder \
|
||||
--archive-file target/ci/backend-integration-tests.tar.zst
|
||||
- name: Save relay artifacts cache
|
||||
@@ -713,6 +714,20 @@ jobs:
|
||||
--run-ignored all
|
||||
env:
|
||||
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz_identity_tests
|
||||
- name: NIP-FI runtime and protected transport conformance
|
||||
run: |
|
||||
cargo nextest run \
|
||||
--archive-file target/ci/backend-integration-tests.tar.zst \
|
||||
-E 'binary(nip_fi_runtime_conformance)'
|
||||
cargo nextest run \
|
||||
--archive-file target/ci/backend-integration-tests.tar.zst \
|
||||
-E 'package(buzz-relay) and (test(protected_media_reads_require_corporate_identity_for_get_and_head) or test(moderation_reads_require_corporate_identity_after_nip98_proof))' \
|
||||
--test-threads 1 \
|
||||
--run-ignored ignored-only
|
||||
env:
|
||||
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
||||
BUZZ_TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
||||
REDIS_URL: redis://localhost:6379
|
||||
- name: Workspace profile (kind:9033) gate tests
|
||||
# Call-site integration for the 9033 authorization gate: open relay
|
||||
# rosterless/steward transitions and the closed-relay admin/owner rule,
|
||||
@@ -725,6 +740,20 @@ jobs:
|
||||
--run-ignored ignored-only
|
||||
env:
|
||||
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
||||
- name: Protected Git and media authority migration tests
|
||||
run: |
|
||||
docker exec -e PGPASSWORD="${BUZZ_TEST_POSTGRES_PASSWORD}" buzz-postgres \
|
||||
psql -U buzz -d postgres -v ON_ERROR_STOP=1 \
|
||||
-c "CREATE DATABASE buzz_visibility_tests"
|
||||
cargo nextest run \
|
||||
--archive-file target/ci/backend-integration-tests.tar.zst \
|
||||
-E '(package(buzz-db) and test(/protected_visibility::tests::cutover_waits/)) or (package(buzz-relay) and test(/api::media_migration::tests::populated_git_and_media_cutover/))' \
|
||||
--test-threads 1 \
|
||||
--run-ignored ignored-only
|
||||
env:
|
||||
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz_visibility_tests
|
||||
BUZZ_TEST_DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz_visibility_tests
|
||||
REDIS_URL: redis://localhost:6379
|
||||
- name: NIP-ER reminder e2e
|
||||
# Feature e2e for NIP-ER (Event Reminders, kind:30300): write-path
|
||||
# validation, author-only read filtering, and scheduler delivery against
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1056,6 +1056,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"blurhash",
|
||||
"buzz-auth",
|
||||
"buzz-core",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
||||
90
crates/buzz-relay/tests/fixtures/nip_fi_trusted_proxy.json
vendored
Normal file
90
crates/buzz-relay/tests/fixtures/nip_fi_trusted_proxy.json
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"fixture_classification": "synthetic-only",
|
||||
"full_stack_conformance_claim": false,
|
||||
"presentation_gate": "disabled",
|
||||
"trusted_proxy_cases": [
|
||||
{
|
||||
"row": "TR-1.direct-bypass",
|
||||
"origin_isolation_enforced": false,
|
||||
"inbound_assertion_header_stripped": true,
|
||||
"expected": "deny-before-verification"
|
||||
},
|
||||
{
|
||||
"row": "TR-1.inbound-header-copy",
|
||||
"origin_isolation_enforced": true,
|
||||
"inbound_assertion_header_stripped": false,
|
||||
"expected": "deny-before-verification"
|
||||
},
|
||||
{
|
||||
"row": "TR-1.complete-deployment-evidence",
|
||||
"origin_isolation_enforced": true,
|
||||
"inbound_assertion_header_stripped": true,
|
||||
"expected": "eligible-for-provider-verification"
|
||||
}
|
||||
],
|
||||
"nip_fi_rows": [
|
||||
{
|
||||
"row": "AS-3.future-iat",
|
||||
"owner": "o4-client-status",
|
||||
"status": "covered"
|
||||
},
|
||||
{
|
||||
"row": "BD-1.cross-domain",
|
||||
"owner": "authorization-runtime",
|
||||
"status": "required-before-full-stack-claim"
|
||||
},
|
||||
{
|
||||
"row": "SE-4.invalidation",
|
||||
"owner": "invalidation-runtime",
|
||||
"status": "required-before-full-stack-claim"
|
||||
},
|
||||
{
|
||||
"row": "DG-3.no-finite-bound",
|
||||
"owner": "delegation-runtime",
|
||||
"status": "required-before-full-stack-claim"
|
||||
},
|
||||
{
|
||||
"row": "OP-2.discovery",
|
||||
"owner": "o4-client-status",
|
||||
"status": "covered"
|
||||
},
|
||||
{
|
||||
"row": "OP-3.absent",
|
||||
"owner": "o4-client-status",
|
||||
"status": "covered"
|
||||
},
|
||||
{
|
||||
"row": "OP-3.implemented",
|
||||
"owner": "o4-client-status",
|
||||
"status": "disabled-pending-approved-rfc-presentation-gate"
|
||||
},
|
||||
{
|
||||
"row": "OP-4.privacy",
|
||||
"owner": "o4-client-status",
|
||||
"status": "covered"
|
||||
}
|
||||
],
|
||||
"j3c_rows": [
|
||||
"J3C-STATUS-RELAY-SIGNER",
|
||||
"J3C-STATUS-EXACT-SCOPE",
|
||||
"J3C-STATUS-FRESHNESS",
|
||||
"J3C-STATUS-REVISION-FOLD",
|
||||
"J3C-STATUS-WITHDRAWAL",
|
||||
"J3C-STATUS-PRIVACY",
|
||||
"J3C-STATUS-VERIFY-ONLY",
|
||||
"J3C-STATUS-DEDICATED-TRANSPORT",
|
||||
"J3C-STATUS-REAL-USER-HIDDEN"
|
||||
],
|
||||
"forbidden_public_fields": [
|
||||
"iss",
|
||||
"sub",
|
||||
"display_name",
|
||||
"bearer_assertion",
|
||||
"private_audience",
|
||||
"provider_tenant_url",
|
||||
"corporate_history",
|
||||
"historical_label",
|
||||
"employment_history"
|
||||
]
|
||||
}
|
||||
381
crates/buzz-relay/tests/nip_fi_runtime_conformance.rs
Normal file
381
crates/buzz-relay/tests/nip_fi_runtime_conformance.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
//! Structural O4 conformance checks for the disabled client-status surface.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const FIXTURE: &str = include_str!("fixtures/nip_fi_trusted_proxy.json");
|
||||
const STATUS_MODULE: &str = include_str!("../src/authorization_runtime/status.rs");
|
||||
const ASSERTION_VERIFIER: &str = include_str!("../src/corporate_identity.rs");
|
||||
const INVALIDATION_RUNTIME: &str = include_str!("../src/authorization_runtime/invalidation.rs");
|
||||
const TRANSPORT_RUNTIME: &str = include_str!("../src/authorization_runtime/transport.rs");
|
||||
const FINALIZATION_RUNTIME: &str = include_str!("../src/authorization_runtime/finalization.rs");
|
||||
const PRODUCTION_RUNTIME: &str = include_str!("../src/authorization_runtime/production.rs");
|
||||
const AUTH_HANDLER: &str = include_str!("../src/handlers/auth.rs");
|
||||
const KIND_REGISTRY: &str = include_str!("../../buzz-core/src/kind.rs");
|
||||
const INGEST_HANDLER: &str = include_str!("../src/handlers/ingest.rs");
|
||||
|
||||
#[test]
|
||||
fn mandatory_o4_security_contracts_are_present() {
|
||||
let cases = [
|
||||
(
|
||||
"protected-header-denial",
|
||||
ASSERTION_VERIFIER.contains("headers.get_all(config.jwt_header.as_str())")
|
||||
&& ASSERTION_VERIFIER.contains("values.next().is_some()")
|
||||
&& ASSERTION_VERIFIER.contains("raw.contains(',')"),
|
||||
),
|
||||
(
|
||||
"bounded-known-key-jwks-degradation",
|
||||
ASSERTION_VERIFIER.contains("JWKS_CACHE_MAX_AGE")
|
||||
&& ASSERTION_VERIFIER.contains("buzz_jwks_stale_key_uses_total")
|
||||
&& ASSERTION_VERIFIER.contains("buzz_jwks_unknown_kid_total")
|
||||
&& ASSERTION_VERIFIER.contains("buzz_jwt_verification_errors_total"),
|
||||
),
|
||||
(
|
||||
"lease-expiry-and-invalidation",
|
||||
TRANSPORT_RUNTIME.contains("expiry_delay")
|
||||
&& INVALIDATION_RUNTIME.contains("cancel_invalid(community_id)"),
|
||||
),
|
||||
(
|
||||
"revocation-enforcement-timing",
|
||||
INVALIDATION_RUNTIME.contains("buzz_authorization_revocation_to_enforcement_seconds"),
|
||||
),
|
||||
(
|
||||
"client-status-fail-closed-degradation",
|
||||
AUTH_HANDLER.contains("client_status_unavailable")
|
||||
&& AUTH_HANDLER.contains("buzz_client_status_degradation_total"),
|
||||
),
|
||||
(
|
||||
"deny-protected-request-renewal-and-session-eviction",
|
||||
FINALIZATION_RUNTIME.contains("DenyProtected")
|
||||
&& PRODUCTION_RUNTIME
|
||||
.contains("\"deny_protected\" => AuthorizationMode::DenyProtected")
|
||||
&& TRANSPORT_RUNTIME.contains("AuthorizationMode::DenyProtected =>")
|
||||
&& TRANSPORT_RUNTIME.contains("deny_protected_request(request)")
|
||||
&& TRANSPORT_RUNTIME.contains("request.cancellation()")
|
||||
&& TRANSPORT_RUNTIME.contains("cancellation.cancel()")
|
||||
&& TRANSPORT_RUNTIME.contains("ProtectedTransportError::DenyProtected"),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, present) in cases {
|
||||
assert!(present, "missing mandatory O4 security contract: {name}");
|
||||
}
|
||||
|
||||
for source in [ASSERTION_VERIFIER, INVALIDATION_RUNTIME, AUTH_HANDLER] {
|
||||
let observability = source
|
||||
.lines()
|
||||
.filter(|line| line.contains("metrics::"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
for forbidden in [
|
||||
"identity_token =",
|
||||
"access_token =",
|
||||
"refresh_token =",
|
||||
"uid =",
|
||||
"subject =",
|
||||
"kid =",
|
||||
] {
|
||||
assert!(
|
||||
!observability.contains(forbidden),
|
||||
"security observability gained a private label: {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_fixture_names_required_nip_fi_and_j3c_rows() {
|
||||
let fixture: serde_json::Value = serde_json::from_str(FIXTURE).expect("fixture JSON parses");
|
||||
assert_eq!(fixture["fixture_classification"], "synthetic-only");
|
||||
assert_eq!(fixture["full_stack_conformance_claim"], false);
|
||||
assert_eq!(fixture["presentation_gate"], "disabled");
|
||||
|
||||
for row in [
|
||||
"TR-1.direct-bypass",
|
||||
"TR-1.inbound-header-copy",
|
||||
"TR-1.complete-deployment-evidence",
|
||||
"AS-3.future-iat",
|
||||
"BD-1.cross-domain",
|
||||
"SE-4.invalidation",
|
||||
"DG-3.no-finite-bound",
|
||||
"OP-2.discovery",
|
||||
"OP-3.absent",
|
||||
"OP-3.implemented",
|
||||
"OP-4.privacy",
|
||||
"J3C-STATUS-RELAY-SIGNER",
|
||||
"J3C-STATUS-EXACT-SCOPE",
|
||||
"J3C-STATUS-FRESHNESS",
|
||||
"J3C-STATUS-REVISION-FOLD",
|
||||
"J3C-STATUS-WITHDRAWAL",
|
||||
"J3C-STATUS-PRIVACY",
|
||||
"J3C-STATUS-VERIFY-ONLY",
|
||||
"J3C-STATUS-DEDICATED-TRANSPORT",
|
||||
"J3C-STATUS-REAL-USER-HIDDEN",
|
||||
] {
|
||||
assert!(FIXTURE.contains(row), "fixture omitted required row {row}");
|
||||
}
|
||||
|
||||
let future_iat = fixture["nip_fi_rows"]
|
||||
.as_array()
|
||||
.expect("NIP-FI rows are an array")
|
||||
.iter()
|
||||
.find(|row| row["row"] == "AS-3.future-iat")
|
||||
.expect("future-iat allocation exists");
|
||||
assert_eq!(future_iat["owner"], "o4-client-status");
|
||||
assert_eq!(future_iat["status"], "covered");
|
||||
|
||||
let projection = fixture["nip_fi_rows"]
|
||||
.as_array()
|
||||
.expect("NIP-FI rows are an array")
|
||||
.iter()
|
||||
.find(|row| row["row"] == "OP-3.implemented")
|
||||
.expect("implemented projection allocation exists");
|
||||
assert_eq!(
|
||||
projection["status"],
|
||||
"disabled-pending-approved-rfc-presentation-gate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_iat_uses_the_shared_injected_authorization_clock() {
|
||||
assert!(ASSERTION_VERIFIER.contains("self.authorization_clock.now()?"));
|
||||
assert!(ASSERTION_VERIFIER.contains("validate_optional_iat("));
|
||||
assert!(
|
||||
!ASSERTION_VERIFIER
|
||||
.contains("validate_optional_iat(&decoded.claims.claims, Timestamp::now().as_secs())"),
|
||||
"optional iat must not bypass the injected authorization clock"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_authored_status_is_rejected_by_the_central_relay_only_fence() {
|
||||
let relay_only_predicate = KIND_REGISTRY
|
||||
.split("pub const fn is_relay_only_kind")
|
||||
.nth(1)
|
||||
.and_then(|suffix| suffix.split("/// Extract the kind").next())
|
||||
.expect("central relay-only predicate exists");
|
||||
assert!(relay_only_predicate.contains("KIND_CLIENT_BINDING_STATUS"));
|
||||
assert!(INGEST_HANDLER.contains("buzz_core::kind::is_relay_only_kind(kind_u32)"));
|
||||
assert!(INGEST_HANDLER.contains("restricted: relay-only kind"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_projection_retirement_is_durable_internal_and_not_operator_wired() {
|
||||
let production = ASSERTION_VERIFIER
|
||||
.split("#[cfg(test)]")
|
||||
.next()
|
||||
.expect("production assertion verifier exists");
|
||||
let migration =
|
||||
include_str!("../../../migrations/0044_identity_public_projection_retirement.sql");
|
||||
assert!(migration.contains("identity_public_projection_retirements"));
|
||||
assert!(migration.contains("source_binding_id"));
|
||||
assert!(migration.contains("source_binding_version"));
|
||||
for private in [
|
||||
"issuer TEXT",
|
||||
"uid",
|
||||
"display_name",
|
||||
"actor",
|
||||
"reason",
|
||||
"identity_token",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
] {
|
||||
assert!(
|
||||
!migration.contains(private),
|
||||
"projection retirement state gained private field {private}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
production.contains("begin_active_public_projection"),
|
||||
"active projection publication must revalidate the exact binding at its database boundary"
|
||||
);
|
||||
let production_runtime = include_str!("../src/authorization_runtime/production.rs");
|
||||
assert!(
|
||||
production_runtime.contains("reconcile_public_projection_retirements_startup"),
|
||||
"committed lifecycle retirement must be reconciled before protected runtime installation"
|
||||
);
|
||||
assert!(
|
||||
production_runtime.contains("run_public_projection_retirement_reconciliation"),
|
||||
"committed lifecycle retirement needs durable periodic/restart reconciliation"
|
||||
);
|
||||
for operator_surface in [
|
||||
include_str!("../src/api/operator.rs"),
|
||||
include_str!("../src/api/bridge.rs"),
|
||||
] {
|
||||
assert!(!operator_surface.contains("PublicProjectionRetirement"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_proxy_fixture_fails_closed_without_both_deployment_controls() {
|
||||
let fixture: serde_json::Value = serde_json::from_str(FIXTURE).expect("fixture JSON parses");
|
||||
let cases = fixture["trusted_proxy_cases"]
|
||||
.as_array()
|
||||
.expect("trusted proxy cases are an array");
|
||||
assert_eq!(cases.len(), 3);
|
||||
|
||||
for case in cases {
|
||||
let isolation = case["origin_isolation_enforced"]
|
||||
.as_bool()
|
||||
.expect("fixture isolation flag is boolean");
|
||||
let stripping = case["inbound_assertion_header_stripped"]
|
||||
.as_bool()
|
||||
.expect("fixture stripping flag is boolean");
|
||||
let expected = case["expected"]
|
||||
.as_str()
|
||||
.expect("fixture expectation is a string");
|
||||
if isolation && stripping {
|
||||
assert_eq!(expected, "eligible-for-provider-verification");
|
||||
} else {
|
||||
assert_eq!(expected, "deny-before-verification");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_only_adapter_has_no_authority_storage_or_pubsub_dependency() {
|
||||
let production = STATUS_MODULE
|
||||
.split("#[cfg(test)]")
|
||||
.next()
|
||||
.expect("production status module exists");
|
||||
for forbidden in [
|
||||
"AuthContext",
|
||||
"AuthorizationLease",
|
||||
"CapabilitySet",
|
||||
"AuthState",
|
||||
"buzz_db",
|
||||
"buzz_pubsub",
|
||||
"publish_event",
|
||||
"store_event",
|
||||
"KIND_USER_TRUSTED_ASSERTION",
|
||||
"corporate_identity",
|
||||
] {
|
||||
assert!(
|
||||
!production.contains(forbidden),
|
||||
"verification-only status gained forbidden authority path {forbidden}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
production
|
||||
.matches("pub struct ClientStatusPresentationPermit {")
|
||||
.count(),
|
||||
1,
|
||||
"the disabled presentation permit must have one opaque definition"
|
||||
);
|
||||
assert!(production.contains("impl ClientStatusPresentationPermit"));
|
||||
assert!(production.contains("pub fn from_complete_stack("));
|
||||
assert!(production.contains("reviewed_implementation_revision"));
|
||||
assert!(production.contains("presentation_gate_passed"));
|
||||
assert!(production.contains("dedicated_client_contract_passed"));
|
||||
assert!(
|
||||
!production.contains("std::env"),
|
||||
"presentation must not be enabled by an environment boolean"
|
||||
);
|
||||
|
||||
let current_issuance = production
|
||||
.split("pub fn issue_verification_only")
|
||||
.nth(1)
|
||||
.and_then(|suffix| suffix.split("/// Sign a generic withdrawal").next())
|
||||
.expect("current-display issuance method exists");
|
||||
assert!(
|
||||
current_issuance.contains("&ClientStatusPresentationPermit"),
|
||||
"current-display signing must require the complete-stack permit"
|
||||
);
|
||||
assert_eq!(
|
||||
production
|
||||
.matches("issue_current(&evidence, label)")
|
||||
.count(),
|
||||
1,
|
||||
"no second production current-display signing path may bypass the permit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_uses_only_the_dedicated_authenticated_production_path() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let repo = manifest
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.expect("relay crate is nested under repository crates directory");
|
||||
let roots = [
|
||||
manifest.join("src/handlers"),
|
||||
manifest.join("src/api"),
|
||||
manifest.join("src/main.rs"),
|
||||
manifest.join("src/router.rs"),
|
||||
manifest.join("src/subscription.rs"),
|
||||
manifest.join("src/connection.rs"),
|
||||
manifest.join("src/protocol.rs"),
|
||||
repo.join("desktop/src"),
|
||||
repo.join("desktop/src-tauri/src"),
|
||||
repo.join("mobile/lib"),
|
||||
repo.join("web/src"),
|
||||
];
|
||||
|
||||
for root in roots {
|
||||
for file in source_files(&root) {
|
||||
if file.ends_with("src/handlers/auth.rs") {
|
||||
continue;
|
||||
}
|
||||
let source = fs::read_to_string(&file).expect("source file is readable");
|
||||
for forbidden in [
|
||||
"KIND_CLIENT_BINDING_STATUS",
|
||||
"ClientBindingStatus",
|
||||
"client_binding_status",
|
||||
"24244",
|
||||
"deliver_verification_only",
|
||||
] {
|
||||
assert!(
|
||||
!source.contains(forbidden),
|
||||
"{} exposes status through an ordinary route {forbidden}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let handler = fs::read_to_string(manifest.join("src/handlers/auth.rs"))
|
||||
.expect("AUTH handler source is readable");
|
||||
let state = fs::read_to_string(manifest.join("src/state.rs"))
|
||||
.expect("application state source is readable");
|
||||
let status = fs::read_to_string(manifest.join("src/authorization_runtime/status.rs"))
|
||||
.expect("status runtime source is readable");
|
||||
let nip11 =
|
||||
fs::read_to_string(manifest.join("src/nip11.rs")).expect("NIP-11 source is readable");
|
||||
let router =
|
||||
fs::read_to_string(manifest.join("src/router.rs")).expect("router source is readable");
|
||||
assert!(handler.contains("present_after_auth"));
|
||||
assert!(state.contains("install_client_status_runtime"));
|
||||
assert!(state.contains("install_nip_fi_discovery"));
|
||||
assert!(status.contains("from_complete_stack"));
|
||||
assert!(status.contains("__buzz_client_binding_status_v1__"));
|
||||
assert!(nip11.contains("state.nip_fi_discovery()"));
|
||||
assert!(nip11.contains("with_conformant_federated_identity"));
|
||||
assert!(router.contains("nip11_document(&state, raw_host).await"));
|
||||
assert!(!status.contains("std::env"));
|
||||
}
|
||||
|
||||
fn source_files(root: &Path) -> Vec<PathBuf> {
|
||||
if root.is_file() {
|
||||
return vec![root.to_path_buf()];
|
||||
}
|
||||
let mut pending = vec![root.to_path_buf()];
|
||||
let mut files = Vec::new();
|
||||
while let Some(directory) = pending.pop() {
|
||||
for entry in fs::read_dir(&directory).expect("source directory is readable") {
|
||||
let path = entry.expect("source directory entry is readable").path();
|
||||
if path.is_dir() {
|
||||
pending.push(path);
|
||||
} else if path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| {
|
||||
matches!(extension, "rs" | "ts" | "tsx" | "js" | "jsx" | "dart")
|
||||
})
|
||||
{
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
files
|
||||
}
|
||||
19
desktop/src-tauri/Cargo.lock
generated
19
desktop/src-tauri/Cargo.lock
generated
@@ -1013,6 +1013,24 @@ dependencies = [
|
||||
"webbrowser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buzz-auth"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"buzz-core",
|
||||
"hex",
|
||||
"nostr",
|
||||
"rand 0.10.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buzz-core"
|
||||
version = "0.1.0"
|
||||
@@ -1130,6 +1148,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"blurhash",
|
||||
"buzz-auth",
|
||||
"buzz-core",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
||||
152
docs/NIP_FI_RUNTIME_OPERATIONS.md
Normal file
152
docs/NIP_FI_RUNTIME_OPERATIONS.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# NIP-FI runtime operations
|
||||
|
||||
This runbook covers provider-neutral NIP-FI session/discovery behavior and the
|
||||
separate disabled relay-authenticated client-status contract. It does not
|
||||
authorize enabling a provider, a client presentation surface, or a conformance
|
||||
claim.
|
||||
|
||||
## Session and reconnect behavior
|
||||
|
||||
For WebSocket authorization, the assertion belongs on the upgrade request and
|
||||
fresh NIP-42 proof follows on that connection. A direct lease ends at the
|
||||
earliest assertion, binding, policy, or implementation bound. Base V1 has no
|
||||
in-connection assertion renewal: expiry requires a new connection, a fresh
|
||||
upgrade assertion, and fresh NIP-42 proof.
|
||||
|
||||
Delegated sessions require a separately validated delegation, an active owner
|
||||
binding, and a positive finite configured implementation maximum. A cached
|
||||
owner lease is not substitute authority. Reconnect requires fresh delegate
|
||||
proof and revalidation of every dependency.
|
||||
|
||||
When an observed binding, identity, key, policy, or delegation dependency
|
||||
becomes invalid, reject protected operations or close the affected connection
|
||||
within the documented detection bound. A polling deployment must publish its
|
||||
maximum detection latency and must not claim immediate revocation.
|
||||
|
||||
The optional assertion `iat` check uses the shared injected authorization
|
||||
clock. The current JWT library still evaluates `exp` and `nbf` with its own
|
||||
system-clock source, so operators must maintain host clock synchronization and
|
||||
must not claim fully centralized assertion time until that library boundary is
|
||||
made injectable.
|
||||
|
||||
`iat` is optional in Base V1. When present, a malformed or more-than-60-second
|
||||
future value is rejected. A `kid` absent from a still-fresh JWKS set is denied
|
||||
without an immediate refetch; the default set lifetime is 300 seconds. Issuers
|
||||
must overlap old and new signing keys for at least the cache lifetime plus the
|
||||
documented clock allowance. Refresh after expiry is single-flight, and refresh
|
||||
or issuer failure never falls back to an unverified key.
|
||||
|
||||
The assertion header is singular. Multiple field lines, a comma-combined
|
||||
value, invalid UTF-8, or an empty value is denied before verification. A
|
||||
trusted-proxy adapter must prove origin isolation and that it stripped every
|
||||
inbound copy before injecting exactly one assertion; merely observing the
|
||||
configured header is not transport provenance.
|
||||
|
||||
Protected media downloads include both `GET` and `HEAD`. An enforcing domain
|
||||
cannot expose either method without current authority; a deployment that wants
|
||||
public media needs a separately reviewed public-media policy rather than an
|
||||
implicit read bypass.
|
||||
|
||||
Client status is presentation-only. It expires independently of an
|
||||
authorization lease and is cleared on expiry, disconnect, relay-key change,
|
||||
domain change, or author change. A status cannot renew a session, authorize an
|
||||
operation, create a binding, mint a lease, or mutate membership.
|
||||
|
||||
## Upgrade sequence
|
||||
|
||||
1. Upgrade and reconcile durable authorization, binding, lifecycle, lease, and
|
||||
status-revision-floor state before enabling any behavior.
|
||||
2. Deploy servers with NIP-FI discovery absent and the client-status
|
||||
presentation gate disabled.
|
||||
3. Run every applicable NIP-FI row against the exact candidate revision. For
|
||||
`trusted-proxy`, attach enforced origin-isolation evidence plus negative
|
||||
direct-bypass and inbound-header-copy tests.
|
||||
4. Confirm mixed-version servers all omit discovery. Never advertise based on
|
||||
a per-process flag or a partial fleet.
|
||||
5. Supply a complete-stack conformance input only after the whole serving fleet
|
||||
runs the reviewed revision and all applicable rows pass.
|
||||
6. Supply the typed client-presentation approval only after its deployment,
|
||||
privacy, and client-compatibility gates pass at one exact revision. The
|
||||
stock binary has no environment or boolean shortcut; without that injected
|
||||
proof it cannot construct the presentation permit or install the dedicated
|
||||
exact-connection transport.
|
||||
|
||||
Old clients ignore unknown status events, and old servers emit none. New
|
||||
clients must default to no indicator when status is absent, invalid, expired,
|
||||
withheld, or unsupported. NIP-FI authorization behavior must remain identical
|
||||
whether client presentation code is present or absent.
|
||||
|
||||
## Rollback
|
||||
|
||||
Remove the complete-stack readiness input before or with the first server
|
||||
rollback so NIP-11 immediately omits NIP-FI discovery. Do not leave discovery
|
||||
enabled for a mixed or unreviewed fleet.
|
||||
|
||||
Client-status rollback requires no authority migration: the events are
|
||||
ephemeral and display-only. Disconnect affected clients or wait no longer than
|
||||
the bounded status lifetime; clients clear on either condition. Never translate
|
||||
a cached status into an authorization decision during rollback.
|
||||
|
||||
Preserve durable authorization and lifecycle state. Preserve and reconcile the
|
||||
status revision floor so a restored older process cannot emit a lower revision
|
||||
that a client might mistake for current state. If that state is unavailable,
|
||||
emit no status.
|
||||
|
||||
## Public projection retirement
|
||||
|
||||
The privacy-approved NIP-85 label projection is optional and never authority.
|
||||
After an authoritative revoke or rotate commits, the lifecycle integration must
|
||||
derive internal retirement work from the committed lifecycle record. The work
|
||||
contains only the server-resolved domain, old public Nostr key, relay author,
|
||||
operation identifier, and opaque binding generation. It must not contain
|
||||
issuer, subject, display label, provider claims, actor, or free-text reason.
|
||||
The reconciler idempotently replaces an active projection with the existing
|
||||
inactive, label-free parameterized event.
|
||||
|
||||
A read, clock, build, or write failure must not roll back the already committed
|
||||
lifecycle mutation. Retry the same domain/key request. If a write committed but
|
||||
its acknowledgement was lost, the retry observes the inactive replacement and
|
||||
terminates without another write.
|
||||
|
||||
The runtime materializes committed revoke/rotate operations into an internal
|
||||
durable queue, fences active publication and retirement with the exact binding
|
||||
generation, and drains unfinished projection and delivery work before protected
|
||||
runtime installation and after restart. Periodic discovery is the crash-window
|
||||
backstop. The active projection TTL remains defense in depth. Authenticated
|
||||
lifecycle routes and durable operator audit remain separately owned.
|
||||
|
||||
## Backup and restore
|
||||
|
||||
Back up authoritative binding/lifecycle state, policy state, cryptographic
|
||||
secrets required by deployment policy, and durable status revision/floor state
|
||||
using the owning subsystem's procedure. Protect the dedicated client-status
|
||||
privacy key as a secret and never reuse it across unrelated deployments.
|
||||
|
||||
Do not back up or restore:
|
||||
|
||||
- authorization or provider caches;
|
||||
- direct or delegated leases;
|
||||
- WebSocket connection state;
|
||||
- client presentation caches;
|
||||
- emitted client-status events; or
|
||||
- ordinary event/pubsub copies of client status, because none may exist.
|
||||
|
||||
After restore, start with discovery absent and presentation disabled. Rebuild
|
||||
authorization decisions from authoritative state, reconcile revision floors,
|
||||
reconcile committed projection retirements, and reconnect clients with fresh
|
||||
assertions/proofs. If the relay signing key or client-status privacy key
|
||||
changed, treat every old presentation as invalid. A restored service must
|
||||
complete same-revision conformance again before discovery can return.
|
||||
|
||||
## Privacy and observability
|
||||
|
||||
Logs, metrics, traces, fixtures, and NIP-11 output must not contain raw bearer
|
||||
assertions or unredacted issuer, subject, audience, tenant URL, claim name,
|
||||
display name, email, or provider-private metadata. Use bounded categorical
|
||||
failure classes and pseudonymous correlation where necessary.
|
||||
|
||||
Alert on aggregate validation failures, revision-source unavailability,
|
||||
dedicated-transport unavailability after a future gate is approved, and
|
||||
dependency-invalidation lag. Do not include the rejected private value in an
|
||||
alert. The presence or absence of a client status is not evidence of access and
|
||||
must never drive an authorization SLO.
|
||||
166
docs/nips/NIP-FI-RUNTIME-CONFORMANCE.md
Normal file
166
docs/nips/NIP-FI-RUNTIME-CONFORMANCE.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# NIP-FI runtime conformance and client-status boundary
|
||||
|
||||
This document maps Buzz runtime evidence to the normative
|
||||
[NIP-FI specification](NIP-FI.md), [formal model](NIP-FI-MODEL.md), and
|
||||
[conformance matrix](NIP-FI-CONFORMANCE.md). It does not make a conformance
|
||||
claim. Discovery remains absent until an injected report proves that every
|
||||
applicable row passed against one reviewed implementation revision.
|
||||
|
||||
## Discovery gate
|
||||
|
||||
`RelayInfo::build` omits both `limitation.federated_identity` and the top-level
|
||||
`federated_identity` object. `ConformanceReadyNipFiDiscovery` is the only API
|
||||
that can add them. It requires all of the following:
|
||||
|
||||
- a provider-neutral discovery value with at least one unique supported
|
||||
transport and exactly one enrollment mode;
|
||||
- a positive finite delegated-lease maximum whenever delegation is advertised;
|
||||
- an exact 40-character reviewed Git revision;
|
||||
- an injected complete-stack result asserting that every applicable row passed
|
||||
at that same revision; and
|
||||
- for `trusted-proxy`, deployment evidence for both origin isolation and
|
||||
stripping untrusted inbound assertion-header copies.
|
||||
|
||||
The reviewed revision and deployment evidence are gate inputs, not public
|
||||
metadata. NIP-11 exposes no issuer URL, tenant URL, claim name, subject,
|
||||
audience, assertion header name, or provisional NIP number. Unsupported
|
||||
behavior is omitted rather than advertised as partially implemented.
|
||||
|
||||
The assertion header is singular at every ingress. Repeated field lines,
|
||||
comma-combined values, invalid UTF-8, and empty values fail closed. An installed
|
||||
adapter must supply verified transport provenance; header presence alone is
|
||||
never classified as `trusted-proxy`. Protected media `GET` and `HEAD` remain
|
||||
inside the enforcing transport inventory unless a separate reviewed public
|
||||
media policy is selected.
|
||||
|
||||
The optional assertion `iat` check uses the shared injected authorization
|
||||
clock and accepts a missing claim. A present value must be an unsigned integer
|
||||
no later than injected verifier time plus the bounded 60-second skew. The
|
||||
current `jsonwebtoken` dependency still evaluates `exp` and `nbf` against its
|
||||
own system-clock source. Therefore `AS-3.future-iat` is covered, but this
|
||||
candidate does not claim that all assertion-time checks use one injected clock;
|
||||
that inherited limitation remains part of the full-stack review.
|
||||
|
||||
## Relay-authenticated client status
|
||||
|
||||
Kind `24244` is a Buzz-local, short-lived presentation contract, not NIP-FI
|
||||
authorization evidence or a NIP-FI conformance surface. A status is signed by
|
||||
the trusted relay and scoped to an exact server-resolved authorization domain
|
||||
and event-author key. A current status contains a binding version, a
|
||||
privacy-keyed policy revision, a monotonic durable status revision, and a
|
||||
bounded validity window. A withdrawal contains only its exact scope, revision,
|
||||
and bounded validity window. The two wire states are:
|
||||
|
||||
- `display_current`; or
|
||||
- `withdrawn`, with no lifecycle cause or historical binding fields.
|
||||
|
||||
Clients fold only within one trusted relay/domain/author scope. A lower
|
||||
revision is rejected. An equal revision is idempotent only for the identical
|
||||
signed event; a conflicting equal revision is rejected. Expiry, disconnect,
|
||||
relay-key change, authorization-domain change, or event-author change clears
|
||||
presentation. Revision high-water state may survive a transient disconnect,
|
||||
but it is never authority.
|
||||
|
||||
The relay adapter is one-way from `VerificationOnlyDisposition` to a signed
|
||||
event. It has no dependency on authorization leases, membership mutation,
|
||||
event ingest, persistence, subscriptions, pub/sub, ordinary delivery, or
|
||||
NIP-85. The production seam targets an exact authenticated connection and can
|
||||
construct its permit only from typed evidence that the RFC presentation,
|
||||
privacy, and dedicated-client gates passed at one exact reviewed revision. The
|
||||
stock binary supplies no such evidence, key, or transport, so status remains
|
||||
disabled by default.
|
||||
|
||||
The optional label constructor accepts only privacy-approved server
|
||||
configuration. There is no constructor from issuer data, subject data,
|
||||
`display_name`, mutable profile content, or provider decisions. The policy
|
||||
revision is a length-framed, domain-separated HMAC under an injected dedicated
|
||||
client-status privacy key; identical provider values are unlinkable under
|
||||
distinct keys.
|
||||
|
||||
## Stable row allocation
|
||||
|
||||
The synthetic fixture is
|
||||
`crates/buzz-relay/tests/fixtures/nip_fi_trusted_proxy.json`. It contains no
|
||||
production issuer, subject, domain, key, assertion, or tenant data.
|
||||
|
||||
| Row | Evidence in this lane | Full-stack state |
|
||||
|---|---|---|
|
||||
| `TR-1.direct-bypass` | Negative origin-isolation fixture | Deployment proof still required |
|
||||
| `TR-1.inbound-header-copy` | Negative header-copy fixture | Deployment proof still required |
|
||||
| `TR-1.complete-deployment-evidence` | Positive two-control fixture shape | Real enforced-control evidence still required |
|
||||
| `AS-3.future-iat` | Optional assertion `iat` accepts absence and bounded skew; malformed or farther-future values fail closed. Status also rejects future issue time | Covered by O4 |
|
||||
| `BD-1.cross-domain` | Status validation and folding reject cross-domain scope | Authorization-runtime row must pass at the reviewed revision |
|
||||
| `SE-4.invalidation` | Withdrawal and client clearing are covered | Lease invalidation runtime must pass at the reviewed revision |
|
||||
| `DG-3.no-finite-bound` | Discovery cannot represent delegation without a positive bound | Delegation authorization must pass at the reviewed revision |
|
||||
| `OP-2.discovery` | Default omission, provider-neutral fields, and complete-stack gate | Covered here; final claim still requires all rows |
|
||||
| `OP-3.absent` | No real-user route or ordinary delivery path | Covered |
|
||||
| `OP-3.implemented` | Dedicated exact-connection production seam exists behind typed complete-stack approval | Disabled unless the approval, privacy key, transport, and runtime are explicitly installed |
|
||||
| `OP-4.privacy` | Keyed revision, bounded configured label, field/source scans | Covered |
|
||||
|
||||
The local client-status rows are:
|
||||
|
||||
- `J3C-STATUS-RELAY-SIGNER`
|
||||
- `J3C-STATUS-EXACT-SCOPE`
|
||||
- `J3C-STATUS-FRESHNESS`
|
||||
- `J3C-STATUS-REVISION-FOLD`
|
||||
- `J3C-STATUS-WITHDRAWAL`
|
||||
- `J3C-STATUS-PRIVACY`
|
||||
- `J3C-STATUS-VERIFY-ONLY`
|
||||
- `J3C-STATUS-DEDICATED-TRANSPORT`
|
||||
- `J3C-STATUS-REAL-USER-HIDDEN`
|
||||
|
||||
These J3C rows test presentation safety only. They cannot substitute for any
|
||||
NIP-FI authorization, lifecycle, session, delegation, or deployment row.
|
||||
|
||||
## Public projection retirement join
|
||||
|
||||
The existing opt-in NIP-85 label projection is separate from both NIP-FI
|
||||
authorization and kind `24244` client status. Its active assertion is TTL
|
||||
bounded, but a committed revoke or rotate also needs an inactive parameterized
|
||||
replacement for the old public key.
|
||||
|
||||
The public-projection retirement reconciler is a provider-neutral post-commit
|
||||
seam. It derives private retry work from committed lifecycle rows and persists
|
||||
only public event coordinates plus opaque binding generations. The relay reads
|
||||
the exact relay-authored projection and, when active, writes the existing
|
||||
`active=false`, `expiration=0`, label-free replacement. A missing or already
|
||||
inactive projection is an idempotent terminal result. Store or clock failure
|
||||
leaves both lifecycle authority and the active projection unchanged while the
|
||||
work remains retryable.
|
||||
|
||||
Active publication and retirement share the identity-key and parameterized
|
||||
event commit boundaries. Server-only head metadata prevents a delayed rotation
|
||||
job from retiring a later legitimate use of the same key. Startup and periodic
|
||||
reconciliation provide restart recovery and Redis/local delivery retry. This
|
||||
lane does not add authenticated lifecycle endpoints or durable operator audit.
|
||||
|
||||
## Compatibility cases
|
||||
|
||||
| Case | Required result |
|
||||
|---|---|
|
||||
| Old relay, new client | No discovery or status; client shows no indicator |
|
||||
| New relay, old client | Unknown ephemeral status is ignored; ordinary event behavior is unchanged |
|
||||
| Mixed relay fleet before complete conformance | Discovery stays absent; presentation stays disabled |
|
||||
| Stale client cache | Expired status is cleared; lower or conflicting revisions cannot restore it |
|
||||
| Spoofed user event | Wrong signer, kind, tags, content, or signature is rejected |
|
||||
| Cross-domain replay | Exact expected domain and author mismatch is rejected; scope change clears state |
|
||||
| Relay signing-key rotation | Old presentation is cleared and the new relay key must be trusted independently |
|
||||
| Privacy-key rotation | Policy revision changes; it grants no authority and clients accept it only at a higher durable status revision |
|
||||
| Provider or lifecycle outage | Relay issues an opaque `withdrawn` status only with authoritative revision evidence, otherwise emits nothing |
|
||||
| Gate disabled | No presentation runtime is installed; no real-user status is delivered |
|
||||
|
||||
## Mechanical checks
|
||||
|
||||
Run from the repository root in the Hermit environment:
|
||||
|
||||
```sh
|
||||
cargo test -p buzz-core client_binding_status
|
||||
cargo test -p buzz-relay authorization_runtime::status
|
||||
cargo test -p buzz-relay nip11
|
||||
cargo test -p buzz-relay --test nip_fi_runtime_conformance
|
||||
```
|
||||
|
||||
The integration test scans ordinary relay ingest, API, router, state,
|
||||
subscription, connection, and protocol sources, plus desktop, mobile, and web
|
||||
client sources. Any reference to the status kind, contract, or disabled
|
||||
delivery method fails the test.
|
||||
Reference in New Issue
Block a user