router: verify narinfo signatures against configured upstream keys

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If60c9653495c0ffd637e75797c6e55326a6a6964
This commit is contained in:
raf 2026-03-06 18:24:47 +03:00
commit df92c9a4a3
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -33,24 +33,39 @@ type Result struct {
// Resolves store paths to the best upstream via cache lookup or parallel racing.
type Router struct {
db *cache.DB
prober *prober.Prober
routeTTL time.Duration
raceTimeout time.Duration
client *http.Client
db *cache.DB
prober *prober.Prober
routeTTL time.Duration
raceTimeout time.Duration
client *http.Client
mu sync.RWMutex
upstreamKeys map[string]string // upstream URL → Nix public key string
}
// Creates a Router.
func New(db *cache.DB, p *prober.Prober, routeTTL, raceTimeout time.Duration) *Router {
return &Router{
db: db,
prober: p,
routeTTL: routeTTL,
raceTimeout: raceTimeout,
client: &http.Client{Timeout: raceTimeout},
db: db,
prober: p,
routeTTL: routeTTL,
raceTimeout: raceTimeout,
client: &http.Client{Timeout: raceTimeout},
upstreamKeys: make(map[string]string),
}
}
// Registers a Nix public key for narinfo signature verification on a given upstream.
// pubKeyStr must be in "name:base64(key)" format (e.g. "cache.nixos.org-1:...").
func (r *Router) SetUpstreamKey(url, pubKeyStr string) error {
if _, _, err := narinfo.ParsePublicKey(pubKeyStr); err != nil {
return err
}
r.mu.Lock()
r.upstreamKeys[url] = pubKeyStr
r.mu.Unlock()
return nil
}
// Returns the best upstream for the given store hash.
// Checks the route cache first; on miss races the provided candidates.
func (r *Router) Resolve(storeHash string, candidates []string) (*Result, error) {
@ -179,8 +194,9 @@ func (r *Router) race(storeHash string, candidates []string) (*Result, error) {
return &Result{URL: winner.url, LatencyMs: winner.latencyMs, NarInfoBytes: narInfoBytes}, nil
}
// Fetches narinfo content from upstream and parses metadata.
// Returns (body, narHash, narSize); body may be non-nil even on parse error.
// Fetches narinfo content from upstream, verifies its signature if a key is
// configured for that upstream, and returns (body, narHash, narSize).
// Returns (nil, "", 0) if the fetch fails or signature verification fails.
func (r *Router) fetchNarInfo(upstream, storeHash string) ([]byte, string, uint64) {
url := upstream + "/" + storeHash + ".narinfo"
resp, err := r.client.Get(url)
@ -199,5 +215,19 @@ func (r *Router) fetchNarInfo(upstream, storeHash string) ([]byte, string, uint6
if err != nil {
return body, "", 0
}
r.mu.RLock()
pubKeyStr := r.upstreamKeys[upstream]
r.mu.RUnlock()
if pubKeyStr != "" {
ok, err := ni.Verify(pubKeyStr)
if err != nil {
slog.Warn("narinfo: public key parse error", "upstream", upstream, "error", err)
return nil, "", 0
}
if !ok {
slog.Warn("narinfo: signature verification failed", "upstream", upstream, "store", storeHash)
return nil, "", 0
}
}
return body, ni.NarHash, ni.NarSize
}