From ce2b5b63b40d2339c45762aee0ffced1127abc23 Mon Sep 17 00:00:00 2001 From: woikos Date: Thu, 5 Feb 2026 12:13:42 +0100 Subject: [PATCH] Add optional server tag requirement for Blossom DELETE replay protection - Add ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG config option (default: false) - Add ValidateAuthEventForDelete() for optional server tag validation - Update handleDeleteBlob to use new validation - Prevents cross-server replay attacks when enabled Based on discussion in hzrd149/blossom PR #87 Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- app/blossom.go | 2 ++ app/config/config.go | 3 +++ pkg/blossom/auth.go | 52 +++++++++++++++++++++++++++++++++++++++++ pkg/blossom/handlers.go | 5 +++- pkg/blossom/server.go | 28 +++++++++++++--------- pkg/version/version | 2 +- 7 files changed, 80 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5040f40..ddd5edc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -281,7 +281,7 @@ The transport manager handles ordered startup (Start fails fast, rolls back) and ## Deploying to relay.orly.dev -- **Architecture**: x86_64 (amd64) +- **Architecture**: **x86_64 (amd64)** — NOT arm64, always use `GOARCH=amd64` - **OS**: Ubuntu 24.04 LTS - **SSH**: `ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes root@69.164.249.71` - **Service**: `systemctl {start|stop|restart|status} orly` diff --git a/app/blossom.go b/app/blossom.go index e0150c4..7dfe112 100644 --- a/app/blossom.go +++ b/app/blossom.go @@ -26,6 +26,8 @@ func initializeBlossomServer( RateLimitEnabled: cfg.BlossomRateLimitEnabled, DailyLimitMB: cfg.BlossomDailyLimitMB, BurstLimitMB: cfg.BlossomBurstLimitMB, + // Delete replay protection (proposed BUD enhancement) + DeleteRequireServerTag: cfg.BlossomDeleteRequireServerTag, } // Create blossom server with relay's ACL registry diff --git a/app/config/config.go b/app/config/config.go index 20a93bb..9f9d649 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -82,6 +82,9 @@ type C struct { BlossomDailyLimitMB int64 `env:"ORLY_BLOSSOM_DAILY_LIMIT_MB" default:"10" usage:"daily upload limit in MB for non-followed users (EMA averaged)"` BlossomBurstLimitMB int64 `env:"ORLY_BLOSSOM_BURST_LIMIT_MB" default:"50" usage:"max burst upload in MB (bucket cap)"` + // Blossom delete replay protection (proposed BUD enhancement) + BlossomDeleteRequireServerTag bool `env:"ORLY_BLOSSOM_DELETE_REQUIRE_SERVER_TAG" default:"false" usage:"require server tag in delete auth events to prevent cross-server replay attacks (not yet ratified in spec)"` + // Web UI and dev mode settings WebDisableEmbedded bool `env:"ORLY_WEB_DISABLE" default:"false" usage:"disable serving the embedded web UI; useful for hot-reload during development"` WebDevProxyURL string `env:"ORLY_WEB_DEV_PROXY_URL" usage:"when ORLY_WEB_DISABLE is true, reverse-proxy non-API paths to this dev server URL (e.g. http://localhost:5173)"` diff --git a/pkg/blossom/auth.go b/pkg/blossom/auth.go index d5338e7..60bce32 100644 --- a/pkg/blossom/auth.go +++ b/pkg/blossom/auth.go @@ -273,6 +273,58 @@ func ValidateAuthEventForGet( return } +// ValidateAuthEventForDelete validates authorization for DELETE requests (BUD-02) +// If requireServerTag is true, the auth event must include a 'server' tag matching the serverURL +// This prevents cross-server replay attacks where a malicious server replays delete auth events +func ValidateAuthEventForDelete( + r *http.Request, serverURL string, sha256Hash []byte, requireServerTag bool, +) (authEv *AuthEvent, err error) { + // First do the standard validation + if authEv, err = ValidateAuthEvent(r, "delete", sha256Hash); chk.E(err) { + return + } + + // If server tag is not required, we're done + if !requireServerTag { + return + } + + // Extract event again to check server tags + var ev *event.E + if ev, err = ExtractAuthEvent(r); chk.E(err) { + return + } + + // Check for server tag + serverTags := ev.Tags.GetAll([]byte("server")) + if len(serverTags) == 0 { + err = errorf.E( + "delete authorization requires 'server' tag for replay protection", + ) + return + } + + // Verify at least one server tag matches + found := false + for _, serverTag := range serverTags { + serverTagValue := string(serverTag.Value()) + if strings.HasPrefix(serverURL, serverTagValue) { + found = true + break + } + } + + if !found { + err = errorf.E( + "no 'server' tag matches this server URL '%s'", + serverURL, + ) + return + } + + return +} + // GetPubkeyFromRequest extracts pubkey from Authorization header if present func GetPubkeyFromRequest(r *http.Request) (pubkey []byte, err error) { authHeader := r.Header.Get(AuthorizationHeader) diff --git a/pkg/blossom/handlers.go b/pkg/blossom/handlers.go index 649f212..1e9d34e 100644 --- a/pkg/blossom/handlers.go +++ b/pkg/blossom/handlers.go @@ -542,7 +542,10 @@ func (s *Server) handleDeleteBlob(w http.ResponseWriter, r *http.Request) { } // Authorization required for delete - authEv, err := ValidateAuthEvent(r, "delete", sha256Hash) + // Use ValidateAuthEventForDelete which optionally requires server tag for replay protection + authEv, err := ValidateAuthEventForDelete( + r, s.getBaseURL(r), sha256Hash, s.deleteRequireServerTag, + ) if err != nil { s.setErrorResponse(w, http.StatusUnauthorized, err.Error()) return diff --git a/pkg/blossom/server.go b/pkg/blossom/server.go index 74ce1d4..ed2c776 100644 --- a/pkg/blossom/server.go +++ b/pkg/blossom/server.go @@ -16,9 +16,10 @@ type Server struct { baseURL string // Configuration - maxBlobSize int64 - allowedMimeTypes map[string]bool - requireAuth bool + maxBlobSize int64 + allowedMimeTypes map[string]bool + requireAuth bool + deleteRequireServerTag bool // Rate limiting for uploads bandwidthLimiter *BandwidthLimiter @@ -35,6 +36,10 @@ type Config struct { RateLimitEnabled bool DailyLimitMB int64 BurstLimitMB int64 + + // Delete replay protection (proposed BUD enhancement) + // When true, DELETE auth events must include a 'server' tag matching this server + DeleteRequireServerTag bool } // NewServer creates a new Blossom server instance @@ -71,14 +76,15 @@ func NewServer(db database.Database, aclRegistry *acl.S, cfg *Config) *Server { } return &Server{ - db: db, - storage: storage, - acl: aclRegistry, - baseURL: cfg.BaseURL, - maxBlobSize: cfg.MaxBlobSize, - allowedMimeTypes: allowedMap, - requireAuth: cfg.RequireAuth, - bandwidthLimiter: bwLimiter, + db: db, + storage: storage, + acl: aclRegistry, + baseURL: cfg.BaseURL, + maxBlobSize: cfg.MaxBlobSize, + allowedMimeTypes: allowedMap, + requireAuth: cfg.RequireAuth, + deleteRequireServerTag: cfg.DeleteRequireServerTag, + bandwidthLimiter: bwLimiter, } } diff --git a/pkg/version/version b/pkg/version/version index 3d6869f..861d9e9 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.58.9 +v0.58.10