mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
chore(db): drop Phase-A NIP-FI relay-side authority ledger (#7221)
Removes the dead relay-side authority ledger introduced by migrations 0041 and 0042, and the dead `require_attested_key` verifier knob from `buzz-auth`. Both are unreachable by design under NIP-FI spec v2 (#7214, squash `d4420eb47`), which makes OSS Buzz stateless for identity: the relay neither stores nor verifies an authority chain. ## What changes **`migrations/0044_drop_nip_fi_ledger.sql`** Drops all fifteen NIP-FI ledger tables and their trigger functions using `CASCADE` to resolve the circular deferred FK between `identity_bindings` and `identity_lifecycle_history`. Drops proceed in FK dependency order: selectors → history/bindings → enrollment_policies/receipts → parallel drop of auth tables. Restores `community_write_fence_excluded_table` to its pre-0041 body (removes NIP-FI table names from the exclusion array). **`schema/schema.sql`** Removes the NIP-FI section (~1885 lines of tables, functions, and triggers) and updates `community_write_fence_excluded_table` to match. **`crates/buzz-db/src/runtime/migration.rs`** - Updates the `embedded_migrator_contains_consolidated_initial_schema` sanity check: count 43→44, adds 0044 assertion block (verifies `DROP TABLE` statements and absence of NIP-FI names from `schema.sql`). - Removes ~2580 lines of NIP-FI Postgres integration tests (all `#[tokio::test] #[ignore = "requires Postgres"]` from the 0041/0042 behavioral coverage). - Removes the `extract_excluded_table_array` drift check (0042 body no longer matches `schema.sql` by design). - Adds `migration_0044_drops_populated_nip_fi_ledger_cleanly`: runs migrations to 0042, seeds rows in `authorization_operation_receipts` and `authorization_invalidation_domains`, then runs to 0044 and verifies all fifteen NIP-FI tables are absent. **`crates/buzz-auth/src/nip_fi/config.rs`** Removes `require_attested_key: bool` from `IssuerPolicy` — field, constructor parameter, accessor, and its contribution to `derive_assertion_policy_id`. **`crates/buzz-auth/src/nip_fi/verifier.rs`** `parse_nostr_pubkey_claim` no longer takes a `policy` parameter. The `None` (absent claim) arm now returns `Err(VerifierError::ClaimRejected)` unconditionally instead of conditionally on `policy.require_attested_key()`. **`crates/buzz-auth/src/nip_fi/verifier/tests.rs`** - Removes `missing_nostr_pubkey_denies_under_attested_key_policy` (the sole `require_attested_key: true` call site). - Removes `false,` from all eleven `IssuerPolicy::new` call sites. - Injects `nostr_pubkey` by default in `mint_signed_by` (spec v2 requires it unconditionally). - Updates `valid_access_token_verifies` to assert `asserted_key().is_some()`. **`crates/buzz-auth/src/nip_fi/startup/tests.rs` + `jwks/tests.rs`** Removes `false,` from all `IssuerPolicy::new` call sites and adds `nostr_pubkey` to all token-minting helpers. ## Verification - Fresh-DB migration run to head: all migrations apply cleanly in sequence. - Populated-0041/0042-DB migration through 0044: seeds rows in live NIP-FI tables, verifies all fifteen are dropped without error. Closes the dead-code inventory item from the spec-v2 cleanup plan (channel `48374f48`). Follows #7214. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -69,7 +69,11 @@ pub(crate) const MAX_JWKS_KEYS: usize = 64;
|
||||
/// semantics** so prepared evidence built against an older contract is
|
||||
/// invalidated. Per-policy fields (issuer, class, bounds, …) are hashed
|
||||
/// separately and need no bump.
|
||||
pub(crate) const VERIFIER_CONTRACT_VERSION: u32 = 1;
|
||||
///
|
||||
/// v2 (PR #7221): `nostr_pubkey` absence now unconditionally rejects — the
|
||||
/// per-issuer `require_attested_key` knob is removed and the NIP-FI v2 spec
|
||||
/// requirement is always enforced.
|
||||
pub(crate) const VERIFIER_CONTRACT_VERSION: u32 = 2;
|
||||
|
||||
/// The transport-contract fingerprint folded into [`TransportContractId`].
|
||||
/// **Bump on any change** to the client-attached parsing, attachment,
|
||||
@@ -347,7 +351,6 @@ pub struct IssuerPolicy {
|
||||
token_class: TokenClass,
|
||||
freshness: FreshnessClass,
|
||||
algorithms: Vec<Algorithm>,
|
||||
require_attested_key: bool,
|
||||
skew_seconds: u64,
|
||||
maximum_assertion_age_seconds: u64,
|
||||
maximum_status_age_seconds: Option<u64>,
|
||||
@@ -404,7 +407,6 @@ impl IssuerPolicy {
|
||||
token_class: TokenClass,
|
||||
freshness: FreshnessClass,
|
||||
algorithms: Vec<Algorithm>,
|
||||
require_attested_key: bool,
|
||||
skew_seconds: u64,
|
||||
maximum_assertion_age_seconds: u64,
|
||||
maximum_status_age_seconds: Option<u64>,
|
||||
@@ -467,7 +469,6 @@ impl IssuerPolicy {
|
||||
&token_class,
|
||||
freshness,
|
||||
&algorithms,
|
||||
require_attested_key,
|
||||
skew_seconds,
|
||||
maximum_assertion_age_seconds,
|
||||
maximum_status_age_seconds,
|
||||
@@ -480,7 +481,6 @@ impl IssuerPolicy {
|
||||
token_class,
|
||||
freshness,
|
||||
algorithms,
|
||||
require_attested_key,
|
||||
skew_seconds,
|
||||
maximum_assertion_age_seconds,
|
||||
maximum_status_age_seconds,
|
||||
@@ -514,11 +514,6 @@ impl IssuerPolicy {
|
||||
&self.algorithms
|
||||
}
|
||||
|
||||
/// Whether enrollment requires a `nostr_pubkey` claim equal to the actor.
|
||||
pub const fn require_attested_key(&self) -> bool {
|
||||
self.require_attested_key
|
||||
}
|
||||
|
||||
/// The accepted clock skew, in seconds.
|
||||
pub const fn skew_seconds(&self) -> u64 {
|
||||
self.skew_seconds
|
||||
@@ -646,7 +641,6 @@ fn derive_assertion_policy_id(
|
||||
token_class: &TokenClass,
|
||||
freshness: FreshnessClass,
|
||||
algorithms: &[Algorithm],
|
||||
require_attested_key: bool,
|
||||
skew_seconds: u64,
|
||||
maximum_assertion_age_seconds: u64,
|
||||
maximum_status_age_seconds: Option<u64>,
|
||||
@@ -702,7 +696,6 @@ fn derive_assertion_policy_id(
|
||||
&mut hasher,
|
||||
algorithms.iter().map(|a| algorithm_tag(*a).as_bytes()),
|
||||
);
|
||||
hasher.update([u8::from(require_attested_key)]);
|
||||
hasher.update(skew_seconds.to_be_bytes());
|
||||
hasher.update(maximum_assertion_age_seconds.to_be_bytes());
|
||||
hasher.update(maximum_status_age_seconds.unwrap_or(0).to_be_bytes());
|
||||
|
||||
@@ -896,7 +896,9 @@ async fn two_issuer_keys_and_generations_are_isolated() {
|
||||
|
||||
fn sign(pkcs8_pem: &str, kid: &str, iss: &str, aud: &str) -> String {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
// nostr_pubkey is required unconditionally by spec v2.
|
||||
let claims = json!({"iss": iss, "aud": aud, "sub": "u",
|
||||
"nostr_pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"iat": now, "exp": now + 600});
|
||||
let mut hdr = Header::new(Algorithm::ES256);
|
||||
hdr.kid = Some(kid.to_owned());
|
||||
@@ -921,7 +923,6 @@ async fn two_issuer_keys_and_generations_are_isolated() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -1112,7 +1113,9 @@ async fn shared_arc_source_verifier_observes_rotation() {
|
||||
|
||||
fn sign_token(pkcs8_pem: &str, kid: &str, iss: &str, aud: &str) -> String {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
// nostr_pubkey is required unconditionally by spec v2.
|
||||
let claims = json!({"iss": iss, "aud": aud, "sub": "u",
|
||||
"nostr_pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"iat": now, "exp": now + 600});
|
||||
let mut hdr = Header::new(Algorithm::ES256);
|
||||
hdr.kid = Some(kid.to_owned());
|
||||
@@ -1171,7 +1174,6 @@ async fn shared_arc_source_verifier_observes_rotation() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -1233,7 +1235,6 @@ fn jwks_contract_uri_canonicalization_convergence_and_divergence() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
30,
|
||||
600,
|
||||
None,
|
||||
@@ -1489,7 +1490,9 @@ async fn shared_arc_source_verifier_rejects_expired_a1_accepts_a2() {
|
||||
|
||||
fn sign_token(pkcs8_pem: &str, kid: &str, iss: &str, aud: &str) -> String {
|
||||
let wall_now = chrono::Utc::now().timestamp();
|
||||
// nostr_pubkey is required unconditionally by spec v2.
|
||||
let claims = json!({"iss": iss, "aud": aud, "sub": "u",
|
||||
"nostr_pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"iat": wall_now, "exp": wall_now + 600});
|
||||
let mut hdr = Header::new(Algorithm::ES256);
|
||||
hdr.kid = Some(kid.to_owned());
|
||||
@@ -1572,7 +1575,6 @@ async fn shared_arc_source_verifier_rejects_expired_a1_accepts_a2() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
HARD_DEADLINE_SECS,
|
||||
None,
|
||||
|
||||
@@ -21,7 +21,6 @@ fn make_offline_policy(issuer: &str) -> IssuerPolicy {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![JwtAlgorithm::ES256],
|
||||
false,
|
||||
0,
|
||||
3600,
|
||||
None,
|
||||
@@ -37,7 +36,6 @@ fn make_status_policy(issuer: &str) -> IssuerPolicy {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::CurrentStatus,
|
||||
vec![JwtAlgorithm::ES256],
|
||||
false,
|
||||
0,
|
||||
3600,
|
||||
Some(60),
|
||||
|
||||
@@ -362,7 +362,7 @@ impl<S: IssuerKeySource> FederatedAssertionVerifier<S> {
|
||||
enforce_claim_semantics(policy, &claims)?;
|
||||
|
||||
let subject = claim_string(&claims, SUBJECT_CLAIM, MAX_SUBJECT_BYTES)?;
|
||||
let asserted_key = parse_nostr_pubkey_claim(policy, &claims)?;
|
||||
let asserted_key = parse_nostr_pubkey_claim(&claims)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let deadlines = self.check_time_and_deadlines(policy, &key_set, &claims, now)?;
|
||||
@@ -720,20 +720,13 @@ fn enforce_claim_semantics(
|
||||
}
|
||||
|
||||
/// Parse the fixed `nostr_pubkey` claim: lowercase hex of exactly one 32-byte
|
||||
/// key. Bech32 and other aliases deny. Absence is permitted unless the policy
|
||||
/// requires an attested key.
|
||||
/// key. Bech32 and other aliases deny. Absence denies; the merged NIP-FI
|
||||
/// spec v2 (PR #7214) requires the `nostr_pubkey` claim unconditionally.
|
||||
fn parse_nostr_pubkey_claim(
|
||||
policy: &IssuerPolicy,
|
||||
claims: &Map<String, Value>,
|
||||
) -> Result<Option<PublicKey>, VerifierError> {
|
||||
match claims.get(NOSTR_PUBKEY_CLAIM) {
|
||||
None => {
|
||||
if policy.require_attested_key() {
|
||||
Err(VerifierError::ClaimRejected)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
None => Err(VerifierError::ClaimRejected),
|
||||
Some(value) => {
|
||||
let raw = value.as_str().ok_or(VerifierError::ClaimRejected)?;
|
||||
if raw.len() != 64
|
||||
|
||||
@@ -27,6 +27,9 @@ const TEST_JWK_Y: &str = "WqQXVwaD6NM7us40BUTNe9dRa1XoJ0NX6vJuJWYU_bA";
|
||||
const TEST_KID: &str = "test-key-1";
|
||||
const ISSUER: &str = "https://issuer.example";
|
||||
const AUDIENCE: &str = "https://relay.example";
|
||||
/// A canonical lowercase-hex nostr pubkey for tokens that are not testing
|
||||
/// the nostr_pubkey claim specifically. Spec v2 requires the claim unconditionally.
|
||||
const TEST_NOSTR_PUBKEY: &str = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
|
||||
|
||||
/// A canonical JWKS contract for the default test issuer. Used wherever a
|
||||
/// `JwksSourceContract` is required but JWKS behavior is not under test.
|
||||
@@ -109,7 +112,6 @@ fn access_token_policy_with(subject_class: SubjectClassContract) -> IssuerPolicy
|
||||
TokenClass::AccessTokenAtJwt { subject_class },
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -131,7 +133,6 @@ fn dedicated_policy(issuer: &str) -> IssuerPolicy {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -147,7 +148,6 @@ fn dedicated_policy_with_audiences(audiences: Vec<String>) -> IssuerPolicy {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -163,7 +163,6 @@ fn dedicated_policy_with_algorithms(algorithms: Vec<Algorithm>) -> IssuerPolicy
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
algorithms,
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -198,6 +197,10 @@ fn mint_signed_by(pkcs8_pem: &str, typ: Option<&str>, kid: &str, mut claims: Val
|
||||
obj.entry("aud").or_insert(json!(AUDIENCE));
|
||||
obj.entry("iat").or_insert(json!(now()));
|
||||
obj.entry("exp").or_insert(json!(now() + 600));
|
||||
// Spec v2 requires nostr_pubkey unconditionally; inject a canonical
|
||||
// test pubkey so tokens that test other behaviours pass the claim check.
|
||||
obj.entry(NOSTR_PUBKEY_CLAIM)
|
||||
.or_insert(json!(TEST_NOSTR_PUBKEY));
|
||||
}
|
||||
let mut header = Header::new(Algorithm::ES256);
|
||||
header.kid = Some(kid.to_owned());
|
||||
@@ -206,6 +209,26 @@ fn mint_signed_by(pkcs8_pem: &str, typ: Option<&str>, kid: &str, mut claims: Val
|
||||
jsonwebtoken::encode(&header, &claims, &key).expect("sign")
|
||||
}
|
||||
|
||||
/// Mint a valid, signed token that deliberately omits `nostr_pubkey`. Used
|
||||
/// only to exercise the unconditional missing-claim rejection path; the normal
|
||||
/// `mint`/`mint_signed_by` helpers always inject the claim via `or_insert` so
|
||||
/// they cannot produce an absent-claim token.
|
||||
fn mint_no_pubkey(typ: Option<&str>, kid: &str, mut claims: Value) -> String {
|
||||
{
|
||||
let obj = claims.as_object_mut().expect("claims object");
|
||||
obj.entry("iss").or_insert(json!(ISSUER));
|
||||
obj.entry("aud").or_insert(json!(AUDIENCE));
|
||||
obj.entry("iat").or_insert(json!(now()));
|
||||
obj.entry("exp").or_insert(json!(now() + 600));
|
||||
// Intentionally does NOT inject nostr_pubkey.
|
||||
}
|
||||
let mut header = Header::new(Algorithm::ES256);
|
||||
header.kid = Some(kid.to_owned());
|
||||
header.typ = typ.map(str::to_owned);
|
||||
let key = EncodingKey::from_ec_pem(TEST_EC_PKCS8_PEM.as_bytes()).expect("valid EC PEM");
|
||||
jsonwebtoken::encode(&header, &claims, &key).expect("sign")
|
||||
}
|
||||
|
||||
/// A resource-owner `at+jwt` claim set: valid subject-class marker plus client_id.
|
||||
fn resource_owner_claims() -> Value {
|
||||
json!({ "sub": "user-123", "client_id": "app-1", "sub_type": "user" })
|
||||
@@ -240,7 +263,8 @@ fn valid_access_token_verifies() {
|
||||
let assertion = verifier.verify(&token).expect("verifies");
|
||||
assert_eq!(assertion.identity().issuer(), ISSUER);
|
||||
assert_eq!(assertion.identity().subject(), "user-123");
|
||||
assert!(assertion.asserted_key().is_none());
|
||||
// Spec v2: nostr_pubkey is injected by mint() and unconditionally required.
|
||||
assert!(assertion.asserted_key().is_some());
|
||||
assert!(!assertion.authority_deadlines().is_empty());
|
||||
assert_eq!(assertion.assertion_policy_id(), access_token_policy().id());
|
||||
}
|
||||
@@ -698,22 +722,18 @@ fn uppercase_nostr_pubkey_denies() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_nostr_pubkey_denies_under_attested_key_policy() {
|
||||
let policy = IssuerPolicy::new(
|
||||
ISSUER.to_owned(),
|
||||
vec![AUDIENCE.to_owned()],
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
true, // require attested key
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
test_jwks_contract(),
|
||||
)
|
||||
.unwrap();
|
||||
let verifier = verifier_with(policy);
|
||||
let token = mint(Some("nip-fi+jwt"), TEST_KID, json!({ "sub": "u" }));
|
||||
fn absent_nostr_pubkey_claim_denies() {
|
||||
// `nostr_pubkey` absence must unconditionally reject — NIP-FI v2 dropped
|
||||
// the per-issuer `require_attested_key` knob that previously made it
|
||||
// optional. This is a direct falsifiable regression test: removing the
|
||||
// `None => Err(VerifierError::ClaimRejected)` arm from
|
||||
// `parse_nostr_pubkey_claim` must turn this test red.
|
||||
let verifier = verifier_with(access_token_policy());
|
||||
let token = mint_no_pubkey(
|
||||
Some("at+jwt"),
|
||||
TEST_KID,
|
||||
json!({ "sub": "u", "client_id": "a", "sub_type": "user" }),
|
||||
);
|
||||
assert_eq!(
|
||||
verifier.verify(&token).unwrap_err(),
|
||||
VerifierError::ClaimRejected
|
||||
@@ -1105,7 +1125,6 @@ fn current_status_policy() -> IssuerPolicy {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::CurrentStatus,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
Some(120), // maximum_status_age required for current-status
|
||||
@@ -1385,7 +1404,6 @@ fn assertion_policy_id_is_deterministic_and_semantic() {
|
||||
changed.token_class().clone(),
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
120, // different skew => different semantics
|
||||
3600,
|
||||
None,
|
||||
@@ -1411,7 +1429,6 @@ fn offline_policy_rejects_inapplicable_maximum_status_age() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
Some(120),
|
||||
@@ -1430,7 +1447,6 @@ fn offline_policy_accepts_absent_maximum_status_age() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -1449,7 +1465,6 @@ fn current_status_policy_still_requires_positive_maximum_status_age() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::CurrentStatus,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
@@ -1463,7 +1478,6 @@ fn current_status_policy_still_requires_positive_maximum_status_age() {
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::CurrentStatus,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
Some(0),
|
||||
@@ -1579,7 +1593,6 @@ fn policy_with_contract(contract: crate::nip_fi::jwks::JwksSourceContract) -> Is
|
||||
TokenClass::DedicatedNipFi,
|
||||
FreshnessClass::OfflineJwt,
|
||||
vec![Algorithm::ES256],
|
||||
false,
|
||||
60,
|
||||
3600,
|
||||
None,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user