From ebb5332b8b8a1e84c2adbee289d97e6b464a38bb Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Wed, 25 Dec 2024 09:18:48 -0600 Subject: [PATCH] Refactor how we deal with configuration. In particular: * Move veil's, veil-verify's, and veil-proxy's configuration to the `config` package. * Add `validate.Object`, which validates a given object and turns validation errors into joined errors. * Unify the way we handle configuration across our three command line tools. --- cmd/veil-proxy/main.go | 25 ++-- cmd/veil-verify/attestation.go | 7 +- cmd/veil-verify/docker.go | 11 +- cmd/veil-verify/main.go | 44 ++----- cmd/veil/main.go | 22 ++-- cmd/veil/main_test.go | 2 +- internal/config/config.go | 113 ++---------------- internal/config/veil.go | 104 ++++++++++++++++ internal/config/veil_proxy.go | 21 ++++ internal/config/veil_proxy_test.go | 33 +++++ .../config/{config_test.go => veil_test.go} | 21 ++-- internal/config/veil_verify.go | 49 ++++++++ internal/config/veil_verify_test.go | 47 ++++++++ internal/service/handle/handlers.go | 4 +- internal/service/routes.go | 22 ++-- internal/service/service.go | 34 +++--- internal/types/validate/validate.go | 44 +++++++ .../validate/validate_test.go} | 2 +- internal/util/validator.go | 27 ----- 19 files changed, 390 insertions(+), 242 deletions(-) create mode 100644 internal/config/veil.go create mode 100644 internal/config/veil_proxy.go create mode 100644 internal/config/veil_proxy_test.go rename internal/config/{config_test.go => veil_test.go} (60%) create mode 100644 internal/config/veil_verify.go create mode 100644 internal/config/veil_verify_test.go create mode 100644 internal/types/validate/validate.go rename internal/{util/validator_test.go => types/validate/validate_test.go} (97%) delete mode 100644 internal/util/validator.go diff --git a/cmd/veil-proxy/main.go b/cmd/veil-proxy/main.go index cd9c34e..dcdc94e 100644 --- a/cmd/veil-proxy/main.go +++ b/cmd/veil-proxy/main.go @@ -12,19 +12,16 @@ import ( "os/signal" "sync" + "github.com/Amnesic-Systems/veil/internal/config" "github.com/Amnesic-Systems/veil/internal/errs" "github.com/Amnesic-Systems/veil/internal/net/nat" "github.com/Amnesic-Systems/veil/internal/net/proxy" "github.com/Amnesic-Systems/veil/internal/net/tun" + "github.com/Amnesic-Systems/veil/internal/types/validate" "github.com/mdlayher/vsock" ) -type config struct { - profile bool - port int -} - -func parseFlags(out io.Writer, args []string) (_ *config, err error) { +func parseFlags(out io.Writer, args []string) (_ *config.VeilProxy, err error) { defer errs.Wrap(&err, "failed to parse flags") fs := flag.NewFlagSet("veil-proxy", flag.ContinueOnError) @@ -35,7 +32,7 @@ func parseFlags(out io.Writer, args []string) (_ *config, err error) { false, "Enable profiling.", ) - port := fs.Int( + port := fs.Uint( "port", 1024, "VSOCK port that the enclave connects to.", @@ -44,10 +41,12 @@ func parseFlags(out io.Writer, args []string) (_ *config, err error) { return nil, err } - return &config{ - profile: *profile, - port: *port, - }, nil + // Build and validate the configuration. + cfg := &config.VeilProxy{ + Profile: *profile, + Port: uint32(*port), + } + return cfg, validate.Object(cfg) } func listenVSOCK(port uint32) (_ net.Listener, err error) { @@ -118,7 +117,7 @@ func run(ctx context.Context, out io.Writer, args []string) (origErr error) { // Create a VSOCK listener that listens for incoming connections from the // enclave. - ln, err := listenVSOCK(uint32(cfg.port)) + ln, err := listenVSOCK(cfg.Port) if err != nil { return err } @@ -127,7 +126,7 @@ func run(ctx context.Context, out io.Writer, args []string) (origErr error) { }() // If desired, set up a Web server for the profiler. - if cfg.profile { + if cfg.Profile { go func() { const hostPort = "localhost:6060" log.Printf("Starting profiling Web server at: http://%s", hostPort) diff --git a/cmd/veil-verify/attestation.go b/cmd/veil-verify/attestation.go index 14bb754..58fcd67 100644 --- a/cmd/veil-verify/attestation.go +++ b/cmd/veil-verify/attestation.go @@ -16,6 +16,7 @@ import ( "github.com/fatih/color" + "github.com/Amnesic-Systems/veil/internal/config" "github.com/Amnesic-Systems/veil/internal/enclave" "github.com/Amnesic-Systems/veil/internal/enclave/nitro" "github.com/Amnesic-Systems/veil/internal/enclave/noop" @@ -33,7 +34,7 @@ var ( func attestEnclave( ctx context.Context, - cfg *config, + cfg *config.VeilVerify, pcrs enclave.PCR, ) (err error) { defer errs.WrapErr(&err, errFailedToAttest) @@ -44,7 +45,7 @@ func attestEnclave( return err } - req, err := buildReq(ctx, cfg.addr, nonce) + req, err := buildReq(ctx, cfg.Addr, nonce) if err != nil { return err } @@ -76,7 +77,7 @@ func attestEnclave( // talking to an enclave. The nonce provides assurance that we are talking // to an alive enclave (instead of a replayed attestation document). var attester enclave.Attester = nitro.NewAttester() - if cfg.testing { + if cfg.Testing { attester = noop.NewAttester() } doc, err := attester.Verify(&rawDoc, nonce) diff --git a/cmd/veil-verify/docker.go b/cmd/veil-verify/docker.go index 0e2870b..b25e9d3 100644 --- a/cmd/veil-verify/docker.go +++ b/cmd/veil-verify/docker.go @@ -13,6 +13,7 @@ import ( "path" "github.com/Amnesic-Systems/veil/internal/addr" + "github.com/Amnesic-Systems/veil/internal/config" "github.com/Amnesic-Systems/veil/internal/enclave" "github.com/Amnesic-Systems/veil/internal/errs" "github.com/docker/docker/api/types" @@ -57,7 +58,7 @@ func removeContainer(cli *client.Client, id string) { func buildEnclaveImage( ctx context.Context, cli *client.Client, - cfg *config, + cfg *config.VeilVerify, out io.Writer, ) (err error) { defer errs.Wrap(&err, "failed to build enclave image") @@ -80,7 +81,7 @@ func buildEnclaveImage( Tty: true, Image: builderImage, Cmd: []string{ - "--dockerfile", cfg.dockerfile, + "--dockerfile", cfg.Dockerfile, "--reproducible", "--no-push", "--log-format", "text", @@ -96,7 +97,7 @@ func buildEnclaveImage( Mounts: []mount.Mount{ { Type: mount.TypeBind, - Source: cfg.dir, + Source: cfg.Dir, Target: "/workspace", }, }, @@ -166,13 +167,13 @@ func getContainerExitCode( func loadEnclaveImage( ctx context.Context, cli *client.Client, - cfg *config, + cfg *config.VeilVerify, verbose io.Writer, ) (err error) { defer errs.Wrap(&err, "failed to load enclave image") // Read the tar image. - file, err := os.Open(path.Join(cfg.dir, enclaveTarImage)) + file, err := os.Open(path.Join(cfg.Dir, enclaveTarImage)) if err != nil { return err } diff --git a/cmd/veil-verify/main.go b/cmd/veil-verify/main.go index 341c168..8854005 100644 --- a/cmd/veil-verify/main.go +++ b/cmd/veil-verify/main.go @@ -4,29 +4,21 @@ import ( "context" "errors" "flag" - "fmt" "io" "log" "os" "os/signal" - "path" "github.com/docker/docker/client" + "github.com/Amnesic-Systems/veil/internal/config" "github.com/Amnesic-Systems/veil/internal/errs" + "github.com/Amnesic-Systems/veil/internal/types/validate" ) var errFailedToParse = errors.New("failed to parse flags") -type config struct { - addr string - dir string - dockerfile string - verbose bool - testing bool -} - -func parseFlags(out io.Writer, args []string) (_ *config, err error) { +func parseFlags(out io.Writer, args []string) (_ *config.VeilVerify, err error) { defer errs.WrapErr(&err, errFailedToParse) fs := flag.NewFlagSet("veil-verify", flag.ContinueOnError) @@ -61,27 +53,15 @@ func parseFlags(out io.Writer, args []string) (_ *config, err error) { return nil, err } - // Ensure that required arguments are set. - if *addr == "" { - return nil, errors.New("flag -addr must be provided") - } - if *dir == "" { - return nil, errors.New("flag -dir must be provided") - } - - // Make sure that the Dockerfile relative to the given directory exists. - p := path.Join(*dir, *dockerfile) - if _, err := os.Stat(p); err != nil { - return nil, fmt.Errorf("given Dockerfile %q does not exist", p) + // Build and validate the configuration. + cfg := &config.VeilVerify{ + Addr: *addr, + Dir: *dir, + Dockerfile: *dockerfile, + Testing: *testing, + Verbose: *verbose, } - - return &config{ - addr: *addr, - dir: *dir, - dockerfile: *dockerfile, - testing: *testing, - verbose: *verbose, - }, nil + return cfg, validate.Object(cfg) } func run(ctx context.Context, out io.Writer, args []string) error { @@ -95,7 +75,7 @@ func run(ctx context.Context, out io.Writer, args []string) error { // By default, we discard Docker's logs but we print them in verbose mode. writer := io.Discard - if cfg.verbose { + if cfg.Verbose { writer = log.Writer() } diff --git a/cmd/veil/main.go b/cmd/veil/main.go index a3be059..beaf170 100644 --- a/cmd/veil/main.go +++ b/cmd/veil/main.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "errors" "flag" "fmt" "io" @@ -23,6 +22,7 @@ import ( "github.com/Amnesic-Systems/veil/internal/httpx" "github.com/Amnesic-Systems/veil/internal/service" "github.com/Amnesic-Systems/veil/internal/tunnel" + "github.com/Amnesic-Systems/veil/internal/types/validate" ) const ( @@ -30,7 +30,7 @@ const ( defaultIntPort = 8080 ) -func parseFlags(out io.Writer, args []string) (*config.Config, error) { +func parseFlags(out io.Writer, args []string) (*config.Veil, error) { fs := flag.NewFlagSet("veil", flag.ContinueOnError) fs.SetOutput(out) @@ -103,8 +103,8 @@ func parseFlags(out io.Writer, args []string) (*config.Config, error) { } } - // Build and validate the config. - return &config.Config{ + // Build and validate the configuration. + cfg := &config.Veil{ AppCmd: *appCmd, AppWebSrv: u, Debug: *debug, @@ -116,7 +116,8 @@ func parseFlags(out io.Writer, args []string) (*config.Config, error) { SilenceApp: *silenceApp, Testing: *testing, WaitForApp: *waitForApp, - }, nil + } + return cfg, validate.Object(cfg) } func run(ctx context.Context, out io.Writer, args []string) (err error) { @@ -135,15 +136,6 @@ func run(ctx context.Context, out io.Writer, args []string) (err error) { return err } - // Validate the configuration. - if problems := cfg.Validate(ctx); len(problems) > 0 { - err := errors.New("invalid configuration") - for field, problem := range problems { - err = errors.Join(err, fmt.Errorf("field %s: %v", field, problem)) - } - return err - } - // Run the application command, if specified. if cfg.AppCmd != "" { go func() { @@ -167,7 +159,7 @@ func run(ctx context.Context, out io.Writer, args []string) (err error) { return nil } -func eventuallyRunAppCmd(ctx context.Context, cfg *config.Config, cmd string) (err error) { +func eventuallyRunAppCmd(ctx context.Context, cfg *config.Veil, cmd string) (err error) { defer errs.Wrap(&err, "failed to run app command") // Wait for the internal service to be ready. diff --git a/cmd/veil/main_test.go b/cmd/veil/main_test.go index b18039e..81165f6 100644 --- a/cmd/veil/main_test.go +++ b/cmd/veil/main_test.go @@ -467,7 +467,7 @@ func TestRunApp(t *testing.T) { // Decode the configuration file and verify that the application // command is identical to what we just ran. - var cfg config.Config + var cfg config.Veil require.NoError(t, json.Unmarshal(content, &cfg)) require.Equal(t, c.command, cfg.AppCmd) }) diff --git a/internal/config/config.go b/internal/config/config.go index 33d7991..fbd147e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,109 +1,14 @@ +// Package config contains the structs representing the configuration of our +// command line tools. package config import ( - "context" - "net/url" - - "github.com/Amnesic-Systems/veil/internal/util" + "github.com/Amnesic-Systems/veil/internal/types/validate" ) -var _ = util.Validator(&Config{}) - -// Config represents the configuration of our enclave service. -type Config struct { - // AppCmd can be set to the command that starts the enclave application. - // For example: - // - // nc -l -p 1234 - // - // Veil starts the given application after its internal Web server is - // running, and subsequently waits for the application to finish. When the - // application stops or crashes, veil terminates. - AppCmd string - - // AppWebSrv should be set to the enclave-internal Web server of the - // enclave application, e.g., "http://127.0.0.1:8080". Nitriding acts as a - // TLS-terminating reverse proxy and forwards incoming HTTP requests to - // this Web server. Note that this configuration option is only necessary - // if the enclave application exposes an HTTP server. Non-HTTP enclave - // applications can ignore this. - AppWebSrv *url.URL - - // Debug can be set to true to see debug messages, i.e., if you are - // starting the enclave in debug mode by running: - // - // nitro-cli run-enclave --debug-mode .... - // - // Do not set this to true in production because printing debug messages - // for each HTTP request slows down the enclave application, and you are - // not able to see debug messages anyway unless you start the enclave using - // nitro-cli's "--debug-mode" flag. - Debug bool - - // EnclaveCodeURI contains the URI of the software repository that's running - // inside the enclave, e.g., "https://github.com/foo/bar". The URL is shown - // on the enclave's index page, as part of instructions on how to do remote - // attestation. - EnclaveCodeURI string - - // ExtPort contains the TCP port that the public Web server should - // listen on, e.g. 443. This port is not *directly* reachable by the - // Internet but the EC2 host's proxy *does* forward Internet traffic to - // this port. This field is required. - ExtPort int - - // FQDN contains the fully qualified domain name that's set in the HTTPS - // certificate of the enclave's Web server, e.g. "example.com". This field - // is required. - FQDN string - - // IntPort contains the TCP port that the internal Web server should listen - // on, e.g., 8080. This port is only reachable from within the enclave and - // is only used by the enclave application. This field is required. - IntPort int - - // Resolver contains the IP address of the DNS resolver that the enclave - // should use, e.g., 1.1.1.1. - Resolver string - - // SilenceApp can be set to discard the application's stdout and stderr if - // -app-cmd is used. - SilenceApp bool - - // Testing facilitates local testing by disabling safety checks that we - // would normally run on the enclave and by using the noop attester instead - // of the real attester. - Testing bool - - // WaitForApp instructs nitriding to wait for the application's signal - // before launching the Internet-facing Web server. Set this flag if your - // application takes a while to bootstrap and you don't want to risk - // inconsistent state when syncing, or unexpected attestation documents. - // If set, your application must make the following request when ready: - // - // GET http://127.0.0.1:{IntPort}/enclave/ready - WaitForApp bool -} - -func isValidPort(port int) bool { - return port > 0 && port < 65536 -} - -func (c *Config) Validate(_ context.Context) map[string]string { - problems := make(map[string]string) - - // Check required fields. - if !isValidPort(c.ExtPort) { - problems["-ext-port"] = "must be a valid port number" - } - if !isValidPort(c.IntPort) { - problems["-int-port"] = "must be a valid port number" - } - - // Check invalid field combinations. - if c.SilenceApp && c.AppCmd == "" { - problems["-silence-app"] = "requires -app-cmd to be set" - } - - return problems -} +// Check that all configuration types satisfy the `Validator` interface. +var ( + _ = validate.Validator(&Veil{}) + _ = validate.Validator(&VeilProxy{}) + _ = validate.Validator(&VeilVerify{}) +) diff --git a/internal/config/veil.go b/internal/config/veil.go new file mode 100644 index 0000000..e55ec94 --- /dev/null +++ b/internal/config/veil.go @@ -0,0 +1,104 @@ +package config + +import ( + "net/url" +) + +// Veil represents veil's configuration. +type Veil struct { + // AppCmd can be set to the command that starts the enclave application. + // For example: + // + // nc -l -p 1234 + // + // Veil starts the given application after its internal Web server is + // running, and subsequently waits for the application to finish. When the + // application stops or crashes, veil terminates. + AppCmd string + + // AppWebSrv should be set to the enclave-internal Web server of the + // enclave application, e.g., "http://127.0.0.1:8080". Nitriding acts as a + // TLS-terminating reverse proxy and forwards incoming HTTP requests to + // this Web server. Note that this configuration option is only necessary + // if the enclave application exposes an HTTP server. Non-HTTP enclave + // applications can ignore this. + AppWebSrv *url.URL + + // Debug can be set to true to see debug messages, i.e., if you are + // starting the enclave in debug mode by running: + // + // nitro-cli run-enclave --debug-mode .... + // + // Do not set this to true in production because printing debug messages + // for each HTTP request slows down the enclave application, and you are + // not able to see debug messages anyway unless you start the enclave using + // nitro-cli's "--debug-mode" flag. + Debug bool + + // EnclaveCodeURI contains the URI of the software repository that's running + // inside the enclave, e.g., "https://github.com/foo/bar". The URL is shown + // on the enclave's index page, as part of instructions on how to do remote + // attestation. + EnclaveCodeURI string + + // ExtPort contains the TCP port that the public Web server should + // listen on, e.g. 443. This port is not *directly* reachable by the + // Internet but the EC2 host's proxy *does* forward Internet traffic to + // this port. This field is required. + ExtPort int + + // FQDN contains the fully qualified domain name that's set in the HTTPS + // certificate of the enclave's Web server, e.g. "example.com". This field + // is required. + FQDN string + + // IntPort contains the TCP port that the internal Web server should listen + // on, e.g., 8080. This port is only reachable from within the enclave and + // is only used by the enclave application. This field is required. + IntPort int + + // Resolver contains the IP address of the DNS resolver that the enclave + // should use, e.g., 1.1.1.1. + Resolver string + + // SilenceApp can be set to discard the application's stdout and stderr if + // -app-cmd is used. + SilenceApp bool + + // Testing facilitates local testing by disabling safety checks that we + // would normally run on the enclave and by using the noop attester instead + // of the real attester. + Testing bool + + // WaitForApp instructs nitriding to wait for the application's signal + // before launching the Internet-facing Web server. Set this flag if your + // application takes a while to bootstrap and you don't want to risk + // inconsistent state when syncing, or unexpected attestation documents. + // If set, your application must make the following request when ready: + // + // GET http://127.0.0.1:{IntPort}/enclave/ready + WaitForApp bool +} + +func isValidPort(port int) bool { + return port > 0 && port < 65536 +} + +func (c *Veil) Validate() map[string]string { + problems := make(map[string]string) + + // Check required fields. + if !isValidPort(c.ExtPort) { + problems["-ext-port"] = "must be a valid port number" + } + if !isValidPort(c.IntPort) { + problems["-int-port"] = "must be a valid port number" + } + + // Check invalid field combinations. + if c.SilenceApp && c.AppCmd == "" { + problems["-silence-app"] = "requires -app-cmd to be set" + } + + return problems +} diff --git a/internal/config/veil_proxy.go b/internal/config/veil_proxy.go new file mode 100644 index 0000000..f1b5d9b --- /dev/null +++ b/internal/config/veil_proxy.go @@ -0,0 +1,21 @@ +package config + +// VeilProxy represents veil-proxy's configuration. +type VeilProxy struct { + // Profile can be set to true to enable profiling. + Profile bool + + // Port determines the VSOCK port that veil-proxy will be listening on for + // incoming connections from the enclave. + Port uint32 +} + +func (c *VeilProxy) Validate() map[string]string { + problems := make(map[string]string) + + if c.Port == 0 { + problems["-port"] = "port must not be 0" + } + + return problems +} diff --git a/internal/config/veil_proxy_test.go b/internal/config/veil_proxy_test.go new file mode 100644 index 0000000..78c72ab --- /dev/null +++ b/internal/config/veil_proxy_test.go @@ -0,0 +1,33 @@ +package config + +import ( + "testing" + + "github.com/Amnesic-Systems/veil/internal/types/validate" + "github.com/stretchr/testify/require" +) + +func TestVeilProxyConfig(t *testing.T) { + cases := []struct { + name string + cfg *VeilProxy + wantErrs int + }{ + { + name: "invalid port", + cfg: &VeilProxy{Port: 0}, + wantErrs: 1, + }, + { + name: "valid port", + cfg: &VeilProxy{Port: 1}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + errs := c.cfg.Validate() + require.Equal(t, c.wantErrs, len(errs), validate.SprintErrs(errs)) + }) + } +} diff --git a/internal/config/config_test.go b/internal/config/veil_test.go similarity index 60% rename from internal/config/config_test.go rename to internal/config/veil_test.go index 2b3001b..eefa8be 100644 --- a/internal/config/config_test.go +++ b/internal/config/veil_test.go @@ -1,35 +1,34 @@ package config import ( - "context" "testing" - "github.com/Amnesic-Systems/veil/internal/util" + "github.com/Amnesic-Systems/veil/internal/types/validate" "github.com/stretchr/testify/require" ) -func TestConfig(t *testing.T) { +func TestVeilConfig(t *testing.T) { cases := []struct { name string - cfg *Config + cfg *Veil wantErrs int }{ { name: "valid config", - cfg: &Config{ExtPort: 8443, IntPort: 8080}, + cfg: &Veil{ExtPort: 8443, IntPort: 8080}, }, { name: "still valid config", - cfg: &Config{ExtPort: 1, IntPort: 65535}, + cfg: &Veil{ExtPort: 1, IntPort: 65535}, }, { name: "invalid ports", - cfg: &Config{ExtPort: 0, IntPort: 65536}, + cfg: &Veil{ExtPort: 0, IntPort: 65536}, wantErrs: 2, }, { name: "invalid flag combination", - cfg: &Config{ + cfg: &Veil{ SilenceApp: true, ExtPort: 8443, IntPort: 8080, @@ -38,7 +37,7 @@ func TestConfig(t *testing.T) { }, { name: "valid flag combination", - cfg: &Config{ + cfg: &Veil{ SilenceApp: true, AppCmd: "echo", ExtPort: 8443, @@ -49,8 +48,8 @@ func TestConfig(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - errs := c.cfg.Validate(context.Background()) - require.Equal(t, c.wantErrs, len(errs), util.SprintErrs(errs)) + errs := c.cfg.Validate() + require.Equal(t, c.wantErrs, len(errs), validate.SprintErrs(errs)) }) } } diff --git a/internal/config/veil_verify.go b/internal/config/veil_verify.go new file mode 100644 index 0000000..94a2ca3 --- /dev/null +++ b/internal/config/veil_verify.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "os" + "path" +) + +// VeilVerify represents veil-verify's configuration. +type VeilVerify struct { + // Addr contains the enclave's address, e.g.: + // https://enclave.example.com + Addr string + + // Dir contains the (relative or absolute) directory of the software + // repository containing the enclave application. + Dir string + + // Dockerfile contains the path (relative to `Dir`) of the Dockerfile that's + // used to build the enclave application. + Dockerfile string + + // Verbose prints extra information if set to true. + Verbose bool + + // Testing facilitates local testing by disabling safety checks that we + // would normally run. + Testing bool +} + +func (c *VeilVerify) Validate() map[string]string { + problems := make(map[string]string) + + // Ensure that required arguments are set. + if c.Addr == "" { + problems["-addr"] = "argument is required" + } + if c.Dir == "" { + problems["-dir"] = "argument is required" + } + + // Make sure that the Dockerfile relative to the given directory exists. + p := path.Join(c.Dir, c.Dockerfile) + if _, err := os.Stat(p); err != nil { + problems["-dockerfile"] = fmt.Sprintf("given Dockerfile %q does not exist", p) + } + + return problems +} diff --git a/internal/config/veil_verify_test.go b/internal/config/veil_verify_test.go new file mode 100644 index 0000000..36443a3 --- /dev/null +++ b/internal/config/veil_verify_test.go @@ -0,0 +1,47 @@ +package config + +import ( + "testing" + + "github.com/Amnesic-Systems/veil/internal/types/validate" + "github.com/stretchr/testify/require" +) + +func TestVeilVerifyConfig(t *testing.T) { + cases := []struct { + name string + cfg *VeilVerify + wantErrs int + }{ + { + name: "missing addr, dir, and dockerfile", + cfg: &VeilVerify{}, + wantErrs: 3, + }, + { + name: "missing addr and dockerfile", + cfg: &VeilVerify{Dir: "foo"}, + wantErrs: 2, + }, + { + name: "missing dir and dockerfile", + cfg: &VeilVerify{Addr: "https://example.com"}, + wantErrs: 2, + }, + { + name: "missing dockerfile", + cfg: &VeilVerify{ + Dir: "foo", + Addr: "https://example.com", + }, + wantErrs: 1, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + errs := c.cfg.Validate() + require.Equal(t, c.wantErrs, len(errs), validate.SprintErrs(errs)) + }) + } +} diff --git a/internal/service/handle/handlers.go b/internal/service/handle/handlers.go index 55d2f17..d44d20c 100644 --- a/internal/service/handle/handlers.go +++ b/internal/service/handle/handlers.go @@ -18,7 +18,7 @@ import ( // Index informs the visitor that this host runs inside an enclave. This is // useful for testing. -func Index(cfg *config.Config) http.HandlerFunc { +func Index(cfg *config.Veil) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { page := "This host runs inside an AWS Nitro Enclave." if cfg.EnclaveCodeURI != "" { @@ -32,7 +32,7 @@ func Index(cfg *config.Config) http.HandlerFunc { // Config returns the enclave's configuration. func Config( builder *attestation.Builder, - cfg *config.Config, + cfg *config.Veil, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // If the client provided a nonce, we will add an attestation document diff --git a/internal/service/routes.go b/internal/service/routes.go index 0ffae3f..f0bc7de 100644 --- a/internal/service/routes.go +++ b/internal/service/routes.go @@ -20,39 +20,39 @@ const ( PathHash = "/veil/hash" ) -func setupMiddlewares(r *chi.Mux, config *config.Config) { - if config.Debug { +func setupMiddlewares(r *chi.Mux, cfg *config.Veil) { + if cfg.Debug { r.Use(middleware.Logger) } } func addExternalPublicRoutes( r *chi.Mux, - config *config.Config, + cfg *config.Veil, builder *attestation.Builder, ) { - setupMiddlewares(r, config) + setupMiddlewares(r, cfg) - r.Get(PathIndex, handle.Index(config)) - r.Get(PathConfig, handle.Config(builder, config)) + r.Get(PathIndex, handle.Index(cfg)) + r.Get(PathConfig, handle.Config(builder, cfg)) r.Get(PathAttestation, handle.Attestation(builder)) // Set up reverse proxy for the application' Web server. - if config.AppWebSrv != nil { - reverseProxy := httputil.NewSingleHostReverseProxy(config.AppWebSrv) + if cfg.AppWebSrv != nil { + reverseProxy := httputil.NewSingleHostReverseProxy(cfg.AppWebSrv) r.Handle("/*", reverseProxy) } } func addInternalRoutes( r *chi.Mux, - config *config.Config, + cfg *config.Veil, hashes *attestation.Hashes, appReady chan struct{}, ) { - setupMiddlewares(r, config) + setupMiddlewares(r, cfg) - if config.WaitForApp { + if cfg.WaitForApp { r.Get(PathReady, handle.Ready(appReady)) } else { close(appReady) diff --git a/internal/service/service.go b/internal/service/service.go index 93cc765..5568842 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -25,22 +25,22 @@ import ( func Run( ctx context.Context, - config *config.Config, + cfg *config.Veil, attester enclave.Attester, mechanism tunnel.Mechanism, ) { var appReady = make(chan struct{}) // Run safety checks and setup tasks before starting. - if err := checkSystemSafety(config); err != nil { + if err := checkSystemSafety(cfg); err != nil { log.Fatalf("Failed safety check: %v", err) } - if err := setupSystem(config); err != nil { + if err := setupSystem(cfg); err != nil { log.Fatalf("Failed to set up system: %v", err) } // Create a TLS certificate for the external Web server. - cert, key, err := httpx.CreateCertificate(config.FQDN) + cert, key, err := httpx.CreateCertificate(cfg.FQDN) if err != nil { log.Fatalf("Failed to create certificate: %v", err) } @@ -50,12 +50,12 @@ func Run( hashes.SetTLSHash(addr.Of(sha256.Sum256(cert))) // Initialize Web servers. - intSrv := newIntSrv(config, hashes, appReady) + intSrv := newIntSrv(cfg, hashes, appReady) builder := attestation.NewBuilder( attester, attestation.WithHashes(hashes), ) - extSrv := newExtSrv(config, builder) + extSrv := newExtSrv(cfg, builder) extSrv.TLSConfig = &tls.Config{ Certificates: []tls.Certificate{ must.Get(tls.X509KeyPair(cert, key)), @@ -73,9 +73,9 @@ func Run( log.Println("Exiting.") } -func checkSystemSafety(config *config.Config) (err error) { +func checkSystemSafety(cfg *config.Veil) (err error) { defer errs.Wrap(&err, "failed system safety check") - if config.Testing { + if cfg.Testing { return nil } @@ -85,15 +85,15 @@ func checkSystemSafety(config *config.Config) (err error) { return nil } -func setupSystem(config *config.Config) (err error) { +func setupSystem(cfg *config.Veil) (err error) { defer errs.Wrap(&err, "failed to set up system") // GitHub Actions won't allow us to set up the lo interface. - if config.Testing { + if cfg.Testing { return nil } - if err := system.SetResolver(config.Resolver); err != nil { + if err := system.SetResolver(cfg.Resolver); err != nil { return err } @@ -146,28 +146,28 @@ func startAllWebSrvs( } func newIntSrv( - config *config.Config, + cfg *config.Veil, hashes *attestation.Hashes, appReady chan struct{}, ) *http.Server { r := chi.NewRouter() - addInternalRoutes(r, config, hashes, appReady) + addInternalRoutes(r, cfg, hashes, appReady) return &http.Server{ - Addr: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", config.IntPort)), + Addr: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", cfg.IntPort)), Handler: http.Handler(r), } } func newExtSrv( - config *config.Config, + cfg *config.Veil, builder *attestation.Builder, ) *http.Server { r := chi.NewRouter() - addExternalPublicRoutes(r, config, builder) + addExternalPublicRoutes(r, cfg, builder) return &http.Server{ - Addr: net.JoinHostPort("0.0.0.0", fmt.Sprintf("%d", config.ExtPort)), + Addr: net.JoinHostPort("0.0.0.0", fmt.Sprintf("%d", cfg.ExtPort)), Handler: http.Handler(r), } } diff --git a/internal/types/validate/validate.go b/internal/types/validate/validate.go new file mode 100644 index 0000000..b87645d --- /dev/null +++ b/internal/types/validate/validate.go @@ -0,0 +1,44 @@ +// Package validate provides a typo and functions to validate input like command +// line configuration or client-submitted JSON objects. +package validate + +import ( + "errors" + "fmt" + "slices" +) + +func SprintErrs(errs map[string]string) string { + var s string + + // Sort the error keys. + errKeys := []string{} + for key := range errs { + errKeys = append(errKeys, key) + } + slices.Sort(errKeys) + + for _, key := range errKeys { + s += key + ": " + errs[key] + "\n" + } + + return s +} + +// Objects validates `v` and returns an error if there are validation errors. If +// there is more than one validation error, the function joins the errors using +// `errors.Join`. +func Object(v Validator) error { + var err error + if problems := v.Validate(); len(problems) > 0 { + for field, problem := range problems { + err = errors.Join(err, fmt.Errorf("field %s: %v", field, problem)) + } + return err + } + return nil +} + +type Validator interface { + Validate() map[string]string +} diff --git a/internal/util/validator_test.go b/internal/types/validate/validate_test.go similarity index 97% rename from internal/util/validator_test.go rename to internal/types/validate/validate_test.go index 75b0223..5f6e39c 100644 --- a/internal/util/validator_test.go +++ b/internal/types/validate/validate_test.go @@ -1,4 +1,4 @@ -package util +package validate import ( "testing" diff --git a/internal/util/validator.go b/internal/util/validator.go deleted file mode 100644 index 17fd892..0000000 --- a/internal/util/validator.go +++ /dev/null @@ -1,27 +0,0 @@ -package util - -import ( - "context" - "slices" -) - -func SprintErrs(errs map[string]string) string { - var s string - - // Sort the error keys. - errKeys := []string{} - for key := range errs { - errKeys = append(errKeys, key) - } - slices.Sort(errKeys) - - for _, key := range errKeys { - s += key + ": " + errs[key] + "\n" - } - - return s -} - -type Validator interface { - Validate(context.Context) map[string]string -}