Browse Source

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 <noreply@anthropic.com>
imwald-v0.58.10
woikos 3 months ago
parent
commit
ce2b5b63b4
No known key found for this signature in database
  1. 2
      CLAUDE.md
  2. 2
      app/blossom.go
  3. 3
      app/config/config.go
  4. 52
      pkg/blossom/auth.go
  5. 5
      pkg/blossom/handlers.go
  6. 28
      pkg/blossom/server.go
  7. 2
      pkg/version/version

2
CLAUDE.md

@ -281,7 +281,7 @@ The transport manager handles ordered startup (Start fails fast, rolls back) and
## Deploying to relay.orly.dev ## Deploying to relay.orly.dev
- **Architecture**: x86_64 (amd64) - **Architecture**: **x86_64 (amd64)** — NOT arm64, always use `GOARCH=amd64`
- **OS**: Ubuntu 24.04 LTS - **OS**: Ubuntu 24.04 LTS
- **SSH**: `ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes root@69.164.249.71` - **SSH**: `ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes root@69.164.249.71`
- **Service**: `systemctl {start|stop|restart|status} orly` - **Service**: `systemctl {start|stop|restart|status} orly`

2
app/blossom.go

@ -26,6 +26,8 @@ func initializeBlossomServer(
RateLimitEnabled: cfg.BlossomRateLimitEnabled, RateLimitEnabled: cfg.BlossomRateLimitEnabled,
DailyLimitMB: cfg.BlossomDailyLimitMB, DailyLimitMB: cfg.BlossomDailyLimitMB,
BurstLimitMB: cfg.BlossomBurstLimitMB, BurstLimitMB: cfg.BlossomBurstLimitMB,
// Delete replay protection (proposed BUD enhancement)
DeleteRequireServerTag: cfg.BlossomDeleteRequireServerTag,
} }
// Create blossom server with relay's ACL registry // Create blossom server with relay's ACL registry

3
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)"` 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)"` 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 // 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"` 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)"` 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)"`

52
pkg/blossom/auth.go

@ -273,6 +273,58 @@ func ValidateAuthEventForGet(
return 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 // GetPubkeyFromRequest extracts pubkey from Authorization header if present
func GetPubkeyFromRequest(r *http.Request) (pubkey []byte, err error) { func GetPubkeyFromRequest(r *http.Request) (pubkey []byte, err error) {
authHeader := r.Header.Get(AuthorizationHeader) authHeader := r.Header.Get(AuthorizationHeader)

5
pkg/blossom/handlers.go

@ -542,7 +542,10 @@ func (s *Server) handleDeleteBlob(w http.ResponseWriter, r *http.Request) {
} }
// Authorization required for delete // 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 { if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error()) s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return return

28
pkg/blossom/server.go

@ -16,9 +16,10 @@ type Server struct {
baseURL string baseURL string
// Configuration // Configuration
maxBlobSize int64 maxBlobSize int64
allowedMimeTypes map[string]bool allowedMimeTypes map[string]bool
requireAuth bool requireAuth bool
deleteRequireServerTag bool
// Rate limiting for uploads // Rate limiting for uploads
bandwidthLimiter *BandwidthLimiter bandwidthLimiter *BandwidthLimiter
@ -35,6 +36,10 @@ type Config struct {
RateLimitEnabled bool RateLimitEnabled bool
DailyLimitMB int64 DailyLimitMB int64
BurstLimitMB 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 // NewServer creates a new Blossom server instance
@ -71,14 +76,15 @@ func NewServer(db database.Database, aclRegistry *acl.S, cfg *Config) *Server {
} }
return &Server{ return &Server{
db: db, db: db,
storage: storage, storage: storage,
acl: aclRegistry, acl: aclRegistry,
baseURL: cfg.BaseURL, baseURL: cfg.BaseURL,
maxBlobSize: cfg.MaxBlobSize, maxBlobSize: cfg.MaxBlobSize,
allowedMimeTypes: allowedMap, allowedMimeTypes: allowedMap,
requireAuth: cfg.RequireAuth, requireAuth: cfg.RequireAuth,
bandwidthLimiter: bwLimiter, deleteRequireServerTag: cfg.DeleteRequireServerTag,
bandwidthLimiter: bwLimiter,
} }
} }

2
pkg/version/version

@ -1 +1 @@
v0.58.9 v0.58.10

Loading…
Cancel
Save