R-879: seal box API keys, owner passphrases, controller keys and PBS-DR tokens at rest
hosts.api_key, customer_configs.api_key / retrieval_password and host_pbs_secrets.value now hold the R-821/R-133 seal (enc:v1:, OFFSITE_SECRET_KEY). The two API keys get an api_key_hash lookup twin (SHA-256, backfilled keyless in migrate()), so box authentication never needs the sealing key; a row with no hash is matched on its plaintext only while it is plaintext. SealLegacyBoxSecrets seals legacy rows at start-up (idempotent, non-fatal). A sealed value that does not open sets SecretsUnreadable: serve/compare paths answer 500, saves refuse the record, the PBS token is not burned. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1101,6 +1101,12 @@ func (h *Handler) handleHostEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// R-879: a sealed passphrase that did not open is a hub fault (500), never a wrong password.
|
||||
if cc.SecretsUnreadable {
|
||||
h.logger.Printf("[ERROR] customer %s: sealed secrets do not open (OFFSITE_SECRET_KEY)", cc.CustomerID)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(cc.RetrievalPassword)) != 1 {
|
||||
http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -1114,6 +1120,12 @@ func (h *Handler) handleHostEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if existing != nil {
|
||||
// R-879: never re-serve an empty key because the seal did not open.
|
||||
if existing.SecretsUnreadable || existing.APIKey == "" {
|
||||
h.logger.Printf("[ERROR] host-enroll: host %s key unreadable (sealed key does not open)", existing.HostID)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"host_id": existing.HostID, "api_key": existing.APIKey})
|
||||
@@ -2570,6 +2582,12 @@ func (h *Handler) handleRecovery(w http.ResponseWriter, r *http.Request, custome
|
||||
return
|
||||
}
|
||||
|
||||
// R-879: a sealed passphrase that did not open is a hub fault (500), never a wrong password.
|
||||
if cfg.SecretsUnreadable {
|
||||
h.logger.Printf("[ERROR] customer %s: sealed secrets do not open (OFFSITE_SECRET_KEY)", cfg.CustomerID)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(cfg.RetrievalPassword)) != 1 {
|
||||
http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -2635,6 +2653,12 @@ func (h *Handler) handleConfigRetrieve(w http.ResponseWriter, r *http.Request, c
|
||||
return
|
||||
}
|
||||
|
||||
// R-879: a sealed passphrase that did not open is a hub fault (500), never a wrong password.
|
||||
if cfg.SecretsUnreadable {
|
||||
h.logger.Printf("[ERROR] customer %s: sealed secrets do not open (OFFSITE_SECRET_KEY)", cfg.CustomerID)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(cfg.RetrievalPassword)) != 1 {
|
||||
http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
|
||||
@@ -2717,6 +2741,12 @@ func (h *Handler) handleArtifactManifest(w http.ResponseWriter, r *http.Request,
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// R-879: a sealed passphrase that did not open is a hub fault (500), never a wrong password.
|
||||
if cfg.SecretsUnreadable {
|
||||
h.logger.Printf("[ERROR] customer %s: sealed secrets do not open (OFFSITE_SECRET_KEY)", cfg.CustomerID)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(password), []byte(cfg.RetrievalPassword)) != 1 {
|
||||
http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user