Skip to content

Commit

Permalink
feat(cmd): add cache-secret-key-path flag for custom signing keys (#176)
Browse files Browse the repository at this point in the history
Add support for providing a secret key file.

The `--cache-secret-key-path` flag allows specifying a file containing a secret key for signing cached paths. When provided, this key will be used instead of generating and storing a new one.

Added documentation for the new flag in the README.
  • Loading branch information
kalbasit authored Jan 2, 2025
1 parent d928c75 commit bccfa5f
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 31 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ These options are specific to the `ncps serve` command:
- `--cache-max-size`: The maximum size of the store. It can be given with units such as 5K, 10G etc. Supported units: B, K, M, G, T (Environment variable: `$CACHE_MAX_SIZE`)
- `--cache-lru-schedule`: The cron spec for cleaning the store to keep it under `--cache-max-size`. Refer to https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Usage for documentation (Environment variable: `$CACHE_LRU_SCHEDULE`)
- `--cache-lru-schedule-timezone`: The name of the timezone to use for the cron schedule (default: "Local"). (Environment variable: `$CACHE_LRU_SCHEDULE_TZ`)
- `--cache-secret-key-path`: The path to the secret key used for signing cached paths. (Environment variable: `$CACHE_SECRET_KEY_PATH`)
- `--server-addr`: The address and port the server listens on (default: ":8501"). (Environment variable: `$SERVER_ADDR`)
- `--upstream-cache`: The URL of an upstream binary cache (e.g., `https://cache.nixos.org`). This flag can be used multiple times to specify multiple upstream caches. (Environment variable: `$UPSTREAM_CACHES`)
- `--upstream-public-key`: The public key of an upstream cache in the format `host:public-key`. This flag is used to verify the signatures of store paths downloaded from upstream caches. This flag can be used multiple times, once for each upstream cache. (Environment variable: `$UPSTREAM_PUBLIC_KEYS`)
Expand Down
6 changes: 6 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ func serveCommand() *cli.Command {
Sources: cli.EnvVars("CACHE_LRU_SCHEDULE_TZ"),
Value: "Local",
},
&cli.StringFlag{
Name: "cache-secret-key-path",
Usage: "The path to the secret key used for signing cached paths",
Sources: cli.EnvVars("CACHE_SECRET_KEY_PATH"),
},
&cli.StringFlag{
Name: "server-addr",
Usage: "The address of the server",
Expand Down Expand Up @@ -225,6 +230,7 @@ func createCache(
localStore,
localStore,
localStore,
cmd.String("cache-secret-key-path"),
)
if err != nil {
return nil, fmt.Errorf("error creating a new cache: %w", err)
Expand Down
41 changes: 28 additions & 13 deletions pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/http"
"net/url"
"os"
"slices"
"strings"
"sync"
Expand Down Expand Up @@ -93,6 +94,7 @@ func New(
configStore storage.ConfigStore,
narInfoStore storage.NarInfoStore,
narStore storage.NarStore,
secretKeyPath string,
) (*Cache, error) {
c := &Cache{
baseContext: ctx,
Expand All @@ -111,13 +113,10 @@ func New(

c.hostName = hostName

sk, err := c.setupSecretKey(ctx)
if err != nil {
if err := c.setupSecretKey(ctx, secretKeyPath); err != nil {
return c, fmt.Errorf("error setting up the secret key: %w", err)
}

c.secretKey = sk

return c, nil
}

Expand Down Expand Up @@ -1170,26 +1169,42 @@ func (c *Cache) validateHostname(hostName string) error {
return nil
}

func (c *Cache) setupSecretKey(ctx context.Context) (signature.SecretKey, error) {
sk, err := c.configStore.GetSecretKey(ctx)
func (c *Cache) setupSecretKey(ctx context.Context, secretKeyPath string) error {
if secretKeyPath != "" {
skc, err := os.ReadFile(secretKeyPath)
if err != nil {
return fmt.Errorf("error reading the given secret key located at %q: %w", secretKeyPath, err)
}

c.secretKey, err = signature.LoadSecretKey(string(skc))
if err != nil {
return fmt.Errorf("error loading the given secret key located at %q: %w", secretKeyPath, err)
}

return nil
}

var err error

c.secretKey, err = c.configStore.GetSecretKey(ctx)
if err == nil {
return sk, nil
return nil
}

if !errors.Is(err, storage.ErrNotFound) {
return sk, fmt.Errorf("error fetching the secret key from the store: %w", err)
return fmt.Errorf("error fetching the secret key from the store: %w", err)
}

sk, _, err = signature.GenerateKeypair(c.hostName, nil)
c.secretKey, _, err = signature.GenerateKeypair(c.hostName, nil)
if err != nil {
return sk, fmt.Errorf("error generating a secret key pair: %w", err)
return fmt.Errorf("error generating a secret key pair: %w", err)
}

if err := c.configStore.PutSecretKey(ctx, sk); err != nil {
return sk, fmt.Errorf("error storing the generated secret key in the store: %w", err)
if err = c.configStore.PutSecretKey(ctx, c.secretKey); err != nil {
return fmt.Errorf("error storing the generated secret key in the store: %w", err)
}

return sk, nil
return nil
}

func (c *Cache) hasUpstreamJob(hash string) bool {
Expand Down
6 changes: 3 additions & 3 deletions pkg/cache/cache_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestAddUpstreamCaches(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.AddUpstreamCaches(newContext(), ucs...)
Expand Down Expand Up @@ -120,7 +120,7 @@ func TestAddUpstreamCaches(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

for _, uc := range ucs {
Expand Down Expand Up @@ -151,7 +151,7 @@ func TestRunLRU(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

ts := testdata.NewTestServer(t, 40)
Expand Down
85 changes: 74 additions & 11 deletions pkg/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestNew(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

_, err = cache.New(newContext(), "", db, localStore, localStore, localStore)
_, err = cache.New(newContext(), "", db, localStore, localStore, localStore, "")
assert.ErrorIs(t, err, cache.ErrHostnameRequired)
})

Expand All @@ -72,7 +72,7 @@ func TestNew(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

_, err = cache.New(newContext(), "https://cache.example.com", db, localStore, localStore, localStore)
_, err = cache.New(newContext(), "https://cache.example.com", db, localStore, localStore, localStore, "")
assert.ErrorIs(t, err, cache.ErrHostnameMustNotContainScheme)
})

Expand All @@ -90,7 +90,7 @@ func TestNew(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

_, err = cache.New(newContext(), "cache.example.com/path/to", db, localStore, localStore, localStore)
_, err = cache.New(newContext(), "cache.example.com/path/to", db, localStore, localStore, localStore, "")
assert.ErrorIs(t, err, cache.ErrHostnameMustNotContainPath)
})

Expand All @@ -108,10 +108,73 @@ func TestNew(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

_, err = cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
_, err = cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)
})
})

t.Run("secretKey", func(t *testing.T) {
t.Parallel()

t.Run("generated", func(t *testing.T) {
dir, err := os.MkdirTemp("", "cache-path-")
require.NoError(t, err)
defer os.RemoveAll(dir) // clean up

dbFile := filepath.Join(dir, "var", "ncps", "db", "db.sqlite")
testhelper.CreateMigrateDatabase(t, dbFile)

db, err := database.Open("sqlite:" + dbFile)
require.NoError(t, err)

localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

sk, err := localStore.GetSecretKey(newContext())
require.NoError(t, err)

assert.Equal(t, sk.ToPublicKey(), c.PublicKey(), "ensure the cache public key matches the one in the local store")
})

t.Run("given", func(t *testing.T) {
dir, err := os.MkdirTemp("", "cache-path-")
require.NoError(t, err)
defer os.RemoveAll(dir) // clean up

dbFile := filepath.Join(dir, "var", "ncps", "db", "db.sqlite")
testhelper.CreateMigrateDatabase(t, dbFile)

db, err := database.Open("sqlite:" + dbFile)
require.NoError(t, err)

localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

sk, _, err := signature.GenerateKeypair(cacheName, nil)
require.NoError(t, err)

skFile, err := os.CreateTemp("", "secret-key")
require.NoError(t, err)

defer os.Remove(skFile.Name())

_, err = skFile.WriteString(sk.String())
require.NoError(t, err)

require.NoError(t, skFile.Close())

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, skFile.Name())
require.NoError(t, err)

_, err = localStore.GetSecretKey(newContext())
require.ErrorIs(t, err, storage.ErrNotFound)

assert.Equal(t, sk.ToPublicKey(), c.PublicKey(), "ensure the cache public key matches the one given")
})
})
}

func TestPublicKey(t *testing.T) {
Expand All @@ -130,7 +193,7 @@ func TestPublicKey(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

pubKey := c.PublicKey().String()
Expand Down Expand Up @@ -172,7 +235,7 @@ func TestGetNarInfo(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.AddUpstreamCaches(newContext(), uc)
Expand Down Expand Up @@ -551,7 +614,7 @@ func TestPutNarInfo(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.SetRecordAgeIgnoreTouch(0)
Expand Down Expand Up @@ -691,7 +754,7 @@ func TestDeleteNarInfo(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.SetRecordAgeIgnoreTouch(0)
Expand Down Expand Up @@ -761,7 +824,7 @@ func TestGetNar(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.AddUpstreamCaches(newContext(), uc)
Expand Down Expand Up @@ -966,7 +1029,7 @@ func TestPutNar(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.SetRecordAgeIgnoreTouch(0)
Expand Down Expand Up @@ -1011,7 +1074,7 @@ func TestDeleteNar(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.SetRecordAgeIgnoreTouch(0)
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/server_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestSetDeletePermitted(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

t.Run("false", func(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestServeHTTP(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.AddUpstreamCaches(newContext(), uc)
Expand Down Expand Up @@ -176,7 +176,7 @@ func TestServeHTTP(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.AddUpstreamCaches(newContext(), uc)
Expand Down Expand Up @@ -289,7 +289,7 @@ func TestServeHTTP(t *testing.T) {
localStore, err := local.New(newContext(), dir)
require.NoError(t, err)

c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore)
c, err := cache.New(newContext(), cacheName, db, localStore, localStore, localStore, "")
require.NoError(t, err)

c.AddUpstreamCaches(newContext(), uc)
Expand Down

0 comments on commit bccfa5f

Please sign in to comment.