Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions cmd/zb/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2025 The zb Authors
// SPDX-License-Identifier: MIT

package main

import (
"cmp"
"context"
"crypto/ed25519"
"fmt"
"io"
"os"

jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/spf13/cobra"
"zb.256lights.llc/pkg/internal/backend"
"zb.256lights.llc/pkg/zbstore"
)

type privateKeyFile struct {
Format zbstore.RealizationSignatureFormat `json:"format"`
Key []byte `json:"key,format:base64"`
}

func (f *privateKeyFile) appendToKeyring(dst *backend.Keyring) error {
switch f.Format {
case zbstore.Ed25519SignatureFormat:
if got, want := len(f.Key), ed25519.SeedSize; got != want {
return fmt.Errorf("key is wrong size (decoded is %d instead of %d bytes)", got, want)
}
dst.Ed25519 = append(dst.Ed25519, ed25519.NewKeyFromSeed(f.Key))
default:
return fmt.Errorf("unknown format %q", f.Format)
}
return nil
}

func readKeyringFromFiles(files []string) (*backend.Keyring, error) {
result := new(backend.Keyring)
for _, path := range files {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var parsed privateKeyFile
if err := jsonv2.Unmarshal(data, &parsed); err != nil {
return nil, fmt.Errorf("read %s: %v", path, err)
}
if err := parsed.appendToKeyring(result); err != nil {
return nil, fmt.Errorf("read %s: %v", path, err)
}
}
return result, nil
}

func newKeyCommand() *cobra.Command {
c := &cobra.Command{
Use: "key COMMAND",
Short: "operate on signing key files",
DisableFlagsInUseLine: true,
SilenceErrors: true,
SilenceUsage: true,
}
c.AddCommand(
newGenerateKeyCommand(),
newShowPublicKeyCommand(),
)
return c
}

func newGenerateKeyCommand() *cobra.Command {
c := &cobra.Command{
Use: "generate [-o PATH]",
Short: "generate a new signing key",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
SilenceErrors: true,
SilenceUsage: true,
}
outputPath := c.Flags().StringP("output", "o", "", "`file` to write to (default is stdout)")
c.RunE = func(cmd *cobra.Command, args []string) error {
outputFile := os.Stdout
if *outputPath != "" {
var err error
outputFile, err = os.Create(*outputPath)
if err != nil {
return err
}
}
err1 := runGenerateKey(cmd.Context(), outputFile)
var err2 error
if *outputPath != "" {
err2 = outputFile.Close()
}
return cmp.Or(err1, err2)
}
return c
}

func runGenerateKey(ctx context.Context, dst io.Writer) error {
_, newKey, err := ed25519.GenerateKey(nil)
if err != nil {
return err
}
keyFile := &privateKeyFile{
Format: zbstore.Ed25519SignatureFormat,
Key: newKey.Seed(),
}
keyFileData, err := jsonv2.Marshal(keyFile, jsontext.Multiline(true))
if err != nil {
return err
}
keyFileData = append(keyFileData, '\n')
_, err = dst.Write(keyFileData)
return err
}

func newShowPublicKeyCommand() *cobra.Command {
c := &cobra.Command{
Use: "show-public [PATH [...]]",
Short: "print public key of signing keys",
DisableFlagsInUseLine: true,
Args: cobra.ArbitraryArgs,
SilenceErrors: true,
SilenceUsage: true,
}
c.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if len(args) == 0 {
return runShowPublicKey(ctx, os.Stdout, os.Stdin)
}
for _, path := range args {
f, err := os.Open(path)
if err != nil {
return err
}
err = runShowPublicKey(ctx, os.Stdout, f)
f.Close()
if err != nil {
return err
}
}
return nil
}
return c
}

func runShowPublicKey(ctx context.Context, dst io.Writer, src io.Reader) error {
keyFile := new(privateKeyFile)
if err := jsonv2.UnmarshalRead(src, keyFile, jsonv2.RejectUnknownMembers(false)); err != nil {
return err
}
k := new(backend.Keyring)
if err := keyFile.appendToKeyring(k); err != nil {
return err
}
var result struct {
Format zbstore.RealizationSignatureFormat `json:"format"`
PublicKey []byte `json:"publicKey,format:base64"`
}
switch {
case len(k.Ed25519) > 0:
result.Format = zbstore.Ed25519SignatureFormat
result.PublicKey = k.Ed25519[0].Public().(ed25519.PublicKey)
default:
return nil
}
data, err := jsonv2.Marshal(result, jsontext.Multiline(true))
if err != nil {
return err
}
data = append(data, '\n')
_, err = dst.Write(data)
return err
}
1 change: 1 addition & 0 deletions cmd/zb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func main() {
newBuildCommand(g),
newDerivationCommand(g),
newEvalCommand(g),
newKeyCommand(),
newNARCommand(),
newServeCommand(g),
newStoreCommand(g),
Expand Down
7 changes: 7 additions & 0 deletions cmd/zb/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type serveOptions struct {
buildDir string
buildUsersGroup string
logDir string
keyFiles []string
sandbox bool
sandboxPaths map[string]backend.SandboxPath
allowKeepFailed bool
Expand Down Expand Up @@ -82,6 +83,7 @@ func newServeCommand(g *globalConfig) *cobra.Command {
c.Flags().StringVar(&opts.buildDir, "build-root", os.TempDir(), "`dir`ectory to store temporary build artifacts")
c.Flags().StringVar(&opts.logDir, "log-directory", filepath.Join(filepath.Dir(string(zbstore.DefaultDirectory())), "var", "log", "zb"), "`dir`ectory to store builder logs in")
c.Flags().StringVar(&opts.buildUsersGroup, "build-users-group", opts.buildUsersGroup, "name of Unix `group` of users to run builds as")
c.Flags().StringArrayVar(&opts.keyFiles, "signing-key", nil, "key `file` for signing realizations (can be passed multiple times)")
c.Flags().BoolVar(&opts.sandbox, "sandbox", opts.sandbox, "run builders in a restricted environment")
sandboxPaths := make(map[string]string)
c.Flags().Var(pathMapFlag(sandboxPaths), "sandbox-path", "`path` to allow in sandbox (can be passed multiple times)")
Expand Down Expand Up @@ -113,6 +115,10 @@ func runServe(ctx context.Context, g *globalConfig, opts *serveOptions) error {
}
return fmt.Errorf("sandboxing requested but unable to use (are you running with admin privileges?)")
}
keyring, err := readKeyringFromFiles(opts.keyFiles)
if err != nil {
return err
}
storeDirGroupID, buildUsers, err := buildUsersForGroup(ctx, opts.buildUsersGroup)
if err != nil {
return err
Expand Down Expand Up @@ -203,6 +209,7 @@ func runServe(ctx context.Context, g *globalConfig, opts *serveOptions) error {
AllowKeepFailed: opts.allowKeepFailed,
CoresPerBuild: opts.coresPerBuild,
BuildLogRetention: opts.buildLogRetention,
Keyring: keyring,
})
defer func() {
if err := backendServer.Close(); err != nil {
Expand Down
52 changes: 52 additions & 0 deletions internal/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package backend

import (
"context"
"crypto/ed25519"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -83,6 +84,10 @@ type Options struct {
// BuildLogRetention is the length of time to retain build logs.
// If non-positive, then build logs will be not be automatically deleted.
BuildLogRetention time.Duration

// Keyring is a set of keys that will be used to sign realizations
// that this server realizes.
Keyring *Keyring
}

// A SandboxPath is the set of options for SandboxPaths in [Options].
Expand Down Expand Up @@ -128,6 +133,7 @@ type Server struct {
db *sqlitemigration.Pool
allowKeepFailed bool
buildContext func(context.Context, string) context.Context
keyring *Keyring

sandbox bool
sandboxPaths map[string]SandboxPath
Expand Down Expand Up @@ -169,6 +175,7 @@ func NewServer(dir zbstore.Directory, dbPath string, opts *Options) *Server {
users: users,
activeBuilds: make(map[uuid.UUID]context.CancelFunc),
buildContext: opts.BuildContext,
keyring: opts.Keyring.Clone(),

db: sqlitemigration.NewPool(dbPath, loadSchema(), sqlitemigration.Options{
Flags: sqlite.OpenCreate | sqlite.OpenReadWrite,
Expand Down Expand Up @@ -894,6 +901,51 @@ func (s *Server) optimizeDatabase(ctx context.Context) {
}
}

// A Keyring is a set of private keys to use for signing.
// Nil or the zero value is an empty set of keys.
type Keyring struct {
Ed25519 []ed25519.PrivateKey
}

// Clone returns a new keyring with contents identical to k.
// If k is nil, then Clone returns nil.
func (k *Keyring) Clone() *Keyring {
if k == nil {
return nil
}
k2 := new(Keyring)
if len(k.Ed25519) > 0 {
k2.Ed25519 = make([]ed25519.PrivateKey, len(k.Ed25519))
for i, key := range k.Ed25519 {
k2.Ed25519[i] = slices.Clone(key)
}
}
return k2
}

// Sign creates signatures for the realization using all the private keys in the keyring.
func (k *Keyring) Sign(ref zbstore.RealizationOutputReference, r *zbstore.Realization) ([]*zbstore.RealizationSignature, error) {
n := 0
if k != nil {
n = len(k.Ed25519)
}
if n == 0 {
return nil, nil
}

result := make([]*zbstore.RealizationSignature, 0, n)
var returnedError error
for _, key := range k.Ed25519 {
sig, err := zbstore.SignRealizationWithEd25519(ref, r, key)
if err != nil {
returnedError = errors.Join(returnedError, err)
continue
}
result = append(result, sig)
}
return result, returnedError
}

func parseBuildID(id string) (_ uuid.UUID, ok bool) {
u, err := uuid.Parse(id)
if err != nil || id != u.String() {
Expand Down
Loading