diff --git a/pkg/policy/new_fields_test.go b/pkg/policy/new_fields_test.go index 4703347..ad5e774 100644 --- a/pkg/policy/new_fields_test.go +++ b/pkg/policy/new_fields_test.go @@ -113,11 +113,11 @@ func TestMaxExpiryDuration(t *testing.T) { expectAllow: true, }, { - name: "valid expiry at exact limit", + name: "expiry at exact limit rejected", maxExpiryDuration: "PT1H", - eventExpiry: 3600, // exactly 1 hour + eventExpiry: 3600, // exactly 1 hour - >= means this is rejected hasExpiryTag: true, - expectAllow: true, + expectAllow: false, }, { name: "expiry exceeds limit", @@ -235,6 +235,79 @@ func TestMaxExpiryDurationPrecedence(t *testing.T) { } } +// Test that max_expiry_duration only applies to writes, not reads +func TestMaxExpiryDurationWriteOnly(t *testing.T) { + signer, pubkey := generateTestKeypair(t) + + // Policy with strict max_expiry_duration + policyJSON := []byte(`{ + "default_policy": "allow", + "rules": { + "4": { + "description": "DM events with expiry", + "max_expiry_duration": "PT10M", + "privileged": true + } + } + }`) + + policy, err := New(policyJSON) + if err != nil { + t.Fatalf("Failed to create policy: %v", err) + } + + // Create event WITHOUT an expiry tag - this would fail write validation + // but should still be readable + ev := createTestEventForNewFields(t, signer, "test DM", 4) + if err := ev.Sign(signer); chk.E(err) { + t.Fatalf("Failed to sign: %v", err) + } + + // Write should fail (no expiry tag when max_expiry_duration is set) + allowed, err := policy.CheckPolicy("write", ev, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy write error: %v", err) + } + if allowed { + t.Error("Write should be denied for event without expiry tag when max_expiry_duration is set") + } + + // Read should succeed (validation constraints don't apply to reads) + allowed, err = policy.CheckPolicy("read", ev, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy read error: %v", err) + } + if !allowed { + t.Error("Read should be allowed - max_expiry_duration is write-only validation") + } + + // Also test with an event that has expiry exceeding the limit + ev2 := createTestEventForNewFields(t, signer, "test DM 2", 4) + expiryTs := ev2.CreatedAt + 7200 // 2 hours - exceeds 10 minute limit + addTagString(ev2, "expiration", int64ToString(expiryTs)) + if err := ev2.Sign(signer); chk.E(err) { + t.Fatalf("Failed to sign: %v", err) + } + + // Write should fail (expiry exceeds limit) + allowed, err = policy.CheckPolicy("write", ev2, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy write error: %v", err) + } + if allowed { + t.Error("Write should be denied for event with expiry exceeding max_expiry_duration") + } + + // Read should still succeed + allowed, err = policy.CheckPolicy("read", ev2, pubkey, "127.0.0.1") + if err != nil { + t.Fatalf("CheckPolicy read error: %v", err) + } + if !allowed { + t.Error("Read should be allowed - max_expiry_duration is write-only validation") + } +} + // ============================================================================= // ProtectedRequired Tests // ============================================================================= diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 65688f9..1f9080b 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -1280,7 +1280,8 @@ func (p *P) checkRulePolicy( } // Check required tags - if len(rule.MustHaveTags) > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && len(rule.MustHaveTags) > 0 { for _, requiredTag := range rule.MustHaveTags { if ev.Tags.GetFirst([]byte(requiredTag)) == nil { return false, nil @@ -1289,7 +1290,8 @@ func (p *P) checkRulePolicy( } // Check expiry time (uses maxExpirySeconds which is parsed from MaxExpiryDuration or MaxExpiry) - if rule.maxExpirySeconds != nil && *rule.maxExpirySeconds > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.maxExpirySeconds != nil && *rule.maxExpirySeconds > 0 { expiryTag := ev.Tags.GetFirst([]byte("expiration")) if expiryTag == nil { return false, nil // Must have expiry if max_expiry is set @@ -1302,7 +1304,7 @@ func (p *P) checkRulePolicy( return false, nil // Invalid expiry format } maxAllowedExpiry := ev.CreatedAt + *rule.maxExpirySeconds - if expiryTs > maxAllowedExpiry { + if expiryTs >= maxAllowedExpiry { log.D.F("expiration %d exceeds max allowed %d (created_at %d + max_expiry %d)", expiryTs, maxAllowedExpiry, ev.CreatedAt, *rule.maxExpirySeconds) return false, nil // Expiry too far in the future @@ -1310,7 +1312,8 @@ func (p *P) checkRulePolicy( } // Check ProtectedRequired (NIP-70: events must have "-" tag) - if rule.ProtectedRequired { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.ProtectedRequired { protectedTag := ev.Tags.GetFirst([]byte("-")) if protectedTag == nil { log.D.F("protected_required: event missing '-' tag (NIP-70)") @@ -1319,7 +1322,8 @@ func (p *P) checkRulePolicy( } // Check IdentifierRegex (validates "d" tag values) - if rule.identifierRegexCache != nil { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.identifierRegexCache != nil { dTags := ev.Tags.GetAll([]byte("d")) if len(dTags) == 0 { log.D.F("identifier_regex: event missing 'd' tag") @@ -1336,7 +1340,8 @@ func (p *P) checkRulePolicy( } // Check MaxAgeOfEvent (maximum age of event in seconds) - if rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 { currentTime := time.Now().Unix() maxAllowedTime := currentTime - *rule.MaxAgeOfEvent if ev.CreatedAt < maxAllowedTime { @@ -1345,7 +1350,8 @@ func (p *P) checkRulePolicy( } // Check MaxAgeEventInFuture (maximum time event can be in the future in seconds) - if rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 { + // Only apply for write access - we validate what goes in, not what comes out + if access == "write" && rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 { currentTime := time.Now().Unix() maxFutureTime := currentTime + *rule.MaxAgeEventInFuture if ev.CreatedAt > maxFutureTime { diff --git a/pkg/version/version b/pkg/version/version index 477e302..c580c49 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.31.6 +v0.31.7