diff --git a/CMakeLists.txt b/CMakeLists.txt index b6d313ea..bb1c4549 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,7 +94,7 @@ add_custom_target(cli ALL COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/build_with_version.cmake "go" "${PROJECT_VERSION}" "${CMAKE_BINARY_DIR}/marblerun" - "github.com/edgelesssys/marblerun/cli/cmd" + "github.com/edgelesssys/marblerun/cli/internal/cmd" ${TRIMPATH} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli ) diff --git a/cli/cmd/certificateChain.go b/cli/cmd/certificateChain.go deleted file mode 100644 index 4b869eb0..00000000 --- a/cli/cmd/certificateChain.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/pem" - "fmt" - "io" - "io/ioutil" - - "github.com/spf13/cobra" -) - -func newCertificateChain() *cobra.Command { - var certFilename string - - cmd := &cobra.Command{ - Use: "chain ", - Short: "Returns the certificate chain of the MarbleRun Coordinator", - Long: `Returns the certificate chain of the MarbleRun Coordinator`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[0] - return cliCertificateChain(cmd.OutOrStdout(), hostName, certFilename, eraConfig, insecureEra, acceptedTCBStatuses) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVarP(&certFilename, "output", "o", "marblerunChainCA.crt", "File to save the certificate to") - - return cmd -} - -// cliCertificateChain gets the certificate chain of the MarbleRun Coordinator. -func cliCertificateChain(out io.Writer, host, output, configFilename string, insecure bool, acceptedTCBStatuses []string) error { - certs, err := verifyCoordinator(out, host, configFilename, insecure, acceptedTCBStatuses) - if err != nil { - return err - } - - if len(certs) == 1 { - fmt.Fprintln(out, "WARNING: Only received root certificate from host.") - } - - var chain []byte - for _, cert := range certs { - chain = append(chain, pem.EncodeToMemory(cert)...) - } - - if err := ioutil.WriteFile(output, chain, 0o644); err != nil { - return err - } - - fmt.Fprintln(out, "Certificate chain written to", output) - - return nil -} diff --git a/cli/cmd/certificateIntermediate.go b/cli/cmd/certificateIntermediate.go deleted file mode 100644 index ee52c211..00000000 --- a/cli/cmd/certificateIntermediate.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/pem" - "fmt" - "io" - "io/ioutil" - - "github.com/spf13/cobra" -) - -func newCertificateIntermediate() *cobra.Command { - var certFilename string - - cmd := &cobra.Command{ - Use: "intermediate ", - Short: "Returns the intermediate certificate of the MarbleRun Coordinator", - Long: `Returns the intermediate certificate of the MarbleRun Coordinator`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[0] - return cliCertificateIntermediate(cmd.OutOrStdout(), hostName, certFilename, eraConfig, insecureEra, acceptedTCBStatuses) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVarP(&certFilename, "output", "o", "marblerunIntermediateCA.crt", "File to save the certificate to") - - return cmd -} - -// cliCertificateIntermediate gets the intermediate certificate of the MarbleRun Coordinator. -func cliCertificateIntermediate(out io.Writer, host, output, configFilename string, insecure bool, acceptedTCBStatuses []string) error { - certs, err := verifyCoordinator(out, host, configFilename, insecure, acceptedTCBStatuses) - if err != nil { - return err - } - - if len(certs) > 1 { - if err := ioutil.WriteFile(output, pem.EncodeToMemory(certs[0]), 0o644); err != nil { - return err - } - fmt.Fprintln(out, "Intermediate certificate written to", output) - } else { - fmt.Fprintln(out, "WARNING: No intermediate certificate received.") - } - - return nil -} diff --git a/cli/cmd/certificateRoot.go b/cli/cmd/certificateRoot.go deleted file mode 100644 index 1314d12b..00000000 --- a/cli/cmd/certificateRoot.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/pem" - "fmt" - "io" - "io/ioutil" - - "github.com/spf13/cobra" -) - -func newCertificateRoot() *cobra.Command { - var certFilename string - - cmd := &cobra.Command{ - Use: "root ", - Short: "Returns the root certificate of the MarbleRun Coordinator", - Long: `Returns the root certificate of the MarbleRun Coordinator`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[0] - return cliCertificateRoot(cmd.OutOrStdout(), hostName, certFilename, eraConfig, insecureEra, acceptedTCBStatuses) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVarP(&certFilename, "output", "o", "marblerunRootCA.crt", "File to save the certificate to") - - return cmd -} - -// cliCertificateRoot gets the root certificate of the MarbleRun Coordinator and saves it to a file. -func cliCertificateRoot(out io.Writer, host, output, configFilename string, insecure bool, acceptedTCBStatuses []string) error { - var certs []*pem.Block - certs, err := verifyCoordinator(out, host, configFilename, insecure, acceptedTCBStatuses) - if err != nil { - return err - } - - if err := ioutil.WriteFile(output, pem.EncodeToMemory(certs[len(certs)-1]), 0o644); err != nil { - return err - } - fmt.Fprintln(out, "Root certificate written to", output) - - return nil -} diff --git a/cli/cmd/completion.go b/cli/cmd/completion.go deleted file mode 100644 index fa303f22..00000000 --- a/cli/cmd/completion.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "fmt" - - "github.com/spf13/cobra" -) - -func newCompletionCmd() *cobra.Command { - example := ` - For bash: - source <(marblerun completion bash) - - For zsh: - If shell completion is not already enabled in your environment you will need to enable it: - echo "autoload -U compinit; compinit" >> ~/.zshrc - - To load completions for each session, execute once: - marblerun completion zsh > "${fpath[1]}/_marblerun" - ` - cmd := &cobra.Command{ - Use: "completion", - Short: "Output script for specified shell to enable autocompletion", - Long: `Output script for specified shell to enable autocompletion`, - Example: example, - Args: cobra.ExactArgs(1), - ValidArgs: []string{"bash", "zsh"}, - DisableFlagsInUseLine: true, - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - shell := args[0] - out, err := cliCompletion(shell, cmd.Root()) - if err != nil { - return err - } - fmt.Print(out) - return nil - }, - } - - return cmd -} - -// cliCompletion returns the autocompletion script for the specified shell. -func cliCompletion(shell string, parent *cobra.Command) (string, error) { - var buf bytes.Buffer - var err error - - switch shell { - case "bash": - err = parent.GenBashCompletion(&buf) - // case "fish": - // err = parent.GenFishCompletion(&buf, false) - case "zsh": - err = parent.GenZshCompletion(&buf) - default: - err = fmt.Errorf("unsupported shell type [%s]", shell) - } - - return buf.String(), err -} diff --git a/cli/cmd/completion_test.go b/cli/cmd/completion_test.go deleted file mode 100644 index 4eee67a0..00000000 --- a/cli/cmd/completion_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCliCompletion(t *testing.T) { - assert := assert.New(t) - - bashCompletion, err := cliCompletion("bash", NewRootCmd()) - assert.NoError(err) - assert.Contains(bashCompletion, "# bash completion for marblerun") - - // fishCompletion, err := cliCompletion("fish", rootCmd) - // assert.NoError(err) - // assert.Contains(fishCompletion, "# fish completion for marblerun") - - zshCompletion, err := cliCompletion("zsh", NewRootCmd()) - assert.NoError(err) - assert.Contains(zshCompletion, "# zsh completion for marblerun") - - _, err = cliCompletion("unsupported-shell", NewRootCmd()) - assert.Error(err) -} diff --git a/cli/cmd/graminePrepare.go b/cli/cmd/graminePrepare.go deleted file mode 100644 index 8ddad921..00000000 --- a/cli/cmd/graminePrepare.go +++ /dev/null @@ -1,388 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - - "github.com/c2h5oh/datasize" - "github.com/fatih/color" - "github.com/pelletier/go-toml" - "github.com/spf13/cobra" -) - -// premainName is the name of the premain executable used. -const premainName = "premain-libos" - -// uuidName is the file name of a Marble's uuid. -const uuidName = "uuid" - -// commentMarbleRunAdditions holds the marker which is appended to the Gramine manifest before the performed additions. -const commentMarbleRunAdditions = "\n# MARBLERUN -- auto generated configuration entries \n" - -// longDescription is the help text shown for this command. -const longDescription = `Modifies a Gramine manifest for use with MarbleRun. - -This command tries to automatically adjust the required parameters in an already existing Gramine manifest template, simplifying the migration of your existing Gramine application to MarbleRun. -Please note that you still need to manually create a MarbleRun manifest. - -For more information about the requirements and changes performed, consult the documentation: https://edglss.cc/doc-mr-gramine - -The parameter of this command is the path of the Gramine manifest template you want to modify. -` - -type diff struct { - alreadyExists bool - // type of the entry, one of {'string', 'array'} - entryType string - // content of the entry - manifestEntry string -} - -func newGraminePrepareCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "gramine-prepare", - Short: "Modifies a Gramine manifest for use with MarbleRun", - Long: longDescription, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - fileName := args[0] - - return addToGramineManifest(fileName) - }, - SilenceUsage: true, - Deprecated: "output generated by this command may not compatible with the latest version of Gramine. " + - "This command will be removed in a future release of MarbleRun.", - } - - return cmd -} - -func addToGramineManifest(fileName string) error { - // Read Gramine manifest and populate TOML tree - fmt.Println("Reading file:", fileName) - - file, err := ioutil.ReadFile(fileName) - if err != nil { - return err - } - if strings.Contains(string(file), premainName) || strings.Contains(string(file), "EDG_MARBLE_COORDINATOR_ADDR") || - strings.Contains(string(file), "EDG_MARBLE_TYPE") || strings.Contains(string(file), "EDG_MARBLE_UUID_FILE") || - strings.Contains(string(file), "EDG_MARBLE_DNS_NAMES") { - color.Yellow("The supplied manifest already contains changes for MarbleRun. Have you selected the correct file?") - return errors.New("manifest already contains MarbleRun changes") - } - - tree, err := toml.LoadFile(fileName) - if os.IsNotExist(err) { - return fmt.Errorf("file does not exist: %v", fileName) - } else if err != nil { - color.Red("ERROR: Cannot parse manifest. Have you selected the corrected file?") - return err - } - - // Parse tree for changes and generate maps with original entries & changes - original, changes, err := parseTreeForChanges(tree) - if err != nil { - return err - } - - // Calculate the differences, apply the changes - return performChanges(calculateChanges(original, changes), fileName) -} - -func parseTreeForChanges(tree *toml.Tree) (map[string]interface{}, map[string]interface{}, error) { - // Create two maps, one with original values, one with the values we want to add or modify - original := make(map[string]interface{}) - changes := make(map[string]interface{}) - - // The values we want to search in the original manifest - original["libos.entrypoint"] = tree.Get("libos.entrypoint") - original["loader.insecure__use_host_env"] = tree.Get("loader.insecure__use_host_env") - original["loader.argv0_override"] = tree.Get("loader.argv0_override") - original["sgx.remote_attestation"] = tree.Get("sgx.remote_attestation") - original["sgx.enclave_size"] = tree.Get("sgx.enclave_size") - original["sgx.thread_num"] = tree.Get("sgx.thread_num") - original["loader.env.EDG_MARBLE_COORDINATOR_ADDR"] = tree.Get("loader.env.EDG_MARBLE_COORDINATOR_ADDR") - original["loader.env.EDG_MARBLE_TYPE"] = tree.Get("loader.env.EDG_MARBLE_TYPE") - original["loader.env.EDG_MARBLE_UUID_FILE"] = tree.Get("loader.env.EDG_MARBLE_UUID_FILE") - original["loader.env.EDG_MARBLE_DNS_NAMES"] = tree.Get("loader.env.EDG_MARBLE_DNS_NAMES") - - // Abort, if we cannot find an entrypoint - if original["libos.entrypoint"] == nil { - return nil, nil, errors.New("cannot find libos.entrypoint") - } - - // add premain and uuid files - if err := insertFile(original, changes, "trusted_files", premainName, tree); err != nil { - return nil, nil, err - } - if err := insertFile(original, changes, "allowed_files", uuidName, tree); err != nil { - return nil, nil, err - } - - // Add premain-libos executable as trusted file & entry point - changes["libos.entrypoint"] = premainName - - // Set original entrypoint as argv0. If one exists, keep the old one - if original["loader.argv0_override"] == nil { - changes["loader.argv0_override"] = original["libos.entrypoint"].(string) - } - - // If insecure host environment is disabled (which hopefully it is), specify the required passthrough variables - if original["loader.insecure__use_host_env"] == nil || !original["loader.insecure__use_host_env"].(bool) { - if original["loader.env.EDG_MARBLE_COORDINATOR_ADDR"] == nil { - changes["loader.env.EDG_MARBLE_COORDINATOR_ADDR"] = "{ passthrough = true }" - } - if original["loader.env.EDG_MARBLE_TYPE"] == nil { - changes["loader.env.EDG_MARBLE_TYPE"] = "{ passthrough = true }" - } - if original["loader.env.EDG_MARBLE_UUID_FILE"] == nil { - changes["loader.env.EDG_MARBLE_UUID_FILE"] = "{ passthrough = true }" - } - if original["loader.env.EDG_MARBLE_DNS_NAMES"] == nil { - changes["loader.env.EDG_MARBLE_DNS_NAMES"] = "{ passthrough = true }" - } - } - - // Enable remote attestation - if original["sgx.remote_attestation"] == nil || !original["sgx.remote_attestation"].(bool) { - changes["sgx.remote_attestation"] = true - } - - // Ensure at least 1024 MB of enclave memory for the premain Go runtime - var v datasize.ByteSize - if original["sgx.enclave_size"] != nil { - v.UnmarshalText([]byte(original["sgx.enclave_size"].(string))) - } - if v.GBytes() < 1.00 { - changes["sgx.enclave_size"] = "1024M" - } - - // Ensure at least 16 SGX threads for the premain Go runtime - if original["sgx.thread_num"] == nil || original["sgx.thread_num"].(int64) < 16 { - changes["sgx.thread_num"] = 16 - } - - return original, changes, nil -} - -// calculateChanges takes two maps with TOML indices and values as input and calculates the difference between them. -func calculateChanges(original map[string]interface{}, updates map[string]interface{}) []diff { - var changeDiffs []diff - // Note: This function only outputs entries which are defined in the original map. - // This is designed this way as we need to check for each value if it already was set and if it was, if it was correct. - // Defining new entries in "updates" is NOT intended here, and these values will be ignored. - for index, originalValue := range original { - if changedValue, ok := updates[index]; ok { - // Add quotation marks for strings, direct value if not - newDiff := diff{alreadyExists: originalValue != nil} - // Add quotation marks for strings, direct value if not - switch v := changedValue.(type) { - case string: - newDiff.entryType = "string" - newDiff.manifestEntry = fmt.Sprintf("%s = \"%v\"", index, v) - case []interface{}: - newDiff.entryType = "array" - newEntry := fmt.Sprintf("%s = [\n", index) - for _, val := range v { - newEntry = fmt.Sprintf("%s \"%v\",\n", newEntry, val) - } - newDiff.manifestEntry = fmt.Sprintf("%s]", newEntry) - default: - newDiff.entryType = "string" - newDiff.manifestEntry = fmt.Sprintf("%s = %v", index, v) - } - changeDiffs = append(changeDiffs, newDiff) - } - } - - // Sort changes alphabetically - sort.Slice(changeDiffs, func(i, j int) bool { - return changeDiffs[i].manifestEntry < changeDiffs[j].manifestEntry - }) - - return changeDiffs -} - -// performChanges displays the suggested changes to the user and tries to automatically perform them. -func performChanges(changeDiffs []diff, fileName string) error { - fmt.Println("\nMarbleRun suggests the following changes to your Gramine manifest:") - for _, entry := range changeDiffs { - if entry.alreadyExists { - color.Yellow(entry.manifestEntry) - } else { - color.Green(entry.manifestEntry) - } - } - - accepted, err := promptYesNo(os.Stdin, promptForChanges) - if err != nil { - return err - } - if !accepted { - fmt.Println("Aborting.") - return nil - } - - directory := filepath.Dir(fileName) - - // Read Gramine manifest as normal text file - manifestContentOriginal, err := ioutil.ReadFile(fileName) - if err != nil { - return err - } - - // Perform modifications to manifest - fmt.Println("Applying changes...") - manifestContentModified, err := appendAndReplace(changeDiffs, manifestContentOriginal) - if err != nil { - return err - } - - // Backup original manifest - backupFileName := filepath.Base(fileName) + ".bak" - fmt.Printf("Saving original manifest as %s...\n", backupFileName) - if err := ioutil.WriteFile(filepath.Join(directory, backupFileName), manifestContentOriginal, 0o644); err != nil { - return err - } - - // Write modified file to disk - fileNameBase := filepath.Base(fileName) - fmt.Printf("Saving changes to %s...\n", fileNameBase) - if err := ioutil.WriteFile(fileName, manifestContentModified, 0o644); err != nil { - return err - } - - fmt.Println("Downloading MarbleRun premain from GitHub...") - // Download MarbleRun premain for Gramine from GitHub - if err := downloadPremain(directory); err != nil { - color.Red("ERROR: Cannot download '%s' from GitHub. Please add the file manually.", premainName) - } - - fmt.Println("\nDone! You should be good to go for MarbleRun!") - - return nil -} - -func downloadPremain(directory string) error { - cleanVersion := "v" + strings.Split(Version, "-")[0] - - // Download premain-libos executable - resp, err := http.Get(fmt.Sprintf("https://github.com/edgelesssys/marblerun/releases/download/%s/%s", cleanVersion, premainName)) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.New("received a non-successful HTTP response") - } - - out, err := os.Create(filepath.Join(directory, premainName)) - if err != nil { - return err - } - defer out.Close() - - if _, err := io.Copy(out, resp.Body); err != nil { - return err - } - - fmt.Printf("Successfully downloaded %s.\n", premainName) - - return nil -} - -/* -Perform the manifest modification. -For existing entries: Run a RegEx search, replace the line. -For new entries: Append to the end of the file. -NOTE: This only works for flat-mapped TOML configs. -These seem to be usually used for Gramine manifests. -However, TOML is quite flexible, and there are no TOML parsers out there which are style & comments preserving -So, if we do not have a flat-mapped config, this will fail at some point. -*/ -func appendAndReplace(changeDiffs []diff, manifestContent []byte) ([]byte, error) { - newManifestContent := manifestContent - - var firstAdditionDone bool - for _, value := range changeDiffs { - if value.alreadyExists { - // If a value was previously existing, we replace the existing entry - key := strings.Split(value.manifestEntry, " =") - regexKey := strings.ReplaceAll(key[0], ".", "\\.") - var regex *regexp.Regexp - - switch value.entryType { - case "string": - regex = regexp.MustCompile("(?m)^" + regexKey + "\\s?=.*$") - case "array": - regex = regexp.MustCompile("(?m)^" + regexKey + "\\s?=([^\\]]*)\\]$") - default: - return nil, fmt.Errorf("unknown manifest entry type: %v", value.entryType) - } - - // Check if we actually found the entry we searched for. If not, we might be dealing with a TOML file we cannot handle correctly without a full parser. - regexMatches := regex.FindAll(newManifestContent, -1) - if regexMatches == nil { - color.Red("ERROR: Cannot find specified entry. Your Gramine config might not be flat-mapped.") - color.Red("MarbleRun can only automatically modify manifests using a flat hierarchy, as otherwise we would lose all styling & comments.") - color.Red("To continue, please manually perform the changes printed above in your Gramine manifest.") - return nil, errors.New("failed to detect position of config entry") - } else if len(regexMatches) > 1 { - color.Red("ERROR: Found multiple potential matches for automatic value substitution.") - color.Red("Is the configuration valid (no multiple declarations)?") - return nil, errors.New("found multiple matches for a single entry") - } - // But if everything went as expected, replace the entry - newManifestContent = regex.ReplaceAll(newManifestContent, []byte(value.manifestEntry)) - } else { - // If a value was not defined previously, we append the new entries down below - if !firstAdditionDone { - appendToFile := commentMarbleRunAdditions - newManifestContent = append(newManifestContent, []byte(appendToFile)...) - firstAdditionDone = true - } - appendToFile := value.manifestEntry + "\n" - newManifestContent = append(newManifestContent, []byte(appendToFile)...) - } - } - - return newManifestContent, nil -} - -// insertFile checks what trusted/allowed file declaration is used in the manifest and inserts files accordingly. -// Trusted/allowed files are either present in legacy 'sgx.trusted_files.identifier = "file:/path/file"' format -// or in TOML-array format. -func insertFile(original, changes map[string]interface{}, fileType, fileName string, tree *toml.Tree) error { - fileTree := tree.Get("sgx." + fileType) - switch fileTree.(type) { - case nil: - // No files are defined in the original manifest - changes["sgx."+fileType] = []interface{}{"file:" + fileName} - return nil - case *toml.Tree: - // legacy format - changes["sgx."+fileType+".marblerun_"+fileName] = "file:" + fileName - case []interface{}: - // TOML-array format, append file to the array - original["sgx."+fileType] = tree.Get("sgx." + fileType) - changes["sgx."+fileType] = append(original["sgx."+fileType].([]interface{}), "file:"+fileName) - default: - return errors.New("could not read files from Gramine manifest") - } - return nil -} diff --git a/cli/cmd/graminePrepare_test.go b/cli/cmd/graminePrepare_test.go deleted file mode 100644 index 2acf7615..00000000 --- a/cli/cmd/graminePrepare_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/c2h5oh/datasize" - "github.com/jarcoal/httpmock" - "github.com/pelletier/go-toml" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const someManifest = ` -libos.entrypoint = "myapplication" -sgx.remote_attestation = false -# Some comment here in between -# This should not match: sgx.enclave_size - 2 -# This should also not match: sgx.enclave_size = 24M -sgx.enclave_size = "128M" -sgx.trusted_files = [ - "file:/usr/favorite.file", - "file:/usr/lib/important.so" -] -sgx.allowed_files.unimportant = "file:/usr/not_that_important.txt" -` - -func TestCalculateChanges(t *testing.T) { - assert := assert.New(t) - - originalMap := make(map[string]interface{}) - changedMap := make(map[string]interface{}) - - originalMap["someString"] = "test" // should be in diffs - originalMap["someNilValue"] = nil // should be in diffs - originalMap["someInt"] = 4 // should not be in diffs - - changedMap["someString"] = "This is a test." // should be in diffs - changedMap["someNilValue"] = true // should be in diffs - changedMap["someNewEntry"] = "This is a new entry." // should not be in diffs - - diffs := calculateChanges(originalMap, changedMap) - - // NOTE: diffs only should contain changes which were at least defined on the original map - // Values which were undefined before should not be in here - for _, value := range diffs { - indexString := strings.Split(value.manifestEntry, " =") - _, ok := originalMap[indexString[0]] - assert.True(ok, "Diffs contains entries which were not defined in the original map initially") - } - - // Check if we got TOML style output - // And check if diffs array was sorted correctly (is supposed to be sorted alphabetically) - assert.Len(diffs, 2, "diffs contains unexpected amount of entries") - assert.Equal(diffs[0].manifestEntry, "someNilValue = true") - assert.Equal(diffs[1].manifestEntry, "someString = \"This is a test.\"") -} - -func TestParseTreeForChanges(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - tree, err := toml.Load(someManifest) - require.NoError(err) - - // Checking all possible combinations will result in tremendous effort... - // So for this, we check if we at least changed the entry point and the memory/thread requirements for the Go runtime - original, changes, err := parseTreeForChanges(tree) - require.NoError(err) - assert.NotEmpty(original) - assert.NotEmpty(changes) - - // Verify minimum changes - var v datasize.ByteSize - - assert.Equal(premainName, changes["libos.entrypoint"]) - assert.GreaterOrEqual(changes["sgx.thread_num"], 16) - require.NoError(v.UnmarshalText([]byte(changes["sgx.enclave_size"].(string)))) - assert.GreaterOrEqual(v.GBytes(), 1.00) - assert.Equal([]interface{}{"file:/usr/favorite.file", "file:/usr/lib/important.so", "file:premain-libos"}, changes["sgx.trusted_files"]) -} - -func TestAppendAndReplace(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Parse hardcoded test manifest - tomlTree, err := toml.Load(someManifest) - require.NoError(err) - - // Get values from hardcoded test manifest - original := make(map[string]interface{}) - changes := make(map[string]interface{}) - original["sgx.remote_attestation"] = tomlTree.Get("sgx.remote_attestation") - original["sgx.enclave_size"] = tomlTree.Get("sgx.enclave_size") - original["sgx.thread_num"] = tomlTree.Get("sgx.thread_num") - original["sgx.trusted_files"] = tomlTree.Get("sgx.trusted_files") - - // Set some changes we want to perform - changes["sgx.remote_attestation"] = true - changes["sgx.enclave_size"] = "1024M" - changes["sgx.thread_num"] = 16 - changedFiles := []interface{}{"file:/usr/favorite.file", "file:/usr/lib/important.so", "file:premain-libos"} - changes["sgx.trusted_files"] = changedFiles - - // Calculate the differences - diffs := calculateChanges(original, changes) - - // Perform the modification - someNewManifest, err := appendAndReplace(diffs, []byte(someManifest)) - assert.NoError(err) - assert.NotEqualValues(someManifest, someNewManifest) - - // Check if it's still valid TOML & if changes were applied correctly - newTomlTree, err := toml.Load(string(someNewManifest)) - assert.NoError(err) - newRemoteAttestation := newTomlTree.Get("sgx.remote_attestation") - assert.EqualValues(true, newRemoteAttestation.(bool)) - newEnclaveSize := newTomlTree.Get("sgx.enclave_size") - assert.EqualValues("1024M", newEnclaveSize.(string)) - newThreadNum := newTomlTree.Get("sgx.thread_num") - assert.EqualValues(16, newThreadNum.(int64)) - newTrustedFiles := newTomlTree.Get("sgx.trusted_files") - assert.EqualValues(changedFiles, newTrustedFiles) -} - -func TestDownloadPremain(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // Use HTTP mock for external download - httpmock.Activate() - defer httpmock.DeactivateAndReset() - testContent := []byte("this is obviously not a binary, but we gotta test this anyway!") - - // We don't want to hardcode the version, so let's use a regexp match here - httpmock.RegisterResponder("GET", `=~^https://github\.com/edgelesssys/marblerun/releases/download/v[0-9\.]*/premain-libos`, - httpmock.NewBytesResponder(200, testContent)) - - // Create tempdir for downloads - tempDir, err := ioutil.TempDir("", "") - require.NoError(err) - defer os.RemoveAll(tempDir) - - // Try to download premain - assert.NoError(downloadPremain(tempDir)) - content, err := ioutil.ReadFile(filepath.Join(tempDir, premainName)) - assert.NoError(err) - assert.Equal(testContent, content) - - // We should have one download here - info := httpmock.GetCallCountInfo() - assert.Equal(1, info[`GET =~^https://github\.com/edgelesssys/marblerun/releases/download/v[0-9\.]*/premain-libos`]) -} diff --git a/cli/cmd/install.go b/cli/cmd/install.go deleted file mode 100644 index d5b5e8e0..00000000 --- a/cli/cmd/install.go +++ /dev/null @@ -1,506 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" - - "github.com/edgelesssys/marblerun/util" - "github.com/gofrs/flock" - "github.com/spf13/cobra" - "gopkg.in/yaml.v2" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/repo" - "helm.sh/helm/v3/pkg/strvals" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -type installOptions struct { - chartPath string - hostname string - version string - resourceKey string - dcapQpl string - pccsUrl string - useSecureCert string - accessToken string - simulation bool - disableInjection bool - clientPort int - meshPort int - kubeClient kubernetes.Interface - settings *cli.EnvSettings -} - -func newInstallCmd() *cobra.Command { - options := &installOptions{} - - cmd := &cobra.Command{ - Use: "install", - Short: "Installs MarbleRun on a Kubernetes cluster", - Long: `Installs MarbleRun on a Kubernetes cluster`, - Example: `# Install MarbleRun in simulation mode -marblerun install --simulation - -# Install MarbleRun using the Intel QPL and custom PCCS -marblerun install --dcap-qpl intel --dcap-pccs-url https://pccs.example.com/sgx/certification/v3/ --dcap-secure-cert FALSE`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - options.settings = cli.New() - var err error - options.kubeClient, err = getKubernetesInterface() - if err != nil { - return err - } - - return cliInstall(options) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVar(&options.hostname, "domain", "localhost", "Sets the CNAME for the Coordinator certificate") - cmd.Flags().StringVar(&options.chartPath, "marblerun-chart-path", "", "Path to MarbleRun helm chart") - cmd.Flags().StringVar(&options.version, "version", "", "Version of the Coordinator to install, latest by default") - cmd.Flags().StringVar(&options.resourceKey, "resource-key", "", "Resource providing SGX, different depending on used device plugin. Use this to set tolerations/resources if your device plugin is not supported by MarbleRun") - cmd.Flags().StringVar(&options.dcapQpl, "dcap-qpl", "azure", `Quote provider library to use by the Coordinator. One of {"azure", "intel"}`) - cmd.Flags().StringVar(&options.pccsUrl, "dcap-pccs-url", "https://localhost:8081/sgx/certification/v3/", "Provisioning Certificate Caching Service (PCCS) server address") - cmd.Flags().StringVar(&options.useSecureCert, "dcap-secure-cert", "TRUE", "To accept insecure HTTPS certificate from the PCCS, set this option to FALSE") - cmd.Flags().StringVar(&options.accessToken, "enterprise-access-token", "", "Access token for Enterprise Coordinator. Leave empty for default installation") - cmd.Flags().BoolVar(&options.simulation, "simulation", false, "Set MarbleRun to start in simulation mode") - cmd.Flags().BoolVar(&options.disableInjection, "disable-auto-injection", false, "Install MarbleRun without auto-injection webhook") - cmd.Flags().IntVar(&options.meshPort, "mesh-server-port", 2001, "Set the mesh server port. Needs to be configured to the same port as in the data-plane marbles") - cmd.Flags().IntVar(&options.clientPort, "client-server-port", 4433, "Set the client server port. Needs to be configured to the same port as in your client tool stack") - - return cmd -} - -// cliInstall installs MarbleRun on the cluster. -func cliInstall(options *installOptions) error { - actionConfig := new(action.Configuration) - if err := actionConfig.Init(options.settings.RESTClientGetter(), helmNamespace, os.Getenv("HELM_DRIVER"), debug); err != nil { - return err - } - - // create helm installer - installer := action.NewInstall(actionConfig) - installer.CreateNamespace = true - installer.Namespace = helmNamespace - installer.ReleaseName = helmRelease - installer.ChartPathOptions.Version = options.version - - if options.chartPath == "" { - // No chart was specified -> add or update edgeless helm repo - err := getRepo(helmRepoName, helmRepoURI, options.settings) - if err != nil { - return err - } - - // Enterprise chart is used if an access token is provided - chartName := helmChartName - if options.accessToken != "" { - chartName = helmChartNameEnterprise - } - options.chartPath, err = installer.ChartPathOptions.LocateChart(chartName, options.settings) - if err != nil { - return err - } - } - chart, err := loader.Load(options.chartPath) - if err != nil { - return err - } - - var resourceKey string - if len(options.resourceKey) <= 0 { - resourceKey, err = getSGXResourceKey(options.kubeClient) - if err != nil { - return err - } - } else { - resourceKey = options.resourceKey - } - - // set overwrite values - finalValues := map[string]interface{}{} - stringValues := []string{} - - stringValues = append(stringValues, fmt.Sprintf("coordinator.meshServerPort=%d", options.meshPort)) - stringValues = append(stringValues, fmt.Sprintf("coordinator.clientServerPort=%d", options.clientPort)) - - if options.simulation { - // simulation mode, disable tolerations and resources, set simulation to true - stringValues = append(stringValues, - fmt.Sprintf("tolerations=%s", "null"), - fmt.Sprintf("coordinator.simulation=%t", options.simulation), - fmt.Sprintf("coordinator.resources.limits=%s", "null"), - fmt.Sprintf("coordinator.hostname=%s", options.hostname), - fmt.Sprintf("dcap=%s", "null"), - ) - } else { - stringValues = append(stringValues, - fmt.Sprintf("coordinator.hostname=%s", options.hostname), - fmt.Sprintf("dcap.qpl=%s", options.dcapQpl), - fmt.Sprintf("dcap.pccsUrl=%s", options.pccsUrl), - fmt.Sprintf("dcap.useSecureCert=%s", options.useSecureCert), - ) - - // Helms value merge function will overwrite any preset values for "tolerations" if we set new ones here - // To avoid this we set the new toleration for "resourceKey" and copy all preset tolerations - needToleration := true - idx := 0 - for _, toleration := range chart.Values["tolerations"].([]interface{}) { - if key, ok := toleration.(map[string]interface{})["key"]; ok { - if key == resourceKey { - needToleration = false - } - stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].key=%v", idx, key)) - } - if operator, ok := toleration.(map[string]interface{})["operator"]; ok { - stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].operator=%v", idx, operator)) - } - if effect, ok := toleration.(map[string]interface{})["effect"]; ok { - stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].effect=%v", idx, effect)) - } - if value, ok := toleration.(map[string]interface{})["value"]; ok { - stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].value=%v", idx, value)) - } - if tolerationSeconds, ok := toleration.(map[string]interface{})["tolerationSeconds"]; ok { - stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].tolerationSeconds=%v", idx, tolerationSeconds)) - } - idx++ - } - if needToleration { - stringValues = append(stringValues, - fmt.Sprintf("tolerations[%d].key=%s", idx, resourceKey), - fmt.Sprintf("tolerations[%d].operator=Exists", idx), - fmt.Sprintf("tolerations[%d].effect=NoSchedule", idx), - ) - } - } - - // Configure enterprise access token - if options.accessToken != "" { - coordinatorCfg, ok := chart.Values["coordinator"].(map[string]interface{}) - if !ok { - return errors.New("coordinator not found in chart values") - } - repository, ok := coordinatorCfg["repository"].(string) - if !ok { - return errors.New("coordinator.registry not found in chart values") - } - - token := fmt.Sprintf(`{"auths":{"%s":{"auth":"%s"}}}`, repository, options.accessToken) - stringValues = append(stringValues, fmt.Sprintf("pullSecret.token=%s", base64.StdEncoding.EncodeToString([]byte(token)))) - } - - if !options.disableInjection { - injectorValues, err := installWebhook(options.kubeClient) - if err != nil { - return errorAndCleanup(err, options.kubeClient) - } - - stringValues = append(stringValues, injectorValues...) - stringValues = append(stringValues, fmt.Sprintf("marbleInjector.resourceKey=%s", resourceKey)) - } - - for _, val := range stringValues { - if err := strvals.ParseInto(val, finalValues); err != nil { - return errorAndCleanup(err, options.kubeClient) - } - } - - if !options.simulation { - setSGXValues(resourceKey, finalValues, chart.Values) - } - - if err := chartutil.ValidateAgainstSchema(chart, finalValues); err != nil { - return errorAndCleanup(err, options.kubeClient) - } - - if _, err := installer.Run(chart, finalValues); err != nil { - return errorAndCleanup(err, options.kubeClient) - } - - fmt.Println("MarbleRun installed successfully") - return nil -} - -// Simplified repo_add from helm cli to add MarbleRun repo if it does not yet exist. -// To make sure we use the newest chart we always download the needed index file. -func getRepo(name string, url string, settings *cli.EnvSettings) error { - repoFile := settings.RepositoryConfig - - // Ensure the file directory exists as it is required for file locking - err := os.MkdirAll(filepath.Dir(repoFile), 0o755) - if err != nil && !os.IsExist(err) { - return err - } - - // Acquire a file lock for process synchronization - fileLock := flock.New(strings.Replace(repoFile, filepath.Ext(repoFile), ".lock", 1)) - lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - locked, err := fileLock.TryLockContext(lockCtx, time.Second) - if err == nil && locked { - defer fileLock.Unlock() - } - if err != nil { - return err - } - - b, err := ioutil.ReadFile(repoFile) - if err != nil && !os.IsNotExist(err) { - return err - } - - var f repo.File - if err := yaml.Unmarshal(b, &f); err != nil { - return err - } - - c := &repo.Entry{ - Name: name, - URL: url, - } - - r, err := repo.NewChartRepository(c, getter.All(settings)) - if err != nil { - return err - } - - if _, err := r.DownloadIndexFile(); err != nil { - return errors.New("chart repository cannot be reached") - } - - f.Update(c) - - if err := f.WriteFile(repoFile, 0o644); err != nil { - return err - } - return nil -} - -// installWebhook enables a mutating admission webhook to allow automatic injection of values into pods. -func installWebhook(kubeClient kubernetes.Interface) ([]string, error) { - // verify 'marblerun' namespace exists, if not create it - if err := verifyNamespace(helmNamespace, kubeClient); err != nil { - return nil, err - } - - fmt.Printf("Setting up MarbleRun Webhook") - certificateHandler, err := getCertificateHandler(kubeClient) - if err != nil { - return nil, err - } - fmt.Printf(".") - if err := certificateHandler.signRequest(); err != nil { - return nil, err - } - fmt.Printf(".") - injectorValues, err := certificateHandler.setCaBundle() - if err != nil { - return nil, err - } - cert, err := certificateHandler.get() - if err != nil { - return nil, err - } - if len(cert) <= 0 { - return nil, fmt.Errorf("certificate was not signed by the CA") - } - fmt.Printf(".") - - if err := createSecret(certificateHandler.getKey(), cert, kubeClient); err != nil { - return nil, err - } - fmt.Printf(" Done\n") - return injectorValues, nil -} - -// createSecret creates a secret containing the signed certificate and private key for the webhook server. -func createSecret(privKey *rsa.PrivateKey, crt []byte, kubeClient kubernetes.Interface) error { - rsaPEM := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privKey), - }, - ) - - newSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "marble-injector-webhook-certs", - Namespace: helmNamespace, - }, - Data: map[string][]byte{ - "tls.crt": crt, - "tls.key": rsaPEM, - }, - } - - _, err := kubeClient.CoreV1().Secrets(helmNamespace).Create(context.TODO(), newSecret, metav1.CreateOptions{}) - return err -} - -func getCertificateHandler(kubeClient kubernetes.Interface) (certificateInterface, error) { - isLegacy, err := checkLegacyKubernetesVersion(kubeClient) - if err != nil { - return nil, err - } - if isLegacy { - fmt.Printf("\nKubernetes version lower than 1.19 detected, using self-signed certificates as CABundle") - return newCertificateLegacy() - } - return newCertificateV1(kubeClient) -} - -func verifyNamespace(namespace string, kubeClient kubernetes.Interface) error { - _, err := kubeClient.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) - if err != nil { - // if the namespace does not exist we create it - if err.Error() == fmt.Sprintf("namespaces \"%s\" not found", namespace) { - marbleNamespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - if _, err := kubeClient.CoreV1().Namespaces().Create(context.TODO(), marbleNamespace, metav1.CreateOptions{}); err != nil { - return err - } - } else { - return err - } - } - return nil -} - -// getSGXResourceKey checks what device plugin is providing SGX on the cluster and returns the corresponding resource key. -func getSGXResourceKey(kubeClient kubernetes.Interface) (string, error) { - nodes, err := kubeClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return "", err - } - - for _, node := range nodes.Items { - if nodeHasAlibabaDevPlugin(node.Status.Capacity) { - return util.AlibabaEpc.String(), nil - } - if nodeHasAzureDevPlugin(node.Status.Capacity) { - return util.AzureEpc.String(), nil - } - if nodeHasIntelDevPlugin(node.Status.Capacity) { - return util.IntelEpc.String(), nil - } - } - - // assume cluster has the intel SGX device plugin by default - return util.IntelEpc.String(), nil -} - -// setSGXValues sets the needed values for the coordinator as a map[string]interface. -// strvals can't parse keys which include dots, e.g. setting as a resource limit key "sgx.intel.com/epc" will lead to errors. -func setSGXValues(resourceKey string, values, chartValues map[string]interface{}) { - values["coordinator"].(map[string]interface{})["resources"] = map[string]interface{}{ - "limits": map[string]interface{}{}, - "requests": map[string]interface{}{}, - } - - var needNewLimit bool - limit := util.GetEPCResourceLimit(resourceKey) - - // remove all previously set sgx resource limits - if presetLimits, ok := chartValues["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{}); ok { - for oldResourceKey := range presetLimits { - // Make sure the key we delete is an unwanted sgx resource and not a custom resource or common resource (cpu, memory, etc.) - if needsDeletion(oldResourceKey, resourceKey) { - values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[oldResourceKey] = nil - needNewLimit = true - } - } - } - - // remove all previously set sgx resource requests - if presetLimits, ok := chartValues["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{}); ok { - for oldResourceKey := range presetLimits { - if needsDeletion(oldResourceKey, resourceKey) { - values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{})[oldResourceKey] = nil - needNewLimit = true - } - } - } - - // Set the new sgx resource limit, kubernetes will automatically set a resource request equal to the limit - if needNewLimit { - values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[resourceKey] = limit - } - - // Make sure provision and enclave bit is set if the Intel plugin is used - if resourceKey == util.IntelEpc.String() { - values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[util.IntelProvision.String()] = 1 - values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[util.IntelEnclave.String()] = 1 - } -} - -// errorAndCleanup returns the given error and deletes resources which might have been created previously. -// This prevents secrets and CSRs to stay on the cluster after a failed installation attempt. -func errorAndCleanup(err error, kubeClient kubernetes.Interface) error { - // We dont care about any additional errors here - cleanupCSR(kubeClient) - cleanupSecrets(kubeClient) - return err -} - -// needsDeletion checks if an existing key of a helm chart should be deleted. -// Choice is based on the resource key of the used SGX device plugin. -func needsDeletion(existingKey, sgxKey string) bool { - sgxResources := []string{ - util.AlibabaEpc.String(), util.AzureEpc.String(), util.IntelEpc.String(), - util.IntelProvision.String(), util.IntelEnclave.String(), - } - - switch sgxKey { - case util.AlibabaEpc.String(), util.AzureEpc.String(): - // Delete all non Alibaba/Azure SGX resources depending on the used SGX device plugin - return sgxKey != existingKey && keyInList(existingKey, sgxResources) - case util.IntelEpc.String(): - // Delete all non Intel SGX resources depending on the used SGX device plugin - // Keep Intel provision and enclave bit - return keyInList(existingKey, []string{util.AlibabaEpc.String(), util.AzureEpc.String()}) - default: - // Either no SGX plugin or a custom SGX plugin is used - // Delete all known SGX resources - return keyInList(existingKey, sgxResources) - } -} - -func keyInList(key string, list []string) bool { - for _, l := range list { - if key == l { - return true - } - } - return false -} - -func debug(format string, v ...interface{}) { -} diff --git a/cli/cmd/manifest.go b/cli/cmd/manifest.go deleted file mode 100644 index a50cd11f..00000000 --- a/cli/cmd/manifest.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/pem" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/url" - - "github.com/spf13/cobra" - "github.com/tidwall/gjson" -) - -func newManifestCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "manifest", - Short: "Manages manifest for the MarbleRun Coordinator", - Long: ` -Manages manifests for the MarbleRun Coordinator. -Used to either set the manifest, update an already set manifest, -or return a signature of the currently set manifest to the user`, - Example: "manifest set manifest.json example.com:4433 [--era-config=config.json] [--insecure]", - } - - cmd.PersistentFlags().StringVar(&eraConfig, "era-config", "", "Path to remote attestation config file in json format, if none provided the newest configuration will be loaded from github") - cmd.PersistentFlags().BoolVarP(&insecureEra, "insecure", "i", false, "Set to skip quote verification, needed when running in simulation mode") - cmd.PersistentFlags().StringSliceVar(&acceptedTCBStatuses, "accepted-tcb-statuses", []string{"UpToDate"}, "Comma-separated list of user accepted TCB statuses (e.g. ConfigurationNeeded,ConfigurationAndSWHardeningNeeded)") - cmd.AddCommand(newManifestGet()) - cmd.AddCommand(newManifestLog()) - cmd.AddCommand(newManifestSet()) - cmd.AddCommand(newManifestSignature()) - cmd.AddCommand(newManifestUpdate()) - cmd.AddCommand(newManifestVerify()) - - return cmd -} - -// cliDataGet requests data from the Coordinators rest api. -func cliDataGet(host, target, jsonPath string, cert []*pem.Block) ([]byte, error) { - client, err := restClient(cert, nil) - if err != nil { - return nil, err - } - - url := url.URL{Scheme: "https", Host: host, Path: target} - resp, err := client.Get(url.String()) - if err != nil { - return nil, err - } - if resp.Body == nil { - return nil, errors.New("received empty response") - } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusOK: - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - manifestData := gjson.GetBytes(respBody, jsonPath) - return []byte(manifestData.String()), nil - default: - return nil, fmt.Errorf("error connecting to server: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) - } -} diff --git a/cli/cmd/manifestLog.go b/cli/cmd/manifestLog.go deleted file mode 100644 index cf8eca5a..00000000 --- a/cli/cmd/manifestLog.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "io/ioutil" - - "github.com/spf13/cobra" -) - -func newManifestLog() *cobra.Command { - var output string - - cmd := &cobra.Command{ - Use: "log ", - Short: "Get the update log from the MarbleRun Coordinator", - Long: `Get the update log from the MarbleRun Coordinator. - The log is list of all successful changes to the Coordinator, - including a timestamp and user performing the operation.`, - Example: "marblerun manifest log $MARBLERUN", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[0] - cert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - cmd.Println("Successfully verified Coordinator, now requesting update log") - response, err := cliDataGet(hostName, "update", "data", cert) - if err != nil { - return err - } - if len(output) > 0 { - return ioutil.WriteFile(output, response, 0o644) - } - cmd.Printf("Update log:\n%s", string(response)) - return nil - }, - SilenceUsage: true, - } - cmd.Flags().StringVarP(&output, "output", "o", "", "Save log to file instead of printing to stdout") - return cmd -} diff --git a/cli/cmd/manifestSet.go b/cli/cmd/manifestSet.go deleted file mode 100644 index 1186178a..00000000 --- a/cli/cmd/manifestSet.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "encoding/json" - "encoding/pem" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - - "github.com/spf13/cobra" - "github.com/tidwall/gjson" - "sigs.k8s.io/yaml" -) - -func newManifestSet() *cobra.Command { - var recoveryFilename string - - cmd := &cobra.Command{ - Use: "set ", - Short: "Sets the manifest for the MarbleRun Coordinator", - Long: "Sets the manifest for the MarbleRun Coordinator", - Example: "marblerun manifest set manifest.json $MARBLERUN --recovery-data=recovery-secret.json --era-config=era.json", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - manifestFile := args[0] - hostName := args[1] - - cert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - - cmd.Println("Successfully verified Coordinator, now uploading manifest") - - // Load manifest - manifest, err := loadManifestFile(manifestFile) - if err != nil { - return err - } - signature := cliManifestSignature(manifest) - cmd.Printf("Manifest signature: %s\n", signature) - - return cliManifestSet(cmd.OutOrStdout(), manifest, hostName, cert, recoveryFilename) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVarP(&recoveryFilename, "recoverydata", "r", "", "File to write recovery data to, print to stdout if non specified") - - return cmd -} - -// cliManifestSet sets the Coordinators manifest using its rest api. -func cliManifestSet(out io.Writer, manifest []byte, host string, cert []*pem.Block, recover string) error { - client, err := restClient(cert, nil) - if err != nil { - return err - } - - url := url.URL{Scheme: "https", Host: host, Path: "manifest"} - resp, err := client.Post(url.String(), "application/json", bytes.NewReader(manifest)) - if err != nil { - return err - } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusOK: - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - fmt.Fprintln(out, "Manifest successfully set") - - if len(respBody) <= 0 { - return nil - } - - response := gjson.GetBytes(respBody, "data") - - // Skip outputting secrets if we do not get any recovery secrets back - if len(response.String()) == 0 { - return nil - } - - // recovery secret was sent, print or save to file - if recover == "" { - fmt.Fprintln(out, response.String()) - } else { - if err := ioutil.WriteFile(recover, []byte(response.String()), 0o644); err != nil { - return err - } - fmt.Fprintf(out, "Recovery data saved to: %s.\n", recover) - } - case http.StatusBadRequest: - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf(response.String()) - default: - return fmt.Errorf("error connecting to server: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - return nil -} - -// loadManifestFile loads a manifest in either json or yaml format and returns the data as json. -func loadManifestFile(filename string) ([]byte, error) { - manifestData, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - // if Valid is true the file was in JSON format and we can just return the data - if json.Valid(manifestData) { - return manifestData, err - } - - // otherwise we try to convert from YAML to json - return yaml.YAMLToJSON(manifestData) -} diff --git a/cli/cmd/manifestUpdate.go b/cli/cmd/manifestUpdate.go deleted file mode 100644 index 63f5dfc2..00000000 --- a/cli/cmd/manifestUpdate.go +++ /dev/null @@ -1,372 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "context" - "crypto/tls" - "encoding/base64" - "fmt" - "io" - "net/http" - "net/url" - "os" - - "github.com/spf13/cobra" - "github.com/tidwall/gjson" -) - -func newManifestUpdate() *cobra.Command { - cmd := &cobra.Command{ - Use: "update", - Short: "Manage manifest updates for the MarbleRun Coordinator", - Long: "Manage manifest updates for the MarbleRun Coordinator.", - } - - cmd.AddCommand(newUpdateApply()) - cmd.AddCommand(newUpdateAcknowledge()) - cmd.AddCommand(newUpdateCancel()) - cmd.AddCommand(newUpdateGet()) - return cmd -} - -func newUpdateApply() *cobra.Command { - cmd := &cobra.Command{ - Use: "apply ", - Short: "Updates the MarbleRun Coordinator with the specified manifest", - Long: ` -Updates the MarbleRun Coordinator with the specified manifest. -An admin certificate specified in the original manifest is needed to verify the authenticity of the update manifest. -`, - Example: "marblerun manifest update apply update-manifest.json $MARBLERUN --cert=admin-cert.pem --key=admin-key.pem --era-config=era.json", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - manifestFile := args[0] - hostName := args[1] - - client, err := authenticatedClient(cmd, hostName) - if err != nil { - return err - } - - // Load manifest - manifest, err := loadManifestFile(manifestFile) - if err != nil { - return err - } - - cmd.Println("Successfully verified Coordinator, now uploading manifest") - return cliManifestUpdate(cmd.OutOrStdout(), manifest, hostName, client) - }, - SilenceUsage: true, - } - - cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") - cmd.MarkFlagRequired("cert") - cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") - cmd.MarkFlagRequired("key") - - return cmd -} - -func newUpdateAcknowledge() *cobra.Command { - cmd := &cobra.Command{ - Use: "acknowledge ", - Short: "Acknowledge a pending update for the MarbleRun Coordinator (Enterprise feature)", - Long: `Acknowledge a pending update for the MarbleRun Coordinator (Enterprise feature). - -In case of multi-party updates, the Coordinator will wait for all participants to acknowledge the update before applying it. -All participants must use the same manifest to acknowledge the pending update. -`, - Example: "marblerun manifest update acknowledge update-manifest.json $MARBLERUN --cert=admin-cert.pem --key=admin-key.pem --era-config=era.json", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - manifestFile := args[0] - hostName := args[1] - - client, err := authenticatedClient(cmd, hostName) - if err != nil { - return err - } - - // Load manifest - manifest, err := loadManifestFile(manifestFile) - if err != nil { - return err - } - - cmd.Println("Successfully verified Coordinator") - return cliManifestUpdateAcknowledge(cmd.OutOrStdout(), manifest, hostName, client) - }, - SilenceUsage: true, - } - - cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") - cmd.MarkFlagRequired("cert") - cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") - cmd.MarkFlagRequired("key") - return cmd -} - -func newUpdateCancel() *cobra.Command { - cmd := &cobra.Command{ - Use: "cancel ", - Short: "Cancel a pending manifest update for the MarbleRun Coordinator (Enterprise feature)", - Long: "Cancel a pending manifest update for the MarbleRun Coordinator (Enterprise feature).", - Example: `marblerun manifest update cancel $MARBLERUN --cert=admin-cert.pem --key=admin-key.pem --era-config=era.json`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[0] - - client, err := authenticatedClient(cmd, hostName) - if err != nil { - return err - } - - cmd.Println("Successfully verified Coordinator") - return cliManifestUpdateCancel(cmd.OutOrStdout(), hostName, client) - }, - SilenceUsage: true, - } - - cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") - cmd.MarkFlagRequired("cert") - cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") - cmd.MarkFlagRequired("key") - return cmd -} - -func newUpdateGet() *cobra.Command { - cmd := &cobra.Command{ - Use: "get ", - Short: "View a pending manifest update (Enterprise feature)", - Long: "View a pending manifest update (Enterprise feature).", - Example: `marblerun manifest update get $MARBLERUN --era-config=era.json`, - Args: cobra.ExactArgs(1), - RunE: runUpdateGet, - SilenceUsage: true, - } - - cmd.Flags().StringP("output", "o", "", "Save output to file instead of printing to stdout") - cmd.Flags().Bool("missing", false, "Display number of missing acknowledgements instead of the manifest") - - return cmd -} - -func authenticatedClient(cmd *cobra.Command, hostName string) (*http.Client, error) { - caCert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return nil, err - } - - cmd.Println("Coordinator verified") - clientAdminCert, err := cmd.Flags().GetString("cert") - if err != nil { - return nil, err - } - clientAdminKey, err := cmd.Flags().GetString("key") - if err != nil { - return nil, err - } - - clCert, err := tls.LoadX509KeyPair(clientAdminCert, clientAdminKey) - if err != nil { - return nil, err - } - - client, err := restClient(caCert, &clCert) - if err != nil { - return nil, err - } - - return client, nil -} - -// cliManifestUpdate updates the Coordinators manifest using its rest api. -func cliManifestUpdate(out io.Writer, manifest []byte, host string, client *http.Client) error { - url := url.URL{Scheme: "https", Host: host, Path: "update"} - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url.String(), bytes.NewReader(manifest)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Fprintln(out, "Update manifest set successfully") - case http.StatusBadRequest: - return fmt.Errorf("unable to update manifest: %s", gjson.GetBytes(respBody, "message").String()) - case http.StatusUnauthorized: - return fmt.Errorf("unable to authorize user: %s", gjson.GetBytes(respBody, "message").String()) - default: - response := gjson.GetBytes(respBody, "message").String() - return fmt.Errorf("error connecting to server: %d %s: %s", resp.StatusCode, http.StatusText(resp.StatusCode), response) - } - - return nil -} - -func cliManifestUpdateAcknowledge(out io.Writer, manifest []byte, host string, client *http.Client) error { - url := url.URL{Scheme: "https", Host: host, Path: "update-manifest"} - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url.String(), bytes.NewReader(manifest)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Fprintf(out, "Acknowledgement successful: %s\n", gjson.GetBytes(respBody, "data").String()) - case http.StatusNotFound: - return fmt.Errorf("unable to update manifest: no pending update found: %s", gjson.GetBytes(respBody, "message").String()) - case http.StatusUnauthorized: - return fmt.Errorf("unable to authorize user: %s", gjson.GetBytes(respBody, "message").String()) - default: - response := gjson.GetBytes(respBody, "message").String() - return fmt.Errorf("error connecting to server: %d %s: %s", resp.StatusCode, http.StatusText(resp.StatusCode), response) - } - - return nil -} - -func cliManifestUpdateCancel(out io.Writer, host string, client *http.Client) error { - url := url.URL{Scheme: "https", Host: host, Path: "update-cancel"} - req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url.String(), http.NoBody) - if err != nil { - return err - } - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Fprintln(out, "Cancellation successful") - case http.StatusNotFound: - return fmt.Errorf("unable to cancel manifest update: no pending update found: %s", gjson.GetBytes(respBody, "message").String()) - case http.StatusUnauthorized: - return fmt.Errorf("unable to authorize user: %s", gjson.GetBytes(respBody, "message").String()) - default: - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf("error connecting to server: %d %s: %s", resp.StatusCode, http.StatusText(resp.StatusCode), response) - } - - return nil -} - -func runUpdateGet(cmd *cobra.Command, args []string) (retErr error) { - hostName := args[0] - - outputFile, err := cmd.Flags().GetString("output") - if err != nil { - return err - } - displayMissing, err := cmd.Flags().GetBool("missing") - if err != nil { - return err - } - - caCert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - client, err := restClient(caCert, nil) - if err != nil { - return err - } - - var out io.Writer - if outputFile != "" { - file, err := os.Create(outputFile) - if err != nil { - return err - } - defer func() { - _ = file.Close() - if retErr != nil { - _ = os.Remove(outputFile) - } - }() - out = file - } else { - out = cmd.OutOrStdout() - } - - cmd.Println("Successfully verified Coordinator") - return cliManifestUpdateGet(out, hostName, client, displayMissing) -} - -func cliManifestUpdateGet(out io.Writer, host string, client *http.Client, displayMissing bool) error { - url := url.URL{Scheme: "https", Host: host, Path: "update-manifest"} - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url.String(), http.NoBody) - if err != nil { - return err - } - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - var response string - - if displayMissing { - msg := gjson.GetBytes(respBody, "data.message") - missingUsers := gjson.GetBytes(respBody, "data.missingUsers") - - response = fmt.Sprintf("%s\nThe following users have not yet acknowledged the update: %s\n", msg.String(), missingUsers.String()) - } else { - mnfB64 := gjson.GetBytes(respBody, "data.manifest").String() - mnf, err := base64.StdEncoding.DecodeString(mnfB64) - if err != nil { - return err - } - response = string(mnf) - } - - fmt.Fprintf(out, response) - - case http.StatusNotFound: - return fmt.Errorf("no pending update found: %s", gjson.GetBytes(respBody, "message").String()) - default: - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf("error connecting to server: %d %s: %s", resp.StatusCode, http.StatusText(resp.StatusCode), response) - } - - return nil -} diff --git a/cli/cmd/manifest_test.go b/cli/cmd/manifest_test.go deleted file mode 100644 index 0e8abbd5..00000000 --- a/cli/cmd/manifest_test.go +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "encoding/pem" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/edgelesssys/marblerun/coordinator/server" - "github.com/edgelesssys/marblerun/test" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" -) - -func TestCliManifestGet(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/manifest", r.RequestURI) - assert.Equal(http.MethodGet, r.Method) - type testResp struct { - ManifestSignature string - } - - data := testResp{ - ManifestSignature: "TestSignature", - } - - serverResp := server.GeneralResponse{ - Status: "success", - Data: data, - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - defer s.Close() - - resp, err := cliDataGet(host, "manifest", "data.ManifestSignature", []*pem.Block{cert}) - require.NoError(err) - assert.Equal("TestSignature", string(resp)) - - s.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - }) - _, err = cliDataGet(host, "manifest", "data.ManifestSignature", []*pem.Block{cert}) - require.Error(err) -} - -func TestConsolidateManifest(t *testing.T) { - assert := assert.New(t) - log := []byte(`{"time":"1970-01-01T01:00:00.0","update":"initial manifest set"} -{"time":"1970-01-01T02:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":5} -{"time":"1970-01-01T03:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":5} -{"time":"1970-01-01T04:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":8} -{"time":"1970-01-01T05:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":12}`) - - manifest, err := consolidateManifest([]byte(test.ManifestJSON), log) - assert.NoError(err) - assert.Contains(manifest, `"SecurityVersion": 12`) - assert.NotContains(manifest, `"RecoveryKeys"`) -} - -func TestDecodeManifest(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - type responseStruct struct { - Manifest []byte - } - - wrapped, err := json.Marshal(responseStruct{[]byte(test.ManifestJSON)}) - require.NoError(err) - - manifest, err := decodeManifest(false, gjson.GetBytes(wrapped, "Manifest").String(), "", nil) - assert.NoError(err) - assert.Equal(test.ManifestJSON, manifest) -} - -func TestRemoveNil(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - testMap := map[string]interface{}{ - "1": "TestValue", - "2": map[string]interface{}{ - "2.1": "TestValue", - "2.2": nil, - }, - "3": nil, - "4": map[string]interface{}{ - "4.1": map[string]interface{}{ - "4.1.1": nil, - "4.1.2": map[string]interface{}{}, - }, - }, - } - - rawMap, err := json.Marshal(testMap) - require.NoError(err) - - removeNil(testMap) - - removedMap, err := json.Marshal(testMap) - require.NoError(err) - assert.NotEqual(rawMap, removedMap) - // three should be removed since its nil - assert.NotContains(removedMap, `"3"`) - // 2.2 should be removed since its nil, but 2 stays since 2.1 is not nil - assert.NotContains(removedMap, `"2.2"`) - // 4 should be removed completly since it only contains empty maps - assert.NotContains(removedMap, `"4"`) -} - -func TestCliManifestSet(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/manifest", r.RequestURI) - assert.Equal(http.MethodPost, r.Method) - - reqData, err := ioutil.ReadAll(r.Body) - assert.NoError(err) - - if string(reqData) == "00" { - return - } - - if string(reqData) == "11" { - serverResp := server.GeneralResponse{ - Status: "success", - Data: "returned recovery secret", - } - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - return - } - - if string(reqData) == "22" { - w.WriteHeader(http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusInternalServerError) - })) - defer s.Close() - - dir, err := ioutil.TempDir("", "unittest") - require.NoError(err) - defer os.RemoveAll(dir) - - var out bytes.Buffer - - err = cliManifestSet(&out, []byte("00"), host, []*pem.Block{cert}, "") - require.NoError(err) - - err = cliManifestSet(&out, []byte("11"), host, []*pem.Block{cert}, "") - require.NoError(err) - - responseFile := filepath.Join(dir, "tmp-recovery.json") - err = cliManifestSet(&out, []byte("11"), host, []*pem.Block{cert}, responseFile) - require.NoError(err) - - err = cliManifestSet(&out, []byte("22"), host, []*pem.Block{cert}, "") - require.Error(err) - - err = cliManifestSet(&out, []byte("55"), host, []*pem.Block{cert}, "") - require.Error(err) -} - -func TestCliManifestUpdate(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/update", r.RequestURI) - assert.Equal(http.MethodPost, r.Method) - - reqData, err := ioutil.ReadAll(r.Body) - assert.NoError(err) - - if string(reqData) == "00" { - return - } - - if string(reqData) == "11" { - w.WriteHeader(http.StatusBadRequest) - return - } - - if string(reqData) == "22" { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusInternalServerError) - })) - defer s.Close() - - client, err := restClient([]*pem.Block{cert}, nil) - require.NoError(err) - - var out bytes.Buffer - - err = cliManifestUpdate(&out, []byte("00"), host, client) - require.NoError(err) - - err = cliManifestUpdate(&out, []byte("11"), host, client) - require.Error(err) - - err = cliManifestUpdate(&out, []byte("22"), host, client) - require.Error(err) - - err = cliManifestUpdate(&out, []byte("33"), host, client) - require.Error(err) -} - -func TestLoadJSON(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - tmpFile, err := ioutil.TempFile("", "unittest") - require.NoError(err) - defer os.Remove(tmpFile.Name()) - - input := []byte(` -{ - "Packages": { - "APackage": { - "SignerID": "1234", - "ProductID": 0, - "SecurityVersion": 0, - "Debug": false - } - } -} -`) - assert.True(json.Valid(input)) - _, err = tmpFile.Write(input) - require.NoError(err) - - dataJSON, err := loadManifestFile(tmpFile.Name()) - require.NoError(err) - assert.True(json.Valid(dataJSON)) -} - -func TestLoadYAML(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - tmpFile, err := ioutil.TempFile("", "unittest") - require.NoError(err) - defer os.Remove(tmpFile.Name()) - - input := []byte(` -Packages: - APackage: - Debug: false - ProductID: 0 - SecurityVersion: 0 - SignerID: "1234" -`) - assert.False(json.Valid(input)) - _, err = tmpFile.Write(input) - require.NoError(err) - - dataJSON, err := loadManifestFile(tmpFile.Name()) - require.NoError(err) - assert.True(json.Valid(dataJSON)) -} - -func TestLoadFailsOnInvalid(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - tmpFile, err := ioutil.TempFile("", "unittest") - require.NoError(err) - defer os.Remove(tmpFile.Name()) - - input := []byte(` -Invalid YAML: -This should return an error -`) - assert.False(json.Valid(input)) - _, err = tmpFile.Write(input) - require.NoError(err) - - dataJSON, err := loadManifestFile(tmpFile.Name()) - require.Error(err) - assert.False(json.Valid(dataJSON)) - - input = []byte(` -{ - "JSON": "Data", - "But its invalid", -} -`) - - assert.False(json.Valid(input)) - _, err = tmpFile.Write(input) - require.NoError(err) - - dataJSON, err = loadManifestFile(tmpFile.Name()) - require.Error(err) - assert.False(json.Valid(dataJSON)) -} - -func TestCliManifestSignature(t *testing.T) { - assert := assert.New(t) - - testValue := []byte("Test") - hash := sha256.Sum256(testValue) - signature := hex.EncodeToString(hash[:]) - assert.Equal(signature, cliManifestSignature(testValue)) -} - -func TestCliManifestVerify(t *testing.T) { - assert := assert.New(t) - - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/manifest", r.RequestURI) - assert.Equal(http.MethodGet, r.Method) - type testResp struct { - ManifestSignature string - } - - data := testResp{ - ManifestSignature: "TestSignature", - } - - serverResp := server.GeneralResponse{ - Status: "success", - Data: data, - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - defer s.Close() - - var out bytes.Buffer - - err := cliManifestVerify(&out, "TestSignature", host, []*pem.Block{cert}) - assert.NoError(err) - assert.Equal("OK\n", out.String()) - - err = cliManifestVerify(&out, "InvalidSignature", host, []*pem.Block{cert}) - assert.Error(err) -} - -func TestGetSignatureFromString(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - tmpFile, err := ioutil.TempFile("", "unittest") - require.NoError(err) - defer os.Remove(tmpFile.Name()) - - testValue := []byte("TestSignature") - hash := sha256.Sum256(testValue) - directSignature := hex.EncodeToString(hash[:]) - - _, err = tmpFile.Write(testValue) - require.NoError(err) - - testSignature1, err := getSignatureFromString(directSignature) - assert.NoError(err) - assert.Equal(directSignature, testSignature1) - - testSignature2, err := getSignatureFromString(tmpFile.Name()) - assert.NoError(err) - assert.Equal(directSignature, testSignature2) - - _, err = getSignatureFromString("invalidFilename") - assert.Error(err) -} - -func TestManifestUpdateAcknowledge(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/update-manifest", r.RequestURI) - assert.Equal(http.MethodPost, r.Method) - - serverResp := server.GeneralResponse{ - Status: "success", - Data: "acknowledgement successful", - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - defer s.Close() - - client, err := restClient([]*pem.Block{cert}, nil) - require.NoError(err) - - var out bytes.Buffer - - err = cliManifestUpdateAcknowledge(&out, []byte("manifest"), host, client) - assert.NoError(err) -} - -func TestManifestUpdateGet(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - manifest := []byte(`{"foo": "bar"}`) - - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/update-manifest", r.RequestURI) - assert.Equal(http.MethodGet, r.Method) - - serverResp := server.GeneralResponse{ - Status: "success", - Data: struct { - Manifest []byte `json:"manifest"` - Message string `json:"message"` - MissingUsers []string `json:"missingUsers"` - }{ - Manifest: manifest, - Message: "message", - MissingUsers: []string{"user1", "user2"}, - }, - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - defer s.Close() - - client, err := restClient([]*pem.Block{cert}, nil) - require.NoError(err) - - var out bytes.Buffer - - err = cliManifestUpdateGet(&out, host, client, false) - assert.NoError(err) - assert.Equal(manifest, out.Bytes()) - - out.Reset() - err = cliManifestUpdateGet(&out, host, client, true) - assert.NoError(err) - assert.True(strings.Contains(out.String(), "message")) - assert.True(strings.Contains(out.String(), "user1")) - assert.True(strings.Contains(out.String(), "user2")) -} - -func TestManifestUpdateCancel(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/update-cancel", r.RequestURI) - assert.Equal(http.MethodPost, r.Method) - - serverResp := server.GeneralResponse{ - Status: "success", - Data: "cancel successful", - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - defer s.Close() - - client, err := restClient([]*pem.Block{cert}, nil) - require.NoError(err) - - var out bytes.Buffer - - err = cliManifestUpdateCancel(&out, host, client) - assert.NoError(err) -} diff --git a/cli/cmd/recover.go b/cli/cmd/recover.go deleted file mode 100644 index 9dd1da7c..00000000 --- a/cli/cmd/recover.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "encoding/pem" - "fmt" - "io/ioutil" - "net/http" - "net/url" - - "github.com/spf13/cobra" - "github.com/tidwall/gjson" -) - -func newRecoverCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "recover ", - Short: "Recovers the MarbleRun Coordinator from a sealed state", - Long: "Recovers the MarbleRun Coordinator from a sealed state", - Example: "marblerun recover recovery_key_decrypted $MARBLERUN", - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - keyFile := args[0] - hostName := args[1] - - cert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - - // read in key - recoveryKey, err := ioutil.ReadFile(keyFile) - if err != nil { - return err - } - - cmd.Println("Successfully verified Coordinator, now uploading key") - - return cliRecover(hostName, recoveryKey, cert) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVar(&eraConfig, "era-config", "", "Path to remote attestation config file in json format, if none provided the newest configuration will be loaded from github") - cmd.Flags().BoolVarP(&insecureEra, "insecure", "i", false, "Set to skip quote verification, needed when running in simulation mode") - cmd.PersistentFlags().StringSliceVar(&acceptedTCBStatuses, "accepted-tcb-statuses", []string{"UpToDate"}, "Comma-separated list of user accepted TCB statuses (e.g. ConfigurationNeeded,ConfigurationAndSWHardeningNeeded)") - - return cmd -} - -// cliRecover tries to unseal the Coordinator by uploading the recovery key. -func cliRecover(host string, key []byte, cert []*pem.Block) error { - client, err := restClient(cert, nil) - if err != nil { - return err - } - - url := url.URL{Scheme: "https", Host: host, Path: "recover"} - resp, err := client.Post(url.String(), "text/plain", bytes.NewReader(key)) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - jsonResponse := gjson.GetBytes(respBody, "data.StatusMessage") - fmt.Printf("%s \n", jsonResponse.String()) - default: - return fmt.Errorf("error connecting to server: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - return nil -} diff --git a/cli/cmd/recover_test.go b/cli/cmd/recover_test.go deleted file mode 100644 index ce7b2349..00000000 --- a/cli/cmd/recover_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/json" - "encoding/pem" - "io/ioutil" - "net/http" - "testing" - - "github.com/edgelesssys/marblerun/coordinator/server" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCliRecover(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/recover", r.RequestURI) - assert.Equal(http.MethodPost, r.Method) - - reqData, err := ioutil.ReadAll(r.Body) - assert.NoError(err) - - type recoveryStatusResp struct { - StatusMessage string - } - - if string(reqData) == "Return Error" { - w.WriteHeader(http.StatusBadRequest) - return - } - - data := recoveryStatusResp{ - StatusMessage: "Recovery successful.", - } - - serverResp := server.GeneralResponse{ - Status: "success", - Data: data, - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - - defer s.Close() - - err := cliRecover(host, []byte{0xAA, 0xAA}, []*pem.Block{cert}) - require.NoError(err) - - err = cliRecover(host, []byte("Return Error"), []*pem.Block{cert}) - require.Error(err) -} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 8090e4db..87d86b8f 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -7,14 +7,21 @@ package cmd import ( + "context" + "os" + + "github.com/edgelesssys/marblerun/cli/internal/cmd" "github.com/spf13/cobra" ) // Execute starts the CLI. func Execute() error { - return NewRootCmd().Execute() + cobra.EnableCommandSorting = false + rootCmd := NewRootCmd() + return rootCmd.ExecuteContext(context.Background()) } +// NewRootCmd returns the root command of the CLI. func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "marblerun", @@ -26,21 +33,39 @@ To install and configure MarbleRun, run: $ marblerun install `, + PersistentPreRun: preRunRoot, } - rootCmd.AddCommand(newCertificateCmd()) - rootCmd.AddCommand(newCheckCmd()) - rootCmd.AddCommand(newCompletionCmd()) - rootCmd.AddCommand(newGraminePrepareCmd()) - rootCmd.AddCommand(newInstallCmd()) - rootCmd.AddCommand(newManifestCmd()) - rootCmd.AddCommand(newPrecheckCmd()) - rootCmd.AddCommand(newPackageInfoCmd()) - rootCmd.AddCommand(newRecoverCmd()) - rootCmd.AddCommand(newSecretCmd()) - rootCmd.AddCommand(newStatusCmd()) - rootCmd.AddCommand(newUninstallCmd()) - rootCmd.AddCommand(newVersionCmd()) + // Set output of cmd.Print to stdout. (By default, it's stderr.) + rootCmd.SetOut(os.Stdout) + + rootCmd.AddCommand(cmd.NewInstallCmd()) + rootCmd.AddCommand(cmd.NewUninstallCmd()) + rootCmd.AddCommand(cmd.NewPrecheckCmd()) + rootCmd.AddCommand(cmd.NewCheckCmd()) + rootCmd.AddCommand(cmd.NewManifestCmd()) + rootCmd.AddCommand(cmd.NewCertificateCmd()) + rootCmd.AddCommand(cmd.NewSecretCmd()) + rootCmd.AddCommand(cmd.NewStatusCmd()) + rootCmd.AddCommand(cmd.NewRecoverCmd()) + rootCmd.AddCommand(cmd.NewPackageInfoCmd()) + rootCmd.AddCommand(cmd.NewVersionCmd()) + + rootCmd.PersistentFlags().String("era-config", "", "Path to remote attestation config file in json format, if none provided the newest configuration will be loaded from github") + rootCmd.PersistentFlags().BoolP("insecure", "i", false, "Set to skip quote verification, needed when running in simulation mode") + rootCmd.PersistentFlags().StringSlice("accepted-tcb-statuses", []string{"UpToDate"}, "Comma-separated list of user accepted TCB statuses (e.g. ConfigurationNeeded,ConfigurationAndSWHardeningNeeded)") + + must(rootCmd.MarkPersistentFlagFilename("era-config", "json")) return rootCmd } + +func preRunRoot(cmd *cobra.Command, args []string) { + cmd.SilenceUsage = true +} + +func must(err error) { + if err != nil { + panic(err) + } +} diff --git a/cli/cmd/secret.go b/cli/cmd/secret.go deleted file mode 100644 index 0f94147c..00000000 --- a/cli/cmd/secret.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "github.com/spf13/cobra" -) - -var ( - userCertFile string - userKeyFile string -) - -func newSecretCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "secret", - Short: "Manages secrets for the MarbleRun Coordinator", - Long: ` -Manages secrets for the MarbleRun Coordinator. -Set or retrieve a secret defined in the manifest.`, - } - - cmd.PersistentFlags().StringVar(&eraConfig, "era-config", "", "Path to remote attestation config file in json format, if none provided the newest configuration will be loaded from github") - cmd.PersistentFlags().StringVarP(&userCertFile, "cert", "c", "", "PEM encoded MarbleRun user certificate file (required)") - cmd.PersistentFlags().StringVarP(&userKeyFile, "key", "k", "", "PEM encoded MarbleRun user key file (required)") - cmd.PersistentFlags().BoolVarP(&insecureEra, "insecure", "i", false, "Set to skip quote verification, needed when running in simulation mode") - cmd.PersistentFlags().StringSliceVar(&acceptedTCBStatuses, "accepted-tcb-statuses", []string{"UpToDate"}, "Comma-separated list of user accepted TCB statuses (e.g. ConfigurationNeeded,ConfigurationAndSWHardeningNeeded)") - cmd.MarkPersistentFlagRequired("key") - cmd.MarkPersistentFlagRequired("cert") - cmd.AddCommand(newSecretSet()) - cmd.AddCommand(newSecretGet()) - - return cmd -} diff --git a/cli/cmd/secret_test.go b/cli/cmd/secret_test.go deleted file mode 100644 index 6e5b0364..00000000 --- a/cli/cmd/secret_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "crypto/tls" - "encoding/json" - "encoding/pem" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/edgelesssys/marblerun/coordinator/manifest" - "github.com/edgelesssys/marblerun/coordinator/server" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSetSecrets(t *testing.T) { - assert := assert.New(t) - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/secrets", r.RequestURI) - assert.Equal(http.MethodPost, r.Method) - request, err := ioutil.ReadAll(r.Body) - assert.NoError(err) - - if strings.Contains(string(request), "restrictedSecret") { - w.WriteHeader(http.StatusUnauthorized) - return - } - - if strings.Contains(string(request), `"Type":"invalid"`) { - w.WriteHeader(http.StatusBadRequest) - return - } - - serverResp := server.GeneralResponse{ - Status: "success", - } - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - defer s.Close() - - err := cliSecretSet(host, []byte(`{"user_secret":{"Type":"plain","Key":"Q0xJIFRlc3QK"}}`), tls.Certificate{}, []*pem.Block{cert}) - assert.NoError(err) - - err = cliSecretSet(host, []byte(`{"restrictedSecret":{"Type":"plain","Key":"Q0xJIFRlc3QK"}}`), tls.Certificate{}, []*pem.Block{cert}) - assert.Error(err) - - err = cliSecretSet(host, []byte(`{"user_secret":{"Type":"invalid","Key":"Q0xJIFRlc3QK"}}`), tls.Certificate{}, []*pem.Block{cert}) - assert.Error(err) -} - -func TestGetSecrets(t *testing.T) { - assert := assert.New(t) - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(http.MethodGet, r.Method) - if r.RequestURI == "/secrets?s=plain_secret&s=certShared&s=secretOne" { - serverResp := server.GeneralResponse{ - Status: "success", - Data: map[string]interface{}{ - "plain_secret": map[string]interface{}{ - "Type": manifest.SecretTypePlain, - "Size": 0, - "Shared": false, - "UserDefined": true, - "Cert": nil, - "ValidFor": 0, - "Private": "base64-data", - "Public": "base64-data", - }, - "secretOne": map[string]interface{}{ - "Type": manifest.SecretTypeSymmetricKey, - "Size": 128, - "Shared": true, - "UserDefined": false, - "Cert": nil, - "ValidFor": 0, - "Private": "base64-priv-data", - "Public": "base64-priv-data", - }, - "certShared": map[string]interface{}{ - "Type": manifest.SecretTypeCertRSA, - "Size": 2048, - "Shared": true, - "UserDefined": false, - "Cert": "base64-cert-data", - "ValidFor": 14, - "Private": "base64-priv-data", - "Public": "base64-pub-data", - }, - }, - } - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - return - } - if r.RequestURI == "/secrets?s=restrictedSecret" { - w.WriteHeader(http.StatusUnauthorized) - return - } - w.WriteHeader(http.StatusBadRequest) - })) - defer s.Close() - options := &secretGetOptions{ - host: host, - secretIDs: []string{ - "plain_secret", - "certShared", - "secretOne", - }, - output: "", - clCert: tls.Certificate{}, - caCert: []*pem.Block{cert}, - } - - var out bytes.Buffer - - err := cliSecretGet(&out, options) - assert.NoError(err) - - options.secretIDs = []string{"restrictedSecret"} - err = cliSecretGet(&out, options) - assert.Error(err) - - options.secretIDs = []string{"this should cause an error"} - err = cliSecretGet(&out, options) - assert.Error(err) -} - -func TestSecretFromPEM(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - const testCert = ` ------BEGIN CERTIFICATE----- -MIICpjCCAg+gAwIBAgIUS5FDU/DJnN3hDISm2eAu7hVWqSUwDQYJKoZIhvcNAQEL -BQAwZTELMAkGA1UEBhMCREUxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM -EEVkZ2VsZXNzIFN5c3RlbXMxEjAQBgNVBAsMCVVuaXQgVGVzdDESMBAGA1UEAwwJ -VW5pdCBUZXN0MB4XDTIxMDYyMzA3NTAxMVoXDTIyMDYyMzA3NTAxMVowZTELMAkG -A1UEBhMCREUxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoMEEVkZ2VsZXNz -IFN5c3RlbXMxEjAQBgNVBAsMCVVuaXQgVGVzdDESMBAGA1UEAwwJVW5pdCBUZXN0 -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDO+ZG7pGx+/poBhr5Zt2lX0+Nh -kcWpYbWdbt69tJXhSWYlZmLeo6FJbeV11bX8zEwVPaDxhYSmlDq2tu9t8o1j8N01 -FAoWy4NnDyGEyx1bJGyGGcMN01mVqD+PTbmKeOuVGYchyz8YBub+k5Eft9l6MxuN -kA7SuJGv9fU3lTpQpQIDAQABo1MwUTAdBgNVHQ4EFgQUmD/6vklf6UsdUcZvOB2x -FeymJU0wHwYDVR0jBBgwFoAUmD/6vklf6UsdUcZvOB2xFeymJU0wDwYDVR0TAQH/ -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCxHFf2dQ+6O/ntEQr6zbHgU4jMidM+ -foF2RSiG5icffjDcjpxttJtpIK+iGh3yguGfWaaMVo72DPFPNAVmqHutoEr80chV -yr93zz66XkRPyMhopTeF3Ld1K3qAQ0CqtWck1kblgHCWJBGYgyngawoxSGhUMkSD -i6zr19jszrNxzg== ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAM75kbukbH7+mgGG -vlm3aVfT42GRxalhtZ1u3r20leFJZiVmYt6joUlt5XXVtfzMTBU9oPGFhKaUOra2 -723yjWPw3TUUChbLg2cPIYTLHVskbIYZww3TWZWoP49NuYp465UZhyHLPxgG5v6T -kR+32XozG42QDtK4ka/19TeVOlClAgMBAAECgYEAyEX3vUEJ9wx3ixiN4hQ2q9SN -BiFeyVqRuSfKAnjWOquiWngrHVHqRDpBuXa05UvuJvN+Y5YV2HZAJgL3xUTZh+jV -sBgj65evWTUE3daVJBPoTDtBRmZCoEXNvonXbUNFExUwWfDaYOraZCSupP9Yg/0q -m1To7ktkWmS84JuVukECQQDmbVibBLYqIClFsEdNVuVjAq0OHHSsN5FEyD3joQso -JZ5EmCUnp/GvJ+yDgOyKY/gOVK9s9BYKEd7WQQBQ3Vs1AkEA5fHsHtYryPMTl3s7 -aycxjEEJyvpDr3y1Pk5tSGdj2YSTvKdkVYP3pJmA0JaCRL/2rqJx3pKuOm1/kOS8 -71xdsQJBAJAbLmC0T6CEwIr+tXjesVJ8Z/H9RdI2ZjlX6aykGLAg5pwLcqEcXP+n -vjh3tnbOEmIUACnpdKcTigMAX8wyw0kCQBpYpro9xdSHbWY822kCm527UfjsxdaU -jluuNr1GA13H3/mMoGVf8n7si6Laq+Besk/+EtfyrH3LUAN1AeTXC3ECQQCua5+L -Ra6Yym8Tq+6I6YFqee2NFPKrsKw2xrExhHjx/vv+V0SMXU/zBfZudCbPUcLQoH3q -LuL049+D8bu8Z+Fe ------END PRIVATE KEY-----` - - secretName := "test-secret" - secret, err := loadSecretFromPEM(secretName, []byte(testCert)) - assert.NoError(err) - - var secretMap map[string]manifest.Secret - err = json.Unmarshal(secret, &secretMap) - require.NoError(err) - - _, ok := secretMap[secretName] - require.True(ok) - assert.True(len(secretMap[secretName].Cert.Raw) > 0) - assert.True(len(secretMap[secretName].Private) > 0) - assert.True(len(secretMap[secretName].Public) == 0) - - // no error here since we stop after finding the first cert-key pair - _, err = loadSecretFromPEM(secretName, []byte(testCert+"\n-----BEGIN MESSAGE-----\ndGVzdA==\n-----END MESSAGE-----")) - assert.NoError(err) - // error since the first pem block contains an invalid type - _, err = loadSecretFromPEM(secretName, []byte("-----BEGIN MESSAGE-----\ndGVzdA==\n-----END MESSAGE-----\n"+testCert)) - assert.Error(err) - _, err = loadSecretFromPEM(secretName, []byte("no PEM data here")) - assert.Error(err) -} diff --git a/cli/cmd/status.go b/cli/cmd/status.go deleted file mode 100644 index 914cc8a3..00000000 --- a/cli/cmd/status.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/json" - "encoding/pem" - "fmt" - "io/ioutil" - "net/http" - - "github.com/spf13/cobra" - "github.com/tidwall/gjson" -) - -const statusDesc = ` -This command provides information about the currently running MarbleRun Coordinator. -Information is obtained from the /status endpoint of the Coordinators REST API. - -The Coordinator will be in one of these 4 states: - 0 recovery mode: Found a sealed state of an old seal key. Waiting for user input on /recovery. - The Coordinator is currently sealed, it can be recovered using the [marblerun recover] command. - - 1 uninitialized: Fresh start, initializing the Coordinator. - The Coordinator is in its starting phase. - - 2 waiting for manifest: Waiting for user input on /manifest. - Send a manifest to the Coordinator using [marblerun manifest set] to start. - - 3 accepting marble: The Coordinator is running, you can add marbles to the mesh or update the - manifest using [marblerun manifest update]. -` - -type statusResponse struct { - StatusCode int `json:"StatusCode"` - StatusMessage string `json:"StatusMessage"` -} - -func newStatusCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "status ", - Short: "Gives information about the status of the MarbleRun Coordinator", - Long: statusDesc, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostname := args[0] - cert, err := verifyCoordinator(cmd.OutOrStdout(), hostname, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - return cliStatus(hostname, cert) - }, - SilenceUsage: true, - } - - cmd.Flags().StringVar(&eraConfig, "era-config", "", "Path to remote attestation config file in json format, if none provided the newest configuration will be loaded from github") - cmd.Flags().BoolVarP(&insecureEra, "insecure", "i", false, "Set to skip quote verification, needed when running in simulation mode") - cmd.PersistentFlags().StringSliceVar(&acceptedTCBStatuses, "accepted-tcb-statuses", []string{"UpToDate"}, "Comma-separated list of user accepted TCB statuses (e.g. ConfigurationNeeded,ConfigurationAndSWHardeningNeeded)") - - return cmd -} - -// cliStatus requests the current status of the Coordinator. -func cliStatus(host string, cert []*pem.Block) error { - client, err := restClient(cert, nil) - if err != nil { - return err - } - - resp, err := client.Get("https://" + host + "/status") - if err != nil { - return err - } - - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusOK: - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - jsonResponse := gjson.GetBytes(respBody, "data") - var statusResp statusResponse - if err := json.Unmarshal([]byte(jsonResponse.String()), &statusResp); err != nil { - return err - } - fmt.Printf("%d: %s\n", statusResp.StatusCode, statusResp.StatusMessage) - default: - return fmt.Errorf("error connecting to server: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - return nil -} diff --git a/cli/cmd/status_test.go b/cli/cmd/status_test.go deleted file mode 100644 index 1598c007..00000000 --- a/cli/cmd/status_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "encoding/json" - "encoding/pem" - "net/http" - "testing" - - "github.com/edgelesssys/marblerun/coordinator/server" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestStatus(t *testing.T) { - require := require.New(t) - assert := assert.New(t) - - s, host, cert := newTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal("/status", r.RequestURI) - - resp := statusResponse{ - StatusCode: 1, - StatusMessage: "Test Server waiting", - } - - serverResp := server.GeneralResponse{ - Status: "success", - Data: resp, - } - - assert.NoError(json.NewEncoder(w).Encode(serverResp)) - })) - - defer s.Close() - - err := cliStatus(host, []*pem.Block{cert}) - require.NoError(err) - - s.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - }) - err = cliStatus(host, []*pem.Block{cert}) - require.Error(err) -} diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go deleted file mode 100644 index 5b04524a..00000000 --- a/cli/cmd/uninstall.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "context" - "fmt" - "os" - - "github.com/spf13/cobra" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/cli" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -func newUninstallCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "uninstall", - Short: "Removes MarbleRun from a Kubernetes cluster", - Long: `Removes MarbleRun from a Kubernetes cluster`, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - settings := cli.New() - kubeClient, err := getKubernetesInterface() - if err != nil { - return fmt.Errorf("failed setting up kubernetes client: %v", err) - } - return cliUninstall(settings, kubeClient) - }, - SilenceUsage: true, - } - - return cmd -} - -// cliUninstall uninstalls MarbleRun. -func cliUninstall(settings *cli.EnvSettings, kubeClient kubernetes.Interface) error { - if err := removeHelmRelease(settings); err != nil { - return err - } - - // If we get a "not found" error the resource was already removed / never created - // and we can continue on without a problem - err := cleanupSecrets(kubeClient) - if err != nil && !errors.IsNotFound(err) { - return err - } - - err = cleanupCSR(kubeClient) - if err != nil && !errors.IsNotFound(err) { - return err - } - - fmt.Println("MarbleRun successfully removed from your cluster") - - return nil -} - -// removeHelmRelease removes kubernetes resources installed using helm. -func removeHelmRelease(settings *cli.EnvSettings) error { - actionConfig := new(action.Configuration) - if err := actionConfig.Init(settings.RESTClientGetter(), helmNamespace, os.Getenv("HELM_DRIVER"), debug); err != nil { - return err - } - - uninstallAction := action.NewUninstall(actionConfig) - _, err := uninstallAction.Run(helmRelease) - - return err -} - -// cleanupSecrets removes secretes set for the Admission Controller. -func cleanupSecrets(kubeClient kubernetes.Interface) error { - return kubeClient.CoreV1().Secrets(helmNamespace).Delete(context.TODO(), "marble-injector-webhook-certs", metav1.DeleteOptions{}) -} - -// cleanupCSR removes a potentially leftover CSR from the Admission Controller. -func cleanupCSR(kubeClient kubernetes.Interface) error { - // in case of kubernetes version < 1.19 no CSR was created by the install command - isLegacy, err := checkLegacyKubernetesVersion(kubeClient) - if err != nil { - return err - } - if isLegacy { - return nil - } - - return kubeClient.CertificatesV1().CertificateSigningRequests().Delete(context.TODO(), webhookName, metav1.DeleteOptions{}) -} diff --git a/cli/cmd/util.go b/cli/cmd/util.go deleted file mode 100644 index 2eea8ef7..00000000 --- a/cli/cmd/util.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bufio" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "io" - "net" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/edgelesssys/ego/attestation" - "github.com/edgelesssys/era/era" - "github.com/edgelesssys/era/util" - "k8s.io/apimachinery/pkg/util/version" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" -) - -const webhookName = "marble-injector.marblerun" - -// helm constants. -const ( - helmChartName = "edgeless/marblerun" - helmChartNameEnterprise = "edgeless/marblerun-enterprise" - helmCoordinatorDeployment = "marblerun-coordinator" - helmInjectorDeployment = "marble-injector" - helmNamespace = "marblerun" - helmRelease = "marblerun" - helmRepoURI = "https://helm.edgeless.systems/stable" - helmRepoName = "edgeless" -) - -const promptForChanges = "Do you want to automatically apply the suggested changes [y/n]? " - -const eraDefaultConfig = "era-config.json" - -var ( - eraConfig string - insecureEra bool - acceptedTCBStatuses []string -) - -func fetchLatestCoordinatorConfiguration(out io.Writer) error { - coordinatorVersion, err := getCoordinatorVersion() - eraURL := fmt.Sprintf("https://github.com/edgelesssys/marblerun/releases/download/%s/coordinator-era.json", coordinatorVersion) - if err != nil { - // if errors were caused by an empty kube config file or by being unable to connect to a cluster we assume the Coordinator is running as a standalone - // and we default to the latest era-config file - var dnsError *net.DNSError - if !clientcmd.IsEmptyConfig(err) && !errors.As(err, &dnsError) && !os.IsNotExist(err) { - return err - } - eraURL = "https://github.com/edgelesssys/marblerun/releases/latest/download/coordinator-era.json" - } - - fmt.Fprintf(out, "No era config file specified, getting config from %s\n", eraURL) - resp, err := http.Get(eraURL) - if err != nil { - return fmt.Errorf("downloading era config for version %s: %w", coordinatorVersion, err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("downloading era config for version: %s: %d: %s", coordinatorVersion, resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - era, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("downloading era config for version %s: %w", coordinatorVersion, err) - } - - if err := os.WriteFile(eraDefaultConfig, era, 0o644); err != nil { - return fmt.Errorf("writing era config file: %w", err) - } - - fmt.Fprintf(out, "Got era config for version %s\n", coordinatorVersion) - return nil -} - -// verify the connection to the MarbleRun Coordinator. -func verifyCoordinator(out io.Writer, host, configFilename string, insecure bool, acceptedTCBStatuses []string) ([]*pem.Block, error) { - // skip verification if specified - if insecure { - fmt.Fprintln(out, "Warning: skipping quote verification") - return era.InsecureGetCertificate(host) - } - - if configFilename == "" { - configFilename = eraDefaultConfig - - // reuse existing config from current working directory if none specified - // or try to get latest config from github if it does not exist - if _, err := os.Stat(configFilename); err == nil { - fmt.Fprintln(out, "Reusing existing config file") - } else if err := fetchLatestCoordinatorConfiguration(out); err != nil { - return nil, err - } - } - - pemBlock, tcbStatus, err := era.GetCertificate(host, configFilename) - if errors.Is(err, attestation.ErrTCBLevelInvalid) && util.StringSliceContains(acceptedTCBStatuses, tcbStatus.String()) { - fmt.Fprintln(out, "Warning: TCB level invalid, but accepted by configuration") - return pemBlock, nil - } - return pemBlock, err -} - -// restClient creates and returns a http client using a provided root certificate and optional client certificate to communicate with the Coordinator REST API. -func restClient(caCert []*pem.Block, clCert *tls.Certificate) (*http.Client, error) { - // Set rootCA for connection to Coordinator - certPool := x509.NewCertPool() - if ok := certPool.AppendCertsFromPEM(pem.EncodeToMemory(caCert[len(caCert)-1])); !ok { - return nil, errors.New("failed to parse certificate") - } - // Add intermediate cert if applicable - if len(caCert) > 1 { - if ok := certPool.AppendCertsFromPEM(pem.EncodeToMemory(caCert[0])); !ok { - return nil, errors.New("failed to parse certificate") - } - } - - var tlsConfig *tls.Config - if clCert != nil { - tlsConfig = &tls.Config{ - RootCAs: certPool, - Certificates: []tls.Certificate{*clCert}, - } - } else { - tlsConfig = &tls.Config{ - RootCAs: certPool, - } - } - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConfig, - }, - } - return client, nil -} - -// getKubernetesInterface returns the kubernetes Clientset to interact with the k8s API. -func getKubernetesInterface() (*kubernetes.Clientset, error) { - path := os.Getenv(clientcmd.RecommendedConfigPathEnvVar) - if path == "" { - homedir, err := os.UserHomeDir() - if err != nil { - return nil, err - } - path = filepath.Join(homedir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName) - } - - kubeConfig, err := clientcmd.BuildConfigFromFlags("", path) - if err != nil { - return nil, err - } - - kubeClient, err := kubernetes.NewForConfig(kubeConfig) - if err != nil { - return nil, fmt.Errorf("failed setting up kubernetes client: %w", err) - } - - return kubeClient, nil -} - -func promptYesNo(stdin io.Reader, question string) (bool, error) { - fmt.Print(question) - reader := bufio.NewReader(stdin) - response, err := reader.ReadString('\n') - if err != nil { - return false, err - } - - response = strings.ToLower(strings.TrimSpace(response)) - - if response != "y" && response != "yes" { - return false, nil - } - - return true, nil -} - -func checkLegacyKubernetesVersion(kubeClient kubernetes.Interface) (bool, error) { - serverVersion, err := kubeClient.Discovery().ServerVersion() - if err != nil { - return false, err - } - versionInfo, err := version.ParseGeneric(serverVersion.String()) - if err != nil { - return false, err - } - - // return the legacy if kubernetes version is < 1.19 - if versionInfo.Major() == 1 && versionInfo.Minor() < 19 { - return true, nil - } - - return false, nil -} diff --git a/cli/cmd/util_test.go b/cli/cmd/util_test.go deleted file mode 100644 index 0fefca17..00000000 --- a/cli/cmd/util_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "bytes" - "encoding/pem" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func newTestServer(handler http.Handler) (server *httptest.Server, addr string, cert *pem.Block) { - s := httptest.NewTLSServer(handler) - return s, s.Listener.Addr().String(), &pem.Block{Type: "CERTIFICATE", Bytes: s.Certificate().Raw} -} - -func TestPromptYesNo(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - var stdin bytes.Buffer - - stdin.Write([]byte("y\n")) - approved, err := promptYesNo(&stdin, promptForChanges) - require.NoError(err) - assert.True(approved) - - // Typos are intentional to test if strings are lowercased later correctly - stdin.Reset() - stdin.Write([]byte("yEs\n")) - approved, err = promptYesNo(&stdin, promptForChanges) - require.NoError(err) - assert.True(approved) - - stdin.Reset() - stdin.Write([]byte("n\n")) - approved, err = promptYesNo(&stdin, promptForChanges) - require.NoError(err) - assert.False(approved) - - stdin.Reset() - stdin.Write([]byte("nO\n")) - approved, err = promptYesNo(&stdin, promptForChanges) - require.NoError(err) - assert.False(approved) - - stdin.Reset() - stdin.Write([]byte("ja\n")) - approved, err = promptYesNo(&stdin, promptForChanges) - require.NoError(err) - assert.False(approved) -} diff --git a/cli/cmd/version.go b/cli/cmd/version.go deleted file mode 100644 index 319b7dcc..00000000 --- a/cli/cmd/version.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Edgeless Systems GmbH. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package cmd - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Version is the CLI version. -var Version = "0.0.0" // Don't touch! Automatically injected at build-time. - -// GitCommit is the git commit hash. -var GitCommit = "0000000000000000000000000000000000000000" // Don't touch! Automatically injected at build-time. - -func newVersionCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "version", - Short: "Display version of this CLI and (if running) the MarbleRun Coordinator", - Long: `Display version of this CLI and (if running) the MarbleRun Coordinator`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("CLI Version: v%s \nCommit: %s\n", Version, GitCommit) - - cVersion, err := getCoordinatorVersion() - if err != nil { - fmt.Println("Unable to find MarbleRun Coordinator") - return - } - fmt.Printf("Coordinator Version: %s\n", cVersion) - }, - SilenceUsage: true, - } - - return cmd -} - -func getCoordinatorVersion() (string, error) { - kubeClient, err := getKubernetesInterface() - if err != nil { - return "", err - } - - coordinatorDeployment, err := kubeClient.AppsV1().Deployments(helmNamespace).Get(context.TODO(), helmCoordinatorDeployment, metav1.GetOptions{}) - if err != nil { - return "", err - } - - version := coordinatorDeployment.Labels["app.kubernetes.io/version"] - if len(version) <= 0 { - return "", fmt.Errorf("deployment has no label [app.kubernetes.io/version]") - } - return version, nil -} diff --git a/cli/cmd/certificate.go b/cli/internal/cmd/certificate.go similarity index 50% rename from cli/cmd/certificate.go rename to cli/internal/cmd/certificate.go index dd46aec3..572f027e 100644 --- a/cli/cmd/certificate.go +++ b/cli/internal/cmd/certificate.go @@ -7,22 +7,32 @@ package cmd import ( + "errors" + "github.com/spf13/cobra" ) -func newCertificateCmd() *cobra.Command { +func NewCertificateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "certificate", Short: "Retrieves the certificate of the MarbleRun Coordinator", Long: `Retrieves the certificate of the MarbleRun Coordinator`, } - cmd.PersistentFlags().StringVar(&eraConfig, "era-config", "", "Path to remote attestation config file in json format, if none provided the newest configuration will be loaded from github") - cmd.PersistentFlags().BoolVarP(&insecureEra, "insecure", "i", false, "Set to skip quote verification, needed when running in simulation mode") - cmd.PersistentFlags().StringSliceVar(&acceptedTCBStatuses, "accepted-tcb-statuses", []string{"UpToDate"}, "Comma-separated list of user accepted TCB statuses (e.g. ConfigurationNeeded,ConfigurationAndSWHardeningNeeded)") cmd.AddCommand(newCertificateRoot()) cmd.AddCommand(newCertificateIntermediate()) cmd.AddCommand(newCertificateChain()) return cmd } + +func outputFlagNotEmpty(cmd *cobra.Command, args []string) error { + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + if output == "" { + return errors.New("output flag must not be empty") + } + return nil +} diff --git a/cli/internal/cmd/certificateChain.go b/cli/internal/cmd/certificateChain.go new file mode 100644 index 00000000..b803b97c --- /dev/null +++ b/cli/internal/cmd/certificateChain.go @@ -0,0 +1,77 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "encoding/pem" + "errors" + "fmt" + "io" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newCertificateChain() *cobra.Command { + cmd := &cobra.Command{ + Use: "chain ", + Short: "Returns the certificate chain of the MarbleRun Coordinator", + Long: `Returns the certificate chain of the MarbleRun Coordinator`, + Args: cobra.ExactArgs(1), + RunE: runCertificateChain, + PreRunE: outputFlagNotEmpty, + } + + cmd.Flags().StringP("output", "o", "marblerunChainCA.crt", "File to save the certificate to") + + return cmd +} + +func runCertificateChain(cmd *cobra.Command, args []string) error { + hostname := args[0] + flags, err := rest.ParseFlags(cmd) + if err != nil { + return err + } + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + certs, err := rest.VerifyCoordinator( + cmd.Context(), cmd.OutOrStdout(), hostname, + flags.EraConfig, flags.Insecure, flags.AcceptedTCBStatuses, + ) + if err != nil { + return fmt.Errorf("retrieving certificate chain from Coordinator: %w", err) + } + return cliCertificateChain(cmd.OutOrStdout(), file.New(output, afero.NewOsFs()), certs) +} + +// cliCertificateChain gets the certificate chain of the MarbleRun Coordinator. +func cliCertificateChain(out io.Writer, file *file.Handler, certs []*pem.Block) error { + if len(certs) == 0 { + return errors.New("no certificates received from Coordinator") + } + if len(certs) == 1 { + fmt.Fprintln(out, "WARNING: Only received root certificate from Coordinator") + } + + var chain []byte + for _, cert := range certs { + chain = append(chain, pem.EncodeToMemory(cert)...) + } + + if err := file.Write(chain); err != nil { + return err + } + fmt.Fprintf(out, "Certificate chain written to %s\n", file.Name()) + + return nil +} diff --git a/cli/internal/cmd/certificateIntermediate.go b/cli/internal/cmd/certificateIntermediate.go new file mode 100644 index 00000000..fdb3d3ce --- /dev/null +++ b/cli/internal/cmd/certificateIntermediate.go @@ -0,0 +1,68 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "encoding/pem" + "errors" + "fmt" + "io" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newCertificateIntermediate() *cobra.Command { + cmd := &cobra.Command{ + Use: "intermediate ", + Short: "Returns the intermediate certificate of the MarbleRun Coordinator", + Long: `Returns the intermediate certificate of the MarbleRun Coordinator`, + Args: cobra.ExactArgs(1), + RunE: runCertificateIntermediate, + PreRunE: outputFlagNotEmpty, + } + + cmd.Flags().StringP("output", "o", "marblerunIntermediateCA.crt", "File to save the certificate to") + + return cmd +} + +func runCertificateIntermediate(cmd *cobra.Command, args []string) error { + hostname := args[0] + flags, err := rest.ParseFlags(cmd) + if err != nil { + return err + } + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + certs, err := rest.VerifyCoordinator( + cmd.Context(), cmd.OutOrStdout(), hostname, + flags.EraConfig, flags.Insecure, flags.AcceptedTCBStatuses, + ) + if err != nil { + return fmt.Errorf("retrieving intermediate certificate from Coordinator: %w", err) + } + return cliCertificateIntermediate(cmd.OutOrStdout(), file.New(output, afero.NewOsFs()), certs) +} + +// cliCertificateIntermediate gets the intermediate certificate of the MarbleRun Coordinator. +func cliCertificateIntermediate(out io.Writer, file *file.Handler, certs []*pem.Block) error { + if len(certs) < 2 { + return errors.New("no intermediate certificate received from Coordinator") + } + if err := file.Write(pem.EncodeToMemory(certs[0])); err != nil { + return err + } + fmt.Fprintf(out, "Intermediate certificate written to %s\n", file.Name()) + + return nil +} diff --git a/cli/internal/cmd/certificateRoot.go b/cli/internal/cmd/certificateRoot.go new file mode 100644 index 00000000..49701fb6 --- /dev/null +++ b/cli/internal/cmd/certificateRoot.go @@ -0,0 +1,68 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "encoding/pem" + "errors" + "fmt" + "io" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newCertificateRoot() *cobra.Command { + cmd := &cobra.Command{ + Use: "root ", + Short: "Returns the root certificate of the MarbleRun Coordinator", + Long: `Returns the root certificate of the MarbleRun Coordinator`, + Args: cobra.ExactArgs(1), + RunE: runCertificateRoot, + PreRunE: outputFlagNotEmpty, + } + + cmd.Flags().StringP("output", "o", "marblerunRootCA.crt", "File to save the certificate to") + + return cmd +} + +func runCertificateRoot(cmd *cobra.Command, args []string) error { + hostname := args[0] + flags, err := rest.ParseFlags(cmd) + if err != nil { + return err + } + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + certs, err := rest.VerifyCoordinator( + cmd.Context(), cmd.OutOrStdout(), hostname, + flags.EraConfig, flags.Insecure, flags.AcceptedTCBStatuses, + ) + if err != nil { + return fmt.Errorf("retrieving root certificate from Coordinator: %w", err) + } + return cliCertificateRoot(cmd.OutOrStdout(), file.New(output, afero.NewOsFs()), certs) +} + +// cliCertificateRoot gets the root certificate of the MarbleRun Coordinator and saves it to a file. +func cliCertificateRoot(out io.Writer, file *file.Handler, certs []*pem.Block) error { + if len(certs) == 0 { + return errors.New("no certificates received from Coordinator") + } + if err := file.Write(pem.EncodeToMemory(certs[len(certs)-1])); err != nil { + return err + } + fmt.Fprintf(out, "Root certificate written to %s\n", file.Name()) + + return nil +} diff --git a/cli/internal/cmd/certificate_test.go b/cli/internal/cmd/certificate_test.go new file mode 100644 index 00000000..b6c464d4 --- /dev/null +++ b/cli/internal/cmd/certificate_test.go @@ -0,0 +1,224 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "encoding/pem" + "testing" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOutputFlagNotEmpty(t *testing.T) { + testCases := map[string]struct { + cmd *cobra.Command + wantErr bool + }{ + "flag not defined": { + cmd: &cobra.Command{}, + wantErr: true, + }, + "flag empty": { + cmd: func() *cobra.Command { + cmd := &cobra.Command{} + cmd.Flags().String("output", "", "") + return cmd + }(), + wantErr: true, + }, + "flag not empty": { + cmd: func() *cobra.Command { + cmd := &cobra.Command{} + cmd.Flags().String("output", "foo", "") + return cmd + }(), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + err := outputFlagNotEmpty(tc.cmd, nil) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + +func TestCertificateRoot(t *testing.T) { + testCases := map[string]struct { + file *file.Handler + certs []*pem.Block + wantErr bool + }{ + "no certs": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{}, + wantErr: true, + }, + "one cert": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{ + { + Type: "CERTIFICATE", + Bytes: []byte("ROOT CERTIFICATE"), + }, + }, + }, + "multiple certs": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{ + { + Type: "CERTIFICATE", + Bytes: []byte("INTERMEDIATE CERTIFICATE"), + }, + { + Type: "CERTIFICATE", + Bytes: []byte("ROOT CERTIFICATE"), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var out bytes.Buffer + + err := cliCertificateRoot(&out, tc.file, tc.certs) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + writtenCert, err := tc.file.Read() + require.NoError(t, err) + assert.Equal(pem.EncodeToMemory(tc.certs[len(tc.certs)-1]), writtenCert) + }) + } +} + +func TestCertificateIntermediate(t *testing.T) { + testCases := map[string]struct { + file *file.Handler + certs []*pem.Block + wantErr bool + }{ + "no certs": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{}, + wantErr: true, + }, + "one cert": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{ + { + Type: "CERTIFICATE", + Bytes: []byte("ROOT CERTIFICATE"), + }, + }, + wantErr: true, + }, + "multiple certs": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{ + { + Type: "CERTIFICATE", + Bytes: []byte("INTERMEDIATE CERTIFICATE"), + }, + { + Type: "CERTIFICATE", + Bytes: []byte("ROOT CERTIFICATE"), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var out bytes.Buffer + + err := cliCertificateIntermediate(&out, tc.file, tc.certs) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + writtenCert, err := tc.file.Read() + require.NoError(t, err) + assert.Equal(pem.EncodeToMemory(tc.certs[0]), writtenCert) + }) + } +} + +func TestCertificateChain(t *testing.T) { + testCases := map[string]struct { + file *file.Handler + certs []*pem.Block + wantErr bool + }{ + "no certs": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{}, + wantErr: true, + }, + "one cert": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{ + { + Type: "CERTIFICATE", + Bytes: []byte("ROOT CERTIFICATE"), + }, + }, + }, + "multiple certs": { + file: file.New("unit-test", afero.NewMemMapFs()), + certs: []*pem.Block{ + { + Type: "CERTIFICATE", + Bytes: []byte("INTERMEDIATE CERTIFICATE"), + }, + { + Type: "CERTIFICATE", + Bytes: []byte("ROOT CERTIFICATE"), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var out bytes.Buffer + + err := cliCertificateChain(&out, tc.file, tc.certs) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + writtenCert, err := tc.file.Read() + require.NoError(t, err) + for _, cert := range tc.certs { + assert.Contains(string(writtenCert), string(pem.EncodeToMemory(cert))) + } + }) + } +} diff --git a/cli/cmd/check.go b/cli/internal/cmd/check.go similarity index 89% rename from cli/cmd/check.go rename to cli/internal/cmd/check.go index 8ab86c00..5d0133c0 100644 --- a/cli/cmd/check.go +++ b/cli/internal/cmd/check.go @@ -12,13 +12,15 @@ import ( "os" "time" + "github.com/edgelesssys/marblerun/cli/internal/helm" + "github.com/edgelesssys/marblerun/cli/internal/kube" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) -func newCheckCmd() *cobra.Command { +func NewCheckCmd() *cobra.Command { var timeout uint cmd := &cobra.Command{ @@ -27,13 +29,12 @@ func newCheckCmd() *cobra.Command { Long: `Check the status of MarbleRun's control plane`, Args: cobra.NoArgs, RunE: func(cobracmd *cobra.Command, args []string) error { - kubeClient, err := getKubernetesInterface() + kubeClient, err := kube.NewClient() if err != nil { return err } return cliCheck(kubeClient, timeout) }, - SilenceUsage: true, } cmd.Flags().UintVar(&timeout, "timeout", 60, "Time to wait before aborting in seconds") @@ -42,11 +43,11 @@ func newCheckCmd() *cobra.Command { // cliCheck if MarbleRun control-plane deployments are ready to use. func cliCheck(kubeClient kubernetes.Interface, timeout uint) error { - if err := checkDeploymentStatus(kubeClient, helmInjectorDeployment, helmNamespace, timeout); err != nil { + if err := checkDeploymentStatus(kubeClient, helm.InjectorDeployment, helm.Namespace, timeout); err != nil { return err } - if err := checkDeploymentStatus(kubeClient, helmCoordinatorDeployment, helmNamespace, timeout); err != nil { + if err := checkDeploymentStatus(kubeClient, helm.CoordinatorDeployment, helm.Namespace, timeout); err != nil { return err } diff --git a/cli/cmd/check_test.go b/cli/internal/cmd/check_test.go similarity index 65% rename from cli/cmd/check_test.go rename to cli/internal/cmd/check_test.go index aa09ce1e..a267d734 100644 --- a/cli/cmd/check_test.go +++ b/cli/internal/cmd/check_test.go @@ -11,6 +11,7 @@ import ( "fmt" "testing" + "github.com/edgelesssys/marblerun/cli/internal/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -23,14 +24,14 @@ func TestDeploymentIsReady(t *testing.T) { assert := assert.New(t) testClient := fake.NewSimpleClientset() - _, _, err := deploymentIsReady(testClient, helmCoordinatorDeployment, helmNamespace) + _, _, err := deploymentIsReady(testClient, helm.CoordinatorDeployment, helm.Namespace) require.Error(err) // create fake deployment with one non ready replica // create a fake deployment with 1/1 available replicas testDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: helmCoordinatorDeployment, + Name: helm.CoordinatorDeployment, }, Status: appsv1.DeploymentStatus{ Replicas: 1, @@ -38,20 +39,20 @@ func TestDeploymentIsReady(t *testing.T) { }, } - _, err = testClient.AppsV1().Deployments(helmNamespace).Create(context.TODO(), testDeployment, metav1.CreateOptions{}) + _, err = testClient.AppsV1().Deployments(helm.Namespace).Create(context.TODO(), testDeployment, metav1.CreateOptions{}) require.NoError(err) - ready, status, err := deploymentIsReady(testClient, helmCoordinatorDeployment, helmNamespace) + ready, status, err := deploymentIsReady(testClient, helm.CoordinatorDeployment, helm.Namespace) require.NoError(err) assert.False(ready, "function returned true when deployment was not ready") assert.Equal("0/1", status, fmt.Sprintf("expected 0/1 ready pods but got %s", status)) testDeployment.Status.UnavailableReplicas = 0 testDeployment.Status.AvailableReplicas = 1 - _, err = testClient.AppsV1().Deployments(helmNamespace).UpdateStatus(context.TODO(), testDeployment, metav1.UpdateOptions{}) + _, err = testClient.AppsV1().Deployments(helm.Namespace).UpdateStatus(context.TODO(), testDeployment, metav1.UpdateOptions{}) require.NoError(err) - ready, status, err = deploymentIsReady(testClient, helmCoordinatorDeployment, helmNamespace) + ready, status, err = deploymentIsReady(testClient, helm.CoordinatorDeployment, helm.Namespace) require.NoError(err) assert.True(ready, "function returned false when deployment was ready") assert.Equal("1/1", status, fmt.Sprintf("expected 1/1 ready pods but got %s", status)) @@ -63,23 +64,23 @@ func TestCheckDeploymentStatus(t *testing.T) { testClient := fake.NewSimpleClientset() // try without any deployments - err := checkDeploymentStatus(testClient, helmCoordinatorDeployment, helmNamespace, 10) + err := checkDeploymentStatus(testClient, helm.CoordinatorDeployment, helm.Namespace, 10) assert.NoError(err) // create a fake deployment with 1/1 available replicas testDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: helmCoordinatorDeployment, + Name: helm.CoordinatorDeployment, }, Status: appsv1.DeploymentStatus{ Replicas: 1, AvailableReplicas: 1, }, } - _, err = testClient.AppsV1().Deployments(helmNamespace).Create(context.TODO(), testDeployment, metav1.CreateOptions{}) + _, err = testClient.AppsV1().Deployments(helm.Namespace).Create(context.TODO(), testDeployment, metav1.CreateOptions{}) require.NoError(err) - err = checkDeploymentStatus(testClient, helmCoordinatorDeployment, helmNamespace, 10) + err = checkDeploymentStatus(testClient, helm.CoordinatorDeployment, helm.Namespace, 10) assert.NoError(err) } @@ -95,32 +96,32 @@ func TestCliCheck(t *testing.T) { // create a fake deployment with 1/1 available replicas testDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: helmCoordinatorDeployment, + Name: helm.CoordinatorDeployment, }, Status: appsv1.DeploymentStatus{ Replicas: 1, AvailableReplicas: 1, }, } - _, err = testClient.AppsV1().Deployments(helmNamespace).Create(context.TODO(), testDeployment, metav1.CreateOptions{}) + _, err = testClient.AppsV1().Deployments(helm.Namespace).Create(context.TODO(), testDeployment, metav1.CreateOptions{}) require.NoError(err) err = cliCheck(testClient, 10) assert.NoError(err) - err = testClient.AppsV1().Deployments(helmNamespace).Delete(context.TODO(), helmCoordinatorDeployment, metav1.DeleteOptions{}) + err = testClient.AppsV1().Deployments(helm.Namespace).Delete(context.TODO(), helm.CoordinatorDeployment, metav1.DeleteOptions{}) require.NoError(err) timeoutDeployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: helmCoordinatorDeployment, + Name: helm.CoordinatorDeployment, }, Status: appsv1.DeploymentStatus{ Replicas: 1, UnavailableReplicas: 0, }, } - _, err = testClient.AppsV1().Deployments(helmNamespace).Create(context.TODO(), timeoutDeployment, metav1.CreateOptions{}) + _, err = testClient.AppsV1().Deployments(helm.Namespace).Create(context.TODO(), timeoutDeployment, metav1.CreateOptions{}) require.NoError(err) err = cliCheck(testClient, 2) diff --git a/cli/internal/cmd/cmd.go b/cli/internal/cmd/cmd.go new file mode 100644 index 00000000..61023509 --- /dev/null +++ b/cli/internal/cmd/cmd.go @@ -0,0 +1,50 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Package cmd implements the MarbleRun's CLI commands. +package cmd + +import ( + "context" + "io" + + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/client-go/kubernetes" +) + +const webhookName = "marble-injector.marblerun" + +type getter interface { + Get(ctx context.Context, path string, body io.Reader, queryParameters ...string) ([]byte, error) +} + +type poster interface { + Post(ctx context.Context, path, contentType string, body io.Reader) ([]byte, error) +} + +func checkLegacyKubernetesVersion(kubeClient kubernetes.Interface) (bool, error) { + serverVersion, err := kubeClient.Discovery().ServerVersion() + if err != nil { + return false, err + } + versionInfo, err := version.ParseGeneric(serverVersion.String()) + if err != nil { + return false, err + } + + // return the legacy if kubernetes version is < 1.19 + if versionInfo.Major() == 1 && versionInfo.Minor() < 19 { + return true, nil + } + + return false, nil +} + +func must(err error) { + if err != nil { + panic(err) + } +} diff --git a/cli/internal/cmd/cmd_test.go b/cli/internal/cmd/cmd_test.go new file mode 100644 index 00000000..befa9a4d --- /dev/null +++ b/cli/internal/cmd/cmd_test.go @@ -0,0 +1,38 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "context" + "io" +) + +type stubGetter struct { + response []byte + requestPath string + query []string + err error +} + +func (s *stubGetter) Get(_ context.Context, request string, _ io.Reader, query ...string) ([]byte, error) { + s.requestPath = request + s.query = query + return s.response, s.err +} + +type stubPoster struct { + response []byte + requestPath string + header string + err error +} + +func (s *stubPoster) Post(_ context.Context, request string, header string, _ io.Reader) ([]byte, error) { + s.requestPath = request + s.header = header + return s.response, s.err +} diff --git a/cli/cmd/csr.go b/cli/internal/cmd/csr.go similarity index 96% rename from cli/cmd/csr.go rename to cli/internal/cmd/csr.go index d203bdf2..c19c5c56 100644 --- a/cli/cmd/csr.go +++ b/cli/internal/cmd/csr.go @@ -16,7 +16,6 @@ import ( "encoding/base64" "encoding/pem" "fmt" - "io/ioutil" "os" "path/filepath" "time" @@ -54,7 +53,7 @@ func newCertificateV1(kubeClient kubernetes.Interface) (*certificateV1, error) { privKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { - return nil, fmt.Errorf("failed creating rsa private key: %v", err) + return nil, fmt.Errorf("failed creating rsa private key: %w", err) } crt.privKey = privKey @@ -113,17 +112,17 @@ func (crt *certificateV1) setCaBundle() ([]string, error) { if len(kubeConfig.CAData) > 0 { caBundle = base64.StdEncoding.EncodeToString(kubeConfig.CAData) } else if len(kubeConfig.CAFile) > 0 { - fileData, err := ioutil.ReadFile(kubeConfig.CAFile) + fileData, err := os.ReadFile(kubeConfig.CAFile) if err != nil { return nil, err } caBundle = base64.StdEncoding.EncodeToString(fileData) } else { - return nil, fmt.Errorf("unable to read CAData or CAFile from kube-config: %s", path) + return nil, fmt.Errorf("reading CAData or CAFile from kube-config: %s", path) } injectorVals := []string{ - fmt.Sprintf("marbleInjector.start=%v", true), + fmt.Sprintf("marbleInjector.start=%t", true), fmt.Sprintf("marbleInjector.CABundle=%s", caBundle), } diff --git a/cli/cmd/csr_legay.go b/cli/internal/cmd/csr_legay.go similarity index 97% rename from cli/cmd/csr_legay.go rename to cli/internal/cmd/csr_legay.go index 3658e6f2..df757582 100644 --- a/cli/cmd/csr_legay.go +++ b/cli/internal/cmd/csr_legay.go @@ -67,7 +67,7 @@ func newCertificateLegacy() (*certificateLegacy, error) { serverPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { - return nil, fmt.Errorf("failed creating rsa private key: %v", err) + return nil, fmt.Errorf("failed creating rsa private key: %w", err) } crt.serverPrivKey = serverPrivKey @@ -84,7 +84,7 @@ func (crt *certificateLegacy) get() ([]byte, error) { func (crt *certificateLegacy) setCaBundle() ([]string, error) { caCertBytes := pem.EncodeToMemory(crt.caCert) injectorValues := []string{ - fmt.Sprintf("marbleInjector.start=%v", true), + fmt.Sprintf("marbleInjector.start=%t", true), fmt.Sprintf("marbleInjector.CABundle=%s", base64.StdEncoding.EncodeToString(caCertBytes)), } return injectorValues, nil diff --git a/cli/cmd/csr_test.go b/cli/internal/cmd/csr_test.go similarity index 97% rename from cli/cmd/csr_test.go rename to cli/internal/cmd/csr_test.go index 1f0edee9..e7341251 100644 --- a/cli/cmd/csr_test.go +++ b/cli/internal/cmd/csr_test.go @@ -10,7 +10,6 @@ import ( "crypto/rand" "crypto/rsa" "fmt" - "io/ioutil" "os" "testing" @@ -68,7 +67,7 @@ func TestCertificateV1(t *testing.T) { require.NoError(err) assert.True((len(testCrt) == 0)) - configFile, err := ioutil.TempFile(os.TempDir(), "unittest") + configFile, err := os.CreateTemp(os.TempDir(), "unittest") require.NoError(err) defer os.Remove(configFile.Name()) err = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, configFile.Name()) diff --git a/cli/internal/cmd/install.go b/cli/internal/cmd/install.go new file mode 100644 index 00000000..dfbbafff --- /dev/null +++ b/cli/internal/cmd/install.go @@ -0,0 +1,336 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + + "github.com/edgelesssys/marblerun/cli/internal/helm" + "github.com/edgelesssys/marblerun/cli/internal/kube" + "github.com/edgelesssys/marblerun/util" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func NewInstallCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "install", + Short: "Installs MarbleRun on a Kubernetes cluster", + Long: `Installs MarbleRun on a Kubernetes cluster`, + Example: `# Install MarbleRun in simulation mode +marblerun install --simulation + +# Install MarbleRun using the Intel QPL and custom PCCS +marblerun install --dcap-qpl intel --dcap-pccs-url https://pccs.example.com/sgx/certification/v3/ --dcap-secure-cert FALSE`, + Args: cobra.NoArgs, + RunE: runInstall, + } + + cmd.Flags().String("domain", "localhost", "Sets the CNAME for the Coordinator certificate") + cmd.Flags().String("marblerun-chart-path", "", "Path to MarbleRun helm chart") + cmd.Flags().String("version", "", "Version of the Coordinator to install, latest by default") + cmd.Flags().String("resource-key", "", "Resource providing SGX, different depending on used device plugin. Use this to set tolerations/resources if your device plugin is not supported by MarbleRun") + cmd.Flags().String("dcap-qpl", "azure", `Quote provider library to use by the Coordinator. One of {"azure", "intel"}`) + cmd.Flags().String("dcap-pccs-url", "https://localhost:8081/sgx/certification/v3/", "Provisioning Certificate Caching Service (PCCS) server address") + cmd.Flags().String("dcap-secure-cert", "TRUE", "To accept insecure HTTPS certificate from the PCCS, set this option to FALSE") + cmd.Flags().String("enterprise-access-token", "", "Access token for Enterprise Coordinator. Leave empty for default installation") + cmd.Flags().Bool("simulation", false, "Set MarbleRun to start in simulation mode") + cmd.Flags().Bool("disable-auto-injection", false, "Install MarbleRun without auto-injection webhook") + cmd.Flags().Bool("wait", false, "Wait for MarbleRun installation to complete before returning") + cmd.Flags().Int("mesh-server-port", 2001, "Set the mesh server port. Needs to be configured to the same port as in the data-plane marbles") + cmd.Flags().Int("client-server-port", 4433, "Set the client server port. Needs to be configured to the same port as in your client tool stack") + + return cmd +} + +func runInstall(cmd *cobra.Command, args []string) error { + kubeClient, err := kube.NewClient() + if err != nil { + return err + } + helmClient, err := helm.New() + if err != nil { + return err + } + + return cliInstall(cmd, helmClient, kubeClient) +} + +// cliInstall installs MarbleRun on the cluster. +func cliInstall(cmd *cobra.Command, helmClient *helm.Client, kubeClient kubernetes.Interface) error { + flags, err := parseInstallFlags(cmd) + if err != nil { + return fmt.Errorf("parsing install flags: %w", err) + } + + chart, err := helmClient.GetChart(flags.chartPath, flags.version, (flags.accessToken != "")) + if err != nil { + return fmt.Errorf("loading MarbleRun helm chart: %w", err) + } + + if flags.resourceKey == "" { + flags.resourceKey, err = getSGXResourceKey(cmd.Context(), kubeClient) + if err != nil { + return fmt.Errorf("trying to determine SGX resource key: %w", err) + } + } + + var webhookSettings []string + if !flags.disableInjection { + webhookSettings, err = installWebhook(cmd, kubeClient) + if err != nil { + return errorAndCleanup(cmd.Context(), fmt.Errorf("installing webhook certs: %w", err), kubeClient) + } + } + + values, err := helmClient.UpdateValues( + helm.Options{ + Hostname: flags.hostname, + DCAPQPL: flags.dcapQPL, + PCCSURL: flags.pccsURL, + UseSecureCert: flags.useSecureCert, + AccessToken: flags.accessToken, + SGXResourceKey: flags.resourceKey, + WebhookSettings: webhookSettings, + SimulationMode: flags.simulation, + CoordinatorRESTPort: flags.clientPort, + CoordinatorGRPCPort: flags.meshPort, + }, + chart.Values, + ) + if err != nil { + return errorAndCleanup(cmd.Context(), fmt.Errorf("generating helm values: %w", err), kubeClient) + } + + if err := helmClient.Install(cmd.Context(), flags.wait, chart, values); err != nil { + return errorAndCleanup(cmd.Context(), fmt.Errorf("installing MarbleRun: %w", err), kubeClient) + } + + cmd.Println("MarbleRun installed successfully") + return nil +} + +// installWebhook enables a mutating admission webhook to allow automatic injection of values into pods. +func installWebhook(cmd *cobra.Command, kubeClient kubernetes.Interface) ([]string, error) { + // verify 'marblerun' namespace exists, if not create it + if err := verifyNamespace(cmd.Context(), helm.Namespace, kubeClient); err != nil { + return nil, err + } + + cmd.Print("Setting up MarbleRun Webhook") + certificateHandler, err := getCertificateHandler(cmd.OutOrStdout(), kubeClient) + if err != nil { + return nil, err + } + cmd.Print(".") + if err := certificateHandler.signRequest(); err != nil { + return nil, err + } + cmd.Print(".") + injectorValues, err := certificateHandler.setCaBundle() + if err != nil { + return nil, err + } + cert, err := certificateHandler.get() + if err != nil { + return nil, err + } + if len(cert) <= 0 { + return nil, fmt.Errorf("certificate was not signed by the CA") + } + cmd.Print(".") + + if err := createSecret(cmd.Context(), certificateHandler.getKey(), cert, kubeClient); err != nil { + return nil, err + } + cmd.Printf(" Done\n") + return injectorValues, nil +} + +// createSecret creates a secret containing the signed certificate and private key for the webhook server. +func createSecret(ctx context.Context, privKey *rsa.PrivateKey, crt []byte, kubeClient kubernetes.Interface) error { + rsaPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privKey), + }, + ) + + newSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "marble-injector-webhook-certs", + Namespace: helm.Namespace, + }, + Data: map[string][]byte{ + "tls.crt": crt, + "tls.key": rsaPEM, + }, + } + + _, err := kubeClient.CoreV1().Secrets(helm.Namespace).Create(ctx, newSecret, metav1.CreateOptions{}) + return err +} + +func getCertificateHandler(out io.Writer, kubeClient kubernetes.Interface) (certificateInterface, error) { + isLegacy, err := checkLegacyKubernetesVersion(kubeClient) + if err != nil { + return nil, err + } + if isLegacy { + fmt.Fprintf(out, "\nKubernetes version lower than 1.19 detected, using self-signed certificates as CABundle") + return newCertificateLegacy() + } + return newCertificateV1(kubeClient) +} + +func verifyNamespace(ctx context.Context, namespace string, kubeClient kubernetes.Interface) error { + _, err := kubeClient.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) + if err != nil { + // if the namespace does not exist we create it + + if errors.IsNotFound(err) { + marbleNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + if _, err := kubeClient.CoreV1().Namespaces().Create(ctx, marbleNamespace, metav1.CreateOptions{}); err != nil { + return err + } + } else { + return err + } + } + return nil +} + +// getSGXResourceKey checks what device plugin is providing SGX on the cluster and returns the corresponding resource key. +func getSGXResourceKey(ctx context.Context, kubeClient kubernetes.Interface) (string, error) { + nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return "", err + } + + for _, node := range nodes.Items { + if nodeHasAlibabaDevPlugin(node.Status.Capacity) { + return util.AlibabaEpc.String(), nil + } + if nodeHasAzureDevPlugin(node.Status.Capacity) { + return util.AzureEpc.String(), nil + } + if nodeHasIntelDevPlugin(node.Status.Capacity) { + return util.IntelEpc.String(), nil + } + } + + // assume cluster has the intel SGX device plugin by default + return util.IntelEpc.String(), nil +} + +// errorAndCleanup returns the given error and deletes resources which might have been created previously. +// This prevents secrets and CSRs to stay on the cluster after a failed installation attempt. +func errorAndCleanup(ctx context.Context, err error, kubeClient kubernetes.Interface) error { + // We dont care about any additional errors here + cleanupCSR(ctx, kubeClient) + cleanupSecrets(ctx, kubeClient) + return err +} + +type installFlags struct { + chartPath string + hostname string + version string + resourceKey string + dcapQPL string + pccsURL string + useSecureCert string + accessToken string + simulation bool + disableInjection bool + wait bool + clientPort int + meshPort int +} + +func parseInstallFlags(cmd *cobra.Command) (installFlags, error) { + chartPath, err := cmd.Flags().GetString("marblerun-chart-path") + if err != nil { + return installFlags{}, err + } + hostname, err := cmd.Flags().GetString("domain") + if err != nil { + return installFlags{}, err + } + version, err := cmd.Flags().GetString("version") + if err != nil { + return installFlags{}, err + } + resourceKey, err := cmd.Flags().GetString("resource-key") + if err != nil { + return installFlags{}, err + } + dcapQPL, err := cmd.Flags().GetString("dcap-qpl") + if err != nil { + return installFlags{}, err + } + pccsURL, err := cmd.Flags().GetString("dcap-pccs-url") + if err != nil { + return installFlags{}, err + } + useSecureCert, err := cmd.Flags().GetString("dcap-secure-cert") + if err != nil { + return installFlags{}, err + } + accessToken, err := cmd.Flags().GetString("enterprise-access-token") + if err != nil { + return installFlags{}, err + } + simulation, err := cmd.Flags().GetBool("simulation") + if err != nil { + return installFlags{}, err + } + disableInjection, err := cmd.Flags().GetBool("disable-auto-injection") + if err != nil { + return installFlags{}, err + } + wait, err := cmd.Flags().GetBool("wait") + if err != nil { + return installFlags{}, err + } + clientPort, err := cmd.Flags().GetInt("client-server-port") + if err != nil { + return installFlags{}, err + } + meshPort, err := cmd.Flags().GetInt("mesh-server-port") + if err != nil { + return installFlags{}, err + } + + return installFlags{ + chartPath: chartPath, + hostname: hostname, + version: version, + resourceKey: resourceKey, + dcapQPL: dcapQPL, + pccsURL: pccsURL, + useSecureCert: useSecureCert, + accessToken: accessToken, + simulation: simulation, + disableInjection: disableInjection, + wait: wait, + clientPort: clientPort, + meshPort: meshPort, + }, nil +} diff --git a/cli/cmd/install_test.go b/cli/internal/cmd/install_test.go similarity index 60% rename from cli/cmd/install_test.go rename to cli/internal/cmd/install_test.go index c9b15a92..bdd9060f 100644 --- a/cli/cmd/install_test.go +++ b/cli/internal/cmd/install_test.go @@ -7,6 +7,7 @@ package cmd import ( + "bytes" "context" "crypto/rand" "crypto/rsa" @@ -14,7 +15,9 @@ import ( "reflect" "testing" + "github.com/edgelesssys/marblerun/cli/internal/helm" "github.com/edgelesssys/marblerun/util" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" certv1 "k8s.io/api/certificates/v1" @@ -30,6 +33,7 @@ import ( func TestCreateSecret(t *testing.T) { require := require.New(t) testClient := fake.NewSimpleClientset() + ctx := context.Background() testKey, err := rsa.GenerateKey(rand.Reader, 1024) require.NoError(err) @@ -37,19 +41,19 @@ func TestCreateSecret(t *testing.T) { newNamespace1 := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: helmNamespace, + Name: helm.Namespace, }, } - _, err = testClient.CoreV1().Namespaces().Create(context.TODO(), newNamespace1, metav1.CreateOptions{}) + _, err = testClient.CoreV1().Namespaces().Create(ctx, newNamespace1, metav1.CreateOptions{}) require.NoError(err) - err = createSecret(testKey, crt, testClient) + err = createSecret(ctx, testKey, crt, testClient) require.NoError(err) - _, err = testClient.CoreV1().Secrets(helmNamespace).Get(context.TODO(), "marble-injector-webhook-certs", metav1.GetOptions{}) + _, err = testClient.CoreV1().Secrets(helm.Namespace).Get(context.TODO(), "marble-injector-webhook-certs", metav1.GetOptions{}) require.NoError(err) // we should get an error since the secret was already created in the previous step - err = createSecret(testKey, crt, testClient) + err = createSecret(ctx, testKey, crt, testClient) require.Error(err) } @@ -58,50 +62,58 @@ func TestGetCertificateHandler(t *testing.T) { require := require.New(t) testClient := fake.NewSimpleClientset() + var out bytes.Buffer + testClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ Major: "1", Minor: "19", GitVersion: "v1.19.4", } - testHandler, err := getCertificateHandler(testClient) + testHandler, err := getCertificateHandler(&out, testClient) require.NoError(err) assert.Equal("*cmd.certificateV1", reflect.TypeOf(testHandler).String()) + assert.Empty(out.String()) + out.Reset() testClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ Major: "1", Minor: "18", GitVersion: "v1.18.4", } - testHandler, err = getCertificateHandler(testClient) + testHandler, err = getCertificateHandler(&out, testClient) require.NoError(err) assert.Equal("*cmd.certificateLegacy", reflect.TypeOf(testHandler).String()) + assert.NotEmpty(out.String()) + out.Reset() testClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ Major: "1", Minor: "24+", GitVersion: "v1.24.3-2+63243a96d1c393", } - testHandler, err = getCertificateHandler(testClient) + testHandler, err = getCertificateHandler(&out, testClient) require.NoError(err) assert.Equal("*cmd.certificateV1", reflect.TypeOf(testHandler).String()) + assert.Empty(out.String()) } func TestVerifyNamespace(t *testing.T) { require := require.New(t) testClient := fake.NewSimpleClientset() + ctx := context.Background() - _, err := testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space", metav1.GetOptions{}) + _, err := testClient.CoreV1().Namespaces().Get(ctx, "test-space", metav1.GetOptions{}) require.Error(err) // namespace does not exist, it should be created here - err = verifyNamespace("test-space", testClient) + err = verifyNamespace(ctx, "test-space", testClient) require.NoError(err) _, err = testClient.CoreV1().Namespaces().Get(context.TODO(), "test-space", metav1.GetOptions{}) require.NoError(err) // namespace exists, should return nil - err = verifyNamespace("test-space", testClient) + err = verifyNamespace(ctx, "test-space", testClient) require.NoError(err) } @@ -115,7 +127,11 @@ func TestInstallWebhook(t *testing.T) { GitVersion: "v1.18.4", } - testValues, err := installWebhook(testClient) + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + testValues, err := installWebhook(cmd, testClient) assert.NoError(err) assert.Equal("marbleInjector.start=true", testValues[0], "failed to set start to true") assert.Contains(testValues[1], "LS0t", "failed to set CABundle") @@ -126,6 +142,7 @@ func TestGetSGXResourceKey(t *testing.T) { require := require.New(t) testClient := fake.NewSimpleClientset() + ctx := context.Background() // Test Intel Device Plugin intelSGXNode := &corev1.Node{ @@ -140,10 +157,10 @@ func TestGetSGXResourceKey(t *testing.T) { }, }, } - _, err := testClient.CoreV1().Nodes().Create(context.TODO(), intelSGXNode, metav1.CreateOptions{}) + _, err := testClient.CoreV1().Nodes().Create(ctx, intelSGXNode, metav1.CreateOptions{}) require.NoError(err) - resourceKey, err := getSGXResourceKey(testClient) + resourceKey, err := getSGXResourceKey(ctx, testClient) assert.NoError(err) assert.Equal(util.IntelEpc.String(), resourceKey) } @@ -158,9 +175,10 @@ func TestErrorAndCleanup(t *testing.T) { Minor: "19", GitVersion: "v1.19.4", } + ctx := context.Background() testError := errors.New("test") - err := errorAndCleanup(testError, testClient) + err := errorAndCleanup(ctx, testError, testClient) assert.Equal(testError, err) // Create and test for CSR @@ -183,87 +201,9 @@ func TestErrorAndCleanup(t *testing.T) { _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), webhookName, metav1.GetOptions{}) require.NoError(err) - err = errorAndCleanup(testError, testClient) + err = errorAndCleanup(ctx, testError, testClient) assert.Equal(testError, err) _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), webhookName, metav1.GetOptions{}) assert.True(kubeErrors.IsNotFound(err)) } - -func TestNeedsDeletion(t *testing.T) { - testCases := map[string]struct { - existingKey string - sgxKey string - wantDeletion bool - }{ - "intel key with azure plugin": { - existingKey: util.IntelEpc.String(), - sgxKey: util.AzureEpc.String(), - wantDeletion: true, - }, - "intel key with alibaba plugin": { - existingKey: util.IntelEpc.String(), - sgxKey: util.AlibabaEpc.String(), - wantDeletion: true, - }, - "azure key with intel plugin": { - existingKey: util.AzureEpc.String(), - sgxKey: util.IntelEpc.String(), - wantDeletion: true, - }, - "azure key with alibaba plugin": { - existingKey: util.AzureEpc.String(), - sgxKey: util.AlibabaEpc.String(), - wantDeletion: true, - }, - "alibaba key with intel plugin": { - existingKey: util.AlibabaEpc.String(), - sgxKey: util.IntelEpc.String(), - wantDeletion: true, - }, - "alibaba key with azure plugin": { - existingKey: util.AlibabaEpc.String(), - sgxKey: util.AzureEpc.String(), - wantDeletion: true, - }, - "same key": { - existingKey: util.IntelEpc.String(), - sgxKey: util.IntelEpc.String(), - wantDeletion: false, - }, - "intel provision with intel plugin": { - existingKey: util.IntelProvision.String(), - sgxKey: util.IntelEpc.String(), - wantDeletion: false, - }, - "intel enclave with intel plugin": { - existingKey: util.IntelEnclave.String(), - sgxKey: util.IntelEpc.String(), - wantDeletion: false, - }, - "regular resource with intel plugin": { - existingKey: "cpu", - sgxKey: util.IntelEpc.String(), - wantDeletion: false, - }, - "custom resource with intel plugin": { - existingKey: "custom-sgx-resource", - sgxKey: util.IntelEpc.String(), - wantDeletion: false, - }, - "intel provision with custom plugin": { - existingKey: util.IntelProvision.String(), - sgxKey: "custom-sgx-resource", - wantDeletion: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - delete := needsDeletion(tc.existingKey, tc.sgxKey) - assert.Equal(tc.wantDeletion, delete) - }) - } -} diff --git a/cli/internal/cmd/manifest.go b/cli/internal/cmd/manifest.go new file mode 100644 index 00000000..039873b3 --- /dev/null +++ b/cli/internal/cmd/manifest.go @@ -0,0 +1,32 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func NewManifestCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "manifest", + Short: "Manages manifest for the MarbleRun Coordinator", + Long: ` +Manages manifests for the MarbleRun Coordinator. +Used to either set the manifest, update an already set manifest, +or return a signature of the currently set manifest to the user`, + Example: "manifest set manifest.json example.com:4433 [--era-config=config.json] [--insecure]", + } + + cmd.AddCommand(newManifestGet()) + cmd.AddCommand(newManifestLog()) + cmd.AddCommand(newManifestSet()) + cmd.AddCommand(newManifestSignature()) + cmd.AddCommand(newManifestUpdate()) + cmd.AddCommand(newManifestVerify()) + + return cmd +} diff --git a/cli/cmd/manifestGet.go b/cli/internal/cmd/manifestGet.go similarity index 50% rename from cli/cmd/manifestGet.go rename to cli/internal/cmd/manifestGet.go index bc170d74..b5ec7a99 100644 --- a/cli/cmd/manifestGet.go +++ b/cli/internal/cmd/manifestGet.go @@ -7,22 +7,21 @@ package cmd import ( + "context" "encoding/base64" "encoding/json" - "encoding/pem" "fmt" - "io/ioutil" + "net/http" + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" "github.com/edgelesssys/marblerun/coordinator/manifest" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/tidwall/gjson" ) func newManifestGet() *cobra.Command { - var output string - var displayUpdate bool - var signature bool - cmd := &cobra.Command{ Use: "get ", Short: "Get the manifest from the MarbleRun Coordinator", @@ -30,43 +29,56 @@ func newManifestGet() *cobra.Command { Optionally get the manifests signature or merge updates into the displayed manifest.`, Example: "marblerun manifest get $MARBLERUN -s --era-config=era.json", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[0] - cert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - cmd.Println("Successfully verified Coordinator, now requesting manifest") - response, err := cliDataGet(hostName, "manifest", "data", cert) - if err != nil { - return err - } - manifest, err := decodeManifest(displayUpdate, gjson.GetBytes(response, "Manifest").String(), hostName, cert) - if err != nil { - return err - } - if signature { - // wrap the signature and manifest into one json object - manifest = fmt.Sprintf("{\n\"ManifestSignature\": \"%s\",\n\"Manifest\": %s}", gjson.GetBytes(response, "ManifestSignature"), manifest) - } - - if len(output) > 0 { - return ioutil.WriteFile(output, []byte(manifest), 0o644) - } - cmd.Println(manifest) - return nil - }, - SilenceUsage: true, + RunE: runManifestGet, } - cmd.Flags().BoolVarP(&signature, "signature", "s", false, "Set to additionally display the manifests signature") - cmd.Flags().BoolVarP(&displayUpdate, "display-update", "u", false, "Set to merge updates into the displayed manifest") - cmd.Flags().StringVarP(&output, "output", "o", "", "Save output to file instead of printing to stdout") + cmd.Flags().BoolP("signature", "s", false, "Set to additionally display the manifests signature") + cmd.Flags().BoolP("display-update", "u", false, "Set to merge updates into the displayed manifest") + cmd.Flags().StringP("output", "o", "", "Save output to file instead of printing to stdout") return cmd } +func runManifestGet(cmd *cobra.Command, args []string) error { + hostname := args[0] + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + cmd.Println("Successfully verified Coordinator, now requesting manifest") + + flags, err := parseManifestGetFlags(cmd) + if err != nil { + return err + } + file := file.New(flags.output, afero.NewOsFs()) + + return cliManifestGet(cmd, flags, file, client) +} + +func cliManifestGet(cmd *cobra.Command, flags manifestGetFlags, file *file.Handler, client getter) error { + resp, err := client.Get(cmd.Context(), rest.ManifestEndpoint, http.NoBody) + if err != nil { + return fmt.Errorf("getting manifest: %w", err) + } + + manifest, err := decodeManifest(cmd.Context(), flags.displayUpdate, gjson.GetBytes(resp, "Manifest").String(), client) + if err != nil { + return err + } + if flags.signature { + // wrap the signature and manifest into one json object + manifest = fmt.Sprintf("{\n\"ManifestSignature\": \"%s\",\n\"Manifest\": %s}", gjson.GetBytes(resp, "ManifestSignature"), manifest) + } + + if file != nil { + return file.Write([]byte(manifest)) + } + cmd.Println(manifest) + return nil +} + // decodeManifest parses a base64 encoded manifest and optionally merges updates. -func decodeManifest(displayUpdate bool, encodedManifest, hostName string, cert []*pem.Block) (string, error) { +func decodeManifest(ctx context.Context, displayUpdate bool, encodedManifest string, client getter) (string, error) { manifest, err := base64.StdEncoding.DecodeString(encodedManifest) if err != nil { return "", err @@ -76,11 +88,10 @@ func decodeManifest(displayUpdate bool, encodedManifest, hostName string, cert [ return string(manifest), nil } - log, err := cliDataGet(hostName, "update", "data", cert) + log, err := client.Get(ctx, rest.UpdateEndpoint, http.NoBody) if err != nil { - return "", err + return "", fmt.Errorf("retrieving update log: %w", err) } - return consolidateManifest(manifest, log) } @@ -134,3 +145,29 @@ func removeNil(m map[string]interface{}) { } } } + +type manifestGetFlags struct { + output string + displayUpdate bool + signature bool +} + +func parseManifestGetFlags(cmd *cobra.Command) (manifestGetFlags, error) { + output, err := cmd.Flags().GetString("output") + if err != nil { + return manifestGetFlags{}, err + } + displayUpdate, err := cmd.Flags().GetBool("display-update") + if err != nil { + return manifestGetFlags{}, err + } + signature, err := cmd.Flags().GetBool("signature") + if err != nil { + return manifestGetFlags{}, err + } + return manifestGetFlags{ + output: output, + displayUpdate: displayUpdate, + signature: signature, + }, nil +} diff --git a/cli/internal/cmd/manifestLog.go b/cli/internal/cmd/manifestLog.go new file mode 100644 index 00000000..06364ee4 --- /dev/null +++ b/cli/internal/cmd/manifestLog.go @@ -0,0 +1,61 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "fmt" + "net/http" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newManifestLog() *cobra.Command { + cmd := &cobra.Command{ + Use: "log ", + Short: "Get the update log from the MarbleRun Coordinator", + Long: `Get the update log from the MarbleRun Coordinator. + The log is list of all successful changes to the Coordinator, + including a timestamp and user performing the operation.`, + Example: "marblerun manifest log $MARBLERUN", + Args: cobra.ExactArgs(1), + RunE: runManifestLog, + } + cmd.Flags().StringP("output", "o", "", "Save log to file instead of printing to stdout") + return cmd +} + +func runManifestLog(cmd *cobra.Command, args []string) error { + hostname := args[0] + + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + + cmd.Println("Successfully verified Coordinator, now requesting update log") + return cliManifestLog(cmd, file.New(output, afero.NewOsFs()), client) +} + +func cliManifestLog(cmd *cobra.Command, file *file.Handler, client getter) error { + resp, err := client.Get(cmd.Context(), rest.UpdateEndpoint, http.NoBody) + if err != nil { + return fmt.Errorf("retrieving update log: %w", err) + } + + if file != nil { + return file.Write(resp) + } + cmd.Printf("Update log:\n%s", resp) + return nil +} diff --git a/cli/internal/cmd/manifestSet.go b/cli/internal/cmd/manifestSet.go new file mode 100644 index 00000000..4da3df4b --- /dev/null +++ b/cli/internal/cmd/manifestSet.go @@ -0,0 +1,101 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" +) + +func newManifestSet() *cobra.Command { + cmd := &cobra.Command{ + Use: "set ", + Short: "Sets the manifest for the MarbleRun Coordinator", + Long: "Sets the manifest for the MarbleRun Coordinator", + Example: "marblerun manifest set manifest.json $MARBLERUN --recovery-data=recovery-secret.json --era-config=era.json", + Args: cobra.ExactArgs(2), + RunE: runManifestSet, + } + + cmd.Flags().StringP("recoverydata", "r", "", "File to write recovery data to, print to stdout if non specified") + + return cmd +} + +func runManifestSet(cmd *cobra.Command, args []string) error { + manifestFile := args[0] + hostname := args[1] + + recoveryFilename, err := cmd.Flags().GetString("recoverydata") + if err != nil { + return err + } + + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + + cmd.Println("Successfully verified Coordinator, now uploading manifest") + + manifest, err := loadManifestFile(file.New(manifestFile, afero.NewOsFs())) + if err != nil { + return err + } + signature := cliManifestSignature(manifest) + cmd.Printf("Manifest signature: %s\n", signature) + + return cliManifestSet(cmd, manifest, file.New(recoveryFilename, afero.NewOsFs()), client) +} + +// cliManifestSet sets the Coordinators manifest using its rest api. +func cliManifestSet(cmd *cobra.Command, manifest []byte, file *file.Handler, client poster) error { + resp, err := client.Post(cmd.Context(), rest.ManifestEndpoint, rest.ContentJSON, bytes.NewReader(manifest)) + if err != nil { + return fmt.Errorf("setting manifest: %w", err) + } + cmd.Println("Manifest successfully set") + + // Skip outputting secrets if we do not get any recovery secrets back + if len(resp) == 0 { + return nil + } + // recovery secret was sent, print or save to file + if file != nil { + if err := file.Write(resp); err != nil { + return err + } + cmd.Printf("Recovery data saved to: %s\n", file.Name()) + } else { + cmd.Println(string(resp)) + } + + return nil +} + +// loadManifestFile loads a manifest in either json or yaml format and returns the data as json. +func loadManifestFile(file *file.Handler) ([]byte, error) { + manifestData, err := file.Read() + if err != nil { + return nil, err + } + + // if Valid is true the file was in JSON format and we can just return the data + if json.Valid(manifestData) { + return manifestData, err + } + + // otherwise we try to convert from YAML to json + return yaml.YAMLToJSON(manifestData) +} diff --git a/cli/cmd/manifestSignature.go b/cli/internal/cmd/manifestSignature.go similarity index 64% rename from cli/cmd/manifestSignature.go rename to cli/internal/cmd/manifestSignature.go index 3b83f280..69385009 100644 --- a/cli/cmd/manifestSignature.go +++ b/cli/internal/cmd/manifestSignature.go @@ -9,8 +9,9 @@ package cmd import ( "crypto/sha256" "encoding/hex" - "fmt" + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/spf13/afero" "github.com/spf13/cobra" ) @@ -20,25 +21,25 @@ func newManifestSignature() *cobra.Command { Short: "Prints the signature of a MarbleRun manifest", Long: "Prints the signature of a MarbleRun manifest", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - manifestFile := args[0] - - // Load manifest - manifest, err := loadManifestFile(manifestFile) - if err != nil { - return err - } - - signature := cliManifestSignature(manifest) - fmt.Printf("%s\n", signature) - return nil - }, - SilenceUsage: true, + RunE: runManifestSignature, } return cmd } +func runManifestSignature(cmd *cobra.Command, args []string) error { + manifestFile := args[0] + + manifest, err := loadManifestFile(file.New(manifestFile, afero.NewOsFs())) + if err != nil { + return err + } + + signature := cliManifestSignature(manifest) + cmd.Println(signature) + return nil +} + func cliManifestSignature(rawManifest []byte) string { hash := sha256.Sum256(rawManifest) return hex.EncodeToString(hash[:]) diff --git a/cli/internal/cmd/manifestUpdate.go b/cli/internal/cmd/manifestUpdate.go new file mode 100644 index 00000000..58159339 --- /dev/null +++ b/cli/internal/cmd/manifestUpdate.go @@ -0,0 +1,252 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "os" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/tidwall/gjson" +) + +func newManifestUpdate() *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Manage manifest updates for the MarbleRun Coordinator", + Long: "Manage manifest updates for the MarbleRun Coordinator.", + } + + cmd.AddCommand(newUpdateApply()) + cmd.AddCommand(newUpdateAcknowledge()) + cmd.AddCommand(newUpdateCancel()) + cmd.AddCommand(newUpdateGet()) + return cmd +} + +func newUpdateApply() *cobra.Command { + cmd := &cobra.Command{ + Use: "apply ", + Short: "Update the MarbleRun Coordinator with the specified manifest", + Long: ` +Update the MarbleRun Coordinator with the specified manifest. +An admin certificate specified in the original manifest is needed to verify the authenticity of the update manifest. +`, + Example: "marblerun manifest update apply update-manifest.json $MARBLERUN --cert=admin-cert.pem --key=admin-key.pem --era-config=era.json", + Args: cobra.ExactArgs(2), + RunE: runUpdateApply, + } + + cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") + cmd.MarkFlagRequired("cert") + cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") + cmd.MarkFlagRequired("key") + + return cmd +} + +func newUpdateAcknowledge() *cobra.Command { + cmd := &cobra.Command{ + Use: "acknowledge ", + Short: "Acknowledge a pending update for the MarbleRun Coordinator (Enterprise feature)", + Long: `Acknowledge a pending update for the MarbleRun Coordinator (Enterprise feature). + +In case of multi-party updates, the Coordinator will wait for all participants to acknowledge the update before applying it. +All participants must use the same manifest to acknowledge the pending update. +`, + Example: "marblerun manifest update acknowledge update-manifest.json $MARBLERUN --cert=admin-cert.pem --key=admin-key.pem --era-config=era.json", + Args: cobra.ExactArgs(2), + RunE: runUpdateAcknowledge, + } + + cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") + cmd.MarkFlagRequired("cert") + cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") + cmd.MarkFlagRequired("key") + return cmd +} + +func newUpdateCancel() *cobra.Command { + cmd := &cobra.Command{ + Use: "cancel ", + Short: "Cancel a pending manifest update for the MarbleRun Coordinator (Enterprise feature)", + Long: "Cancel a pending manifest update for the MarbleRun Coordinator (Enterprise feature).", + Example: `marblerun manifest update cancel $MARBLERUN --cert=admin-cert.pem --key=admin-key.pem --era-config=era.json`, + Args: cobra.ExactArgs(1), + RunE: runUpdateCancel, + } + + cmd.Flags().StringP("cert", "c", "", "PEM encoded admin certificate file (required)") + cmd.MarkFlagRequired("cert") + cmd.Flags().StringP("key", "k", "", "PEM encoded admin key file (required)") + cmd.MarkFlagRequired("key") + return cmd +} + +func newUpdateGet() *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "View a pending manifest update (Enterprise feature)", + Long: "View a pending manifest update (Enterprise feature).", + Example: `marblerun manifest update get $MARBLERUN --era-config=era.json`, + Args: cobra.ExactArgs(1), + RunE: runUpdateGet, + } + + cmd.Flags().StringP("output", "o", "", "Save output to file instead of printing to stdout") + cmd.Flags().Bool("missing", false, "Display number of missing acknowledgements instead of the manifest") + + return cmd +} + +func runUpdateApply(cmd *cobra.Command, args []string) error { + manifestFile := args[0] + hostname := args[1] + + client, err := rest.NewAuthenticatedClient(cmd, hostname) + if err != nil { + return err + } + + manifest, err := loadManifestFile(file.New(manifestFile, afero.NewOsFs())) + if err != nil { + return err + } + + cmd.Println("Successfully verified Coordinator, now uploading manifest") + return cliManifestUpdateApply(cmd, manifest, client) +} + +// cliManifestUpdate updates the Coordinators manifest using its rest api. +func cliManifestUpdateApply(cmd *cobra.Command, manifest []byte, client poster) error { + _, err := client.Post(cmd.Context(), rest.UpdateEndpoint, rest.ContentJSON, bytes.NewReader(manifest)) + if err != nil { + return fmt.Errorf("applying update: %w", err) + } + + cmd.Println("Update manifest set successfully") + return nil +} + +func runUpdateAcknowledge(cmd *cobra.Command, args []string) error { + manifestFile := args[0] + hostname := args[1] + + client, err := rest.NewAuthenticatedClient(cmd, hostname) + if err != nil { + return err + } + + manifest, err := loadManifestFile(file.New(manifestFile, afero.NewOsFs())) + if err != nil { + return err + } + + cmd.Println("Successfully verified Coordinator") + return cliManifestUpdateAcknowledge(cmd, manifest, client) +} + +func cliManifestUpdateAcknowledge(cmd *cobra.Command, manifest []byte, client poster) error { + resp, err := client.Post(cmd.Context(), rest.UpdateStatusEndpoint, rest.ContentJSON, bytes.NewReader(manifest)) + if err != nil { + return fmt.Errorf("acknowledging update manifest: %w", err) + } + + cmd.Printf("Acknowledgement successful: %s\n", resp) + return nil +} + +func runUpdateCancel(cmd *cobra.Command, args []string) error { + hostname := args[0] + + client, err := rest.NewAuthenticatedClient(cmd, hostname) + if err != nil { + return err + } + + cmd.Println("Successfully verified Coordinator") + return cliManifestUpdateCancel(cmd, client) +} + +func cliManifestUpdateCancel(cmd *cobra.Command, client poster) error { + _, err := client.Post(cmd.Context(), rest.UpdateCancelEndpoint, "", http.NoBody) + if err != nil { + return fmt.Errorf("canceling update: %w", err) + } + cmd.Println("Cancellation successful") + return nil +} + +func runUpdateGet(cmd *cobra.Command, args []string) (retErr error) { + hostname := args[0] + + outputFile, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + displayMissing, err := cmd.Flags().GetBool("missing") + if err != nil { + return err + } + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + + var out io.Writer + if outputFile != "" { + file, err := os.Create(outputFile) + if err != nil { + return err + } + defer func() { + _ = file.Close() + if retErr != nil { + _ = os.Remove(outputFile) + } + }() + out = file + } else { + out = cmd.OutOrStdout() + } + + cmd.Println("Successfully verified Coordinator") + return cliManifestUpdateGet(cmd.Context(), out, displayMissing, client) +} + +func cliManifestUpdateGet(ctx context.Context, out io.Writer, displayMissing bool, client getter) error { + resp, err := client.Get(ctx, rest.UpdateStatusEndpoint, http.NoBody) + if err != nil { + return fmt.Errorf("retrieving pending update manifest: %w", err) + } + + var response string + if displayMissing { + msg := gjson.GetBytes(resp, "message") + missingUsers := gjson.GetBytes(resp, "missingUsers") + + response = fmt.Sprintf("%s\nThe following users have not yet acknowledged the update: %s\n", msg.String(), missingUsers.String()) + } else { + mnfB64 := gjson.GetBytes(resp, "manifest").String() + mnf, err := base64.StdEncoding.DecodeString(mnfB64) + if err != nil { + return err + } + response = string(mnf) + } + fmt.Fprint(out, response) + + return nil +} diff --git a/cli/cmd/manifestVerify.go b/cli/internal/cmd/manifestVerify.go similarity index 57% rename from cli/cmd/manifestVerify.go rename to cli/internal/cmd/manifestVerify.go index 77d2122f..d7e4dd52 100644 --- a/cli/cmd/manifestVerify.go +++ b/cli/internal/cmd/manifestVerify.go @@ -9,47 +9,48 @@ package cmd import ( "crypto/sha256" "encoding/hex" - "encoding/pem" "fmt" - "io" - "io/ioutil" + "net/http" "os" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/tidwall/gjson" ) func newManifestVerify() *cobra.Command { cmd := &cobra.Command{ Use: "verify ", - Short: "Verifies the signature of a MarbleRun manifest", - Long: `Verifies that the signature returned by the Coordinator is equal to a local signature`, + Short: "Verify the signature of a MarbleRun manifest", + Long: `Verify that the signature returned by the Coordinator is equal to a local signature`, Example: "marblerun manifest verify manifest.json $MARBLERUN", Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - manifest := args[0] - hostName := args[1] + RunE: runManifestVerify, + } - cert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } + return cmd +} - localSignature, err := getSignatureFromString(manifest) - if err != nil { - return err - } +func runManifestVerify(cmd *cobra.Command, args []string) error { + manifest := args[0] + hostname := args[1] - return cliManifestVerify(cmd.OutOrStdout(), localSignature, hostName, cert) - }, - SilenceUsage: true, + localSignature, err := getSignatureFromString(manifest, afero.Afero{Fs: afero.NewOsFs()}) + if err != nil { + return err } - return cmd + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + return cliManifestVerify(cmd, localSignature, client) } // getSignatureFromString checks if a string is a file or a valid signature. -func getSignatureFromString(manifest string) (string, error) { - if _, err := os.Stat(manifest); err != nil { +func getSignatureFromString(manifest string, fs afero.Afero) (string, error) { + if _, err := fs.Stat(manifest); err != nil { if !os.IsNotExist(err) { return "", err } @@ -66,7 +67,7 @@ func getSignatureFromString(manifest string) (string, error) { } // manifest is an existing file -> return the signature of the file - rawManifest, err := ioutil.ReadFile(manifest) + rawManifest, err := fs.ReadFile(manifest) if err != nil { return "", err } @@ -74,16 +75,16 @@ func getSignatureFromString(manifest string) (string, error) { } // cliManifestVerify verifies if a signature returned by the MarbleRun Coordinator is equal to one locally created. -func cliManifestVerify(out io.Writer, localSignature string, host string, cert []*pem.Block) error { - remoteSignature, err := cliDataGet(host, "manifest", "data.ManifestSignature", cert) +func cliManifestVerify(cmd *cobra.Command, localSignature string, client getter) error { + resp, err := client.Get(cmd.Context(), rest.ManifestEndpoint, http.NoBody) if err != nil { return err } - - if string(remoteSignature) != localSignature { - return fmt.Errorf("remote signature differs from local signature: %s != %s", string(remoteSignature), localSignature) + remoteSignature := gjson.GetBytes(resp, "ManifestSignature").String() + if remoteSignature != localSignature { + return fmt.Errorf("remote signature differs from local signature: %s != %s", remoteSignature, localSignature) } - fmt.Fprintln(out, "OK") + cmd.Println("OK") return nil } diff --git a/cli/internal/cmd/manifest_test.go b/cli/internal/cmd/manifest_test.go new file mode 100644 index 00000000..ebbcafa7 --- /dev/null +++ b/cli/internal/cmd/manifest_test.go @@ -0,0 +1,453 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "testing" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/edgelesssys/marblerun/test" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testLog = []byte(`{"time":"1970-01-01T01:00:00.0","update":"initial manifest set"} +{"time":"1970-01-01T02:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":5} +{"time":"1970-01-01T03:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":5} +{"time":"1970-01-01T04:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":8} +{"time":"1970-01-01T05:00:00.0","update":"SecurityVersion increased","user":"admin","package":"frontend","new version":12}`) + +func TestConsolidateManifest(t *testing.T) { + assert := assert.New(t) + log := testLog + + manifest, err := consolidateManifest([]byte(test.ManifestJSON), log) + assert.NoError(err) + assert.Contains(manifest, `"SecurityVersion": 12`) + assert.NotContains(manifest, `"RecoveryKeys"`) +} + +func TestDecodeManifest(t *testing.T) { + assert := assert.New(t) + + manifestRaw := base64.StdEncoding.EncodeToString([]byte(test.ManifestJSON)) + + manifest, err := decodeManifest(context.Background(), false, manifestRaw, &stubGetter{}) + assert.NoError(err) + assert.Equal(test.ManifestJSON, manifest) + + getter := &stubGetter{response: testLog} + manifest, err = decodeManifest(context.Background(), true, string(manifestRaw), getter) + assert.NoError(err) + assert.Contains(manifest, `"SecurityVersion": 12`) +} + +func TestRemoveNil(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + testMap := map[string]interface{}{ + "1": "TestValue", + "2": map[string]interface{}{ + "2.1": "TestValue", + "2.2": nil, + }, + "3": nil, + "4": map[string]interface{}{ + "4.1": map[string]interface{}{ + "4.1.1": nil, + "4.1.2": map[string]interface{}{}, + }, + }, + } + + rawMap, err := json.Marshal(testMap) + require.NoError(err) + + removeNil(testMap) + + removedMap, err := json.Marshal(testMap) + require.NoError(err) + assert.NotEqual(rawMap, removedMap) + // three should be removed since its nil + assert.NotContains(removedMap, `"3"`) + // 2.2 should be removed since its nil, but 2 stays since 2.1 is not nil + assert.NotContains(removedMap, `"2.2"`) + // 4 should be removed completly since it only contains empty maps + assert.NotContains(removedMap, `"4"`) +} + +func TestCliManifestSet(t *testing.T) { + someErr := errors.New("failed") + testCases := map[string]struct { + poster *stubPoster + file *file.Handler + wantErr bool + }{ + "success": { + poster: &stubPoster{}, + file: file.New("unit-test", afero.NewMemMapFs()), + }, + "success with secrets": { + poster: &stubPoster{response: []byte("secret")}, + file: nil, + }, + "success with secrets and file": { + poster: &stubPoster{response: []byte("secret")}, + file: file.New("unit-test", afero.NewMemMapFs()), + }, + "post error": { + poster: &stubPoster{err: someErr}, + wantErr: true, + }, + "writing file error": { + poster: &stubPoster{response: []byte("secret")}, + file: file.New("unit-test", afero.NewReadOnlyFs(afero.NewMemMapFs())), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliManifestSet(cmd, []byte("manifest"), tc.file, tc.poster) + + if tc.wantErr { + assert.Error(err) + return + } + + require.NoError(err) + assert.Contains(out.String(), "Manifest successfully set") + assert.Equal(rest.ManifestEndpoint, tc.poster.requestPath) + assert.Equal(rest.ContentJSON, tc.poster.header) + + if tc.poster.response != nil { + if tc.file != nil { + manifestResponse, err := tc.file.Read() + require.NoError(err) + assert.Equal(tc.poster.response, manifestResponse) + } else { + assert.Contains(out.String(), string(tc.poster.response)) + } + } + }) + } +} + +func TestCliManifestUpdateApply(t *testing.T) { + testCases := map[string]struct { + poster *stubPoster + wantErr bool + }{ + "success": { + poster: &stubPoster{}, + wantErr: false, + }, + "error": { + poster: &stubPoster{ + err: errors.New("failed"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliManifestUpdateApply(cmd, []byte("manifest"), tc.poster) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Contains(out.String(), "Update manifest set successfully") + assert.Equal(rest.UpdateEndpoint, tc.poster.requestPath) + assert.Equal(rest.ContentJSON, tc.poster.header) + }) + } +} + +func TestLoadManifestFile(t *testing.T) { + require := require.New(t) + + testCases := map[string]struct { + file *file.Handler + wantErr bool + }{ + "json data": { + file: func() *file.Handler { + file := file.New("unit-test", afero.NewMemMapFs()) + require.NoError(file.Write([]byte(`{"Packages": {"APackage": {"SignerID": "1234","ProductID": 0,"SecurityVersion": 0,"Debug": false}}}`))) + return file + }(), + }, + "yaml data": { + file: func() *file.Handler { + file := file.New("unit-test", afero.NewMemMapFs()) + require.NoError(file.Write([]byte(` +Package: + SomePackage: + Debug: false + ProductID: 0 + SecurityVersion: 0 + SignerID: "1234" +`))) + return file + }(), + }, + "invalid data": { + file: func() *file.Handler { + file := file.New("unit-test", afero.NewMemMapFs()) + require.NoError(file.Write([]byte(` + Invalid YAML: + This should return an error`))) + return file + }(), + wantErr: true, + }, + "file not found": { + file: file.New("unit-test", afero.NewReadOnlyFs(afero.NewMemMapFs())), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + dataJSON, err := loadManifestFile(tc.file) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.True(json.Valid(dataJSON)) + }) + } +} + +func TestCliManifestSignature(t *testing.T) { + assert := assert.New(t) + + testValue := []byte("Test") + hash := sha256.Sum256(testValue) + signature := hex.EncodeToString(hash[:]) + assert.Equal(signature, cliManifestSignature(testValue)) +} + +func TestCliManifestVerify(t *testing.T) { + testCases := map[string]struct { + localSignature string + getter *stubGetter + wantErr bool + }{ + "success": { + localSignature: "signature", + getter: &stubGetter{response: []byte(`{"ManifestSignature": "signature"}`)}, + }, + "get error": { + localSignature: "signature", + getter: &stubGetter{err: errors.New("failed")}, + wantErr: true, + }, + "invalid signature": { + localSignature: "signature", + getter: &stubGetter{response: []byte(`{"ManifestSignature": "invalid"}`)}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliManifestVerify(cmd, tc.localSignature, tc.getter) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal("OK\n", out.String()) + }) + } +} + +func TestGetSignatureFromString(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fs := afero.Afero{Fs: afero.NewMemMapFs()} + + testValue := []byte("TestSignature") + hash := sha256.Sum256(testValue) + directSignature := hex.EncodeToString(hash[:]) + + filename := "testSignature" + require.NoError(fs.WriteFile(filename, testValue, 0o644)) + + testSignature1, err := getSignatureFromString(directSignature, fs) + assert.NoError(err) + assert.Equal(directSignature, testSignature1) + + testSignature2, err := getSignatureFromString(filename, fs) + assert.NoError(err) + assert.Equal(directSignature, testSignature2) + + _, err = getSignatureFromString("invalidFilename", fs) + assert.Error(err) +} + +func TestManifestUpdateAcknowledge(t *testing.T) { + testCases := map[string]struct { + poster *stubPoster + wantErr bool + }{ + "success": { + poster: &stubPoster{response: []byte("response")}, + wantErr: false, + }, + "error": { + poster: &stubPoster{ + err: errors.New("failed"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliManifestUpdateAcknowledge(cmd, []byte("manifest"), tc.poster) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Contains(out.String(), "Acknowledgement successful") + assert.Contains(out.String(), string(tc.poster.response)) + assert.Equal(rest.UpdateStatusEndpoint, tc.poster.requestPath) + assert.Equal(rest.ContentJSON, tc.poster.header) + }) + } +} + +func TestManifestUpdateGet(t *testing.T) { + testCases := map[string]struct { + getter *stubGetter + displayMissing bool + wantErr bool + }{ + "success": { + getter: &stubGetter{ + response: []byte(`{"manifest": "bWFuaWZlc3Q=", "missingUsers": ["user1", "user2"]}`), + }, + }, + "success display missing": { + getter: &stubGetter{ + response: []byte(`{"manifest": "bWFuaWZlc3Q=", "missingUsers": ["user1", "user2"]}`), + }, + displayMissing: true, + }, + "get error": { + getter: &stubGetter{err: errors.New("failed")}, + wantErr: true, + }, + "invalid manifest encoding": { + getter: &stubGetter{ + response: []byte(`{"manifest": "_invalid_data_", "missingUsers": ["user1", "user2"]}`), + }, + displayMissing: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var out bytes.Buffer + + err := cliManifestUpdateGet(context.Background(), &out, tc.displayMissing, tc.getter) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.NotEmpty(out.String()) + }) + } +} + +func TestManifestUpdateCancel(t *testing.T) { + testCases := map[string]struct { + poster *stubPoster + wantErr bool + }{ + "success": { + poster: &stubPoster{}, + wantErr: false, + }, + "error": { + poster: &stubPoster{ + err: errors.New("failed"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliManifestUpdateCancel(cmd, tc.poster) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Contains(out.String(), "Cancellation successful") + assert.Equal(rest.UpdateCancelEndpoint, tc.poster.requestPath) + }) + } +} diff --git a/cli/cmd/packageInfo.go b/cli/internal/cmd/packageInfo.go similarity index 60% rename from cli/cmd/packageInfo.go rename to cli/internal/cmd/packageInfo.go index fc119752..82b69b96 100644 --- a/cli/cmd/packageInfo.go +++ b/cli/internal/cmd/packageInfo.go @@ -14,78 +14,76 @@ import ( "encoding/hex" "errors" "fmt" - "io/ioutil" + "io" "os" "path/filepath" - "github.com/fatih/color" "github.com/spf13/cobra" ) -func newPackageInfoCmd() *cobra.Command { +func NewPackageInfoCmd() *cobra.Command { cmd := &cobra.Command{ Use: "package-info", - Short: "Prints the package signature properties of an enclave", - Long: "Prints the package signature properties of an enclave", + Short: "Print the package signature properties of an enclave", + Long: "Print the package signature properties of an enclave", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - path := args[0] + RunE: runPackageInfo, + } - // Check if given filename is actually a directory - stat, err := os.Stat(path) - if err != nil { - return err - } - isDirectory := stat.IsDir() - - // For Open Enclave / Edgeless RT / EGo, we require to directly point to the signed enclave binary, as these do not have a specific directory structure - var errOpenEnclave error - if !isDirectory { - errOpenEnclave = decodeOpenEnclaveSigStruct(path) - if errOpenEnclave == nil { - return nil - } - } + return cmd +} - // In every other case, try to guess if it's a directory, or expect a specific file to be pointed to - errGramine := decodeGramineSigStruct(path, isDirectory) - if errGramine == nil { - return nil - } - errOcclum := decodeSGXSDKSigStruct(path, isDirectory) // Either Occlum or SGX SDK - if errOcclum == nil { - return nil - } +func runPackageInfo(cmd *cobra.Command, args []string) error { + path := args[0] - color.Red("ERROR: Failed to automatically determine SGX package signature properties detection.") - if isDirectory { - color.Red("A directory was supplied, but it appears not to be a Gramine or Occlum instance.") - color.Red("Please either specify the .sig file (Gramine) or SGX enclave binary (Occlum / SGX SDK) directly, or the root directory of an Gramine or Occlum instance.") - } + // Check if given filename is actually a directory + stat, err := os.Stat(path) + if err != nil { + return err + } + isDirectory := stat.IsDir() + + // For Open Enclave / Edgeless RT / EGo, we require to directly point to the signed enclave binary, as these do not have a specific directory structure + var errOpenEnclave error + if !isDirectory { + errOpenEnclave = decodeOpenEnclaveSigStruct(cmd.OutOrStdout(), path) + if errOpenEnclave == nil { + return nil + } + } - fmt.Printf("\n") - // Output exact errors for each detection method to user - if errOpenEnclave != nil { - color.Red("Open Enclave detection error: %v\n", errOpenEnclave) - } - fmt.Printf("Error - Gramine: %v\n", errGramine) - fmt.Printf("Error - Occlum / SGX SDK: %v\n", errOcclum) - return errors.New("unable to determine enclave properties") - }, - SilenceUsage: true, + // In every other case, try to guess if it's a directory, or expect a specific file to be pointed to + errGramine := decodeGramineSigStruct(cmd.OutOrStdout(), path, isDirectory) + if errGramine == nil { + return nil + } + errOcclum := decodeSGXSDKSigStruct(cmd.OutOrStdout(), path, isDirectory) // Either Occlum or SGX SDK + if errOcclum == nil { + return nil } - return cmd + cmd.PrintErrln("ERROR: Failed to automatically determine SGX package signature properties.") + if isDirectory { + cmd.PrintErrln("A directory was supplied, but it appears not to be a Gramine or Occlum instance.") + cmd.PrintErrln("Please either specify the .sig file (Gramine) or SGX enclave binary (Occlum / SGX SDK) directly, or the root directory of an Gramine or Occlum instance.") + } + cmd.Println() + + // Output exact errors for each detection method to user + cmd.PrintErrf("Open Enclave detection error: %s\n", errOpenEnclave) + cmd.PrintErrf("Error - Gramine: %s\n", errGramine) + cmd.PrintErrf("Error - Occlum / SGX SDK: %s\n", errOcclum) + return errors.New("unable to determine enclave properties") } -func decodeSGXSDKSigStruct(path string, isDirectory bool) error { +func decodeSGXSDKSigStruct(out io.Writer, path string, isDirectory bool) error { // If the path is a directory, we try to find out if it's an Occlum image directory var elfFile *elf.File var isOcclumInstance bool var err error if isDirectory { if elfFile, err = elf.Open(filepath.Join(path, "build/lib/libocclum-libos.signed.so")); err == nil { - color.Green("Detected Occlum image.") + fmt.Fprintln(out, "Detected Occlum image.") isOcclumInstance = true } } else { @@ -115,12 +113,12 @@ func decodeSGXSDKSigStruct(path string, isDirectory bool) error { // Display the determined properties if isOcclumInstance { - color.Cyan("PackageProperties for Occlum image at '%s':\n", path) + fmt.Fprintf(out, "PackageProperties for Occlum image at '%s':\n", path) } else { - color.Cyan("PackageProperties for '%s':\n", path) + fmt.Fprintf(out, "PackageProperties for '%s':\n", path) } - printPackageProperties(mrenclave, mrsigner, isvprodid, isvsvn) + printPackageProperties(out, mrenclave, mrsigner, isvprodid, isvsvn) return nil } @@ -168,11 +166,11 @@ func parseSigStruct(sgxMetaData []byte) ([]byte, []byte, []byte, []byte, error) return mrenclave, mrsigner[:], isvprodid, isvsvn, nil } -func decodeGramineSigStruct(path string, isDirectory bool) error { +func decodeGramineSigStruct(out io.Writer, path string, isDirectory bool) error { // Check if directory contains a file ending in .sig var sigFile string if isDirectory { - fsInfo, err := ioutil.ReadDir(path) + fsInfo, err := os.ReadDir(path) if err != nil { return err } @@ -184,7 +182,7 @@ func decodeGramineSigStruct(path string, isDirectory bool) error { } foundSigFile = true sigFile = entry.Name() - color.Green("Detected Gramine instance.") + fmt.Fprintln(out, "Detected Gramine instance.") } } if !foundSigFile { @@ -196,7 +194,7 @@ func decodeGramineSigStruct(path string, isDirectory bool) error { } // Try to load the file - sigContent, err := ioutil.ReadFile(sigFile) + sigContent, err := os.ReadFile(sigFile) if err != nil { return err } @@ -207,24 +205,24 @@ func decodeGramineSigStruct(path string, isDirectory bool) error { } if isDirectory { - color.Cyan("PackageProperties for Gramine instance at '%s':\n", path) + fmt.Fprintf(out, "PackageProperties for Gramine instance at '%s':\n", path) } else { - color.Cyan("PackageProperties for '%s':\n", path) + fmt.Fprintf(out, "PackageProperties for '%s':\n", path) } - printPackageProperties(mrenclave, mrsigner[:], isvprodid, isvsvn) + printPackageProperties(out, mrenclave, mrsigner[:], isvprodid, isvsvn) return nil } -func printPackageProperties(mrenclave []byte, mrsigner []byte, isvprodid []byte, isvsvn []byte) { - fmt.Printf("UniqueID (MRENCLAVE) : %s\n", hex.EncodeToString(mrenclave)) - fmt.Printf("SignerID (MRSIGNER) : %s\n", hex.EncodeToString(mrsigner[:])) - fmt.Printf("ProductID (ISVPRODID) : %d\n", binary.LittleEndian.Uint16(isvprodid)) - fmt.Printf("SecurityVersion (ISVSVN) : %d\n", binary.LittleEndian.Uint16(isvsvn)) +func printPackageProperties(out io.Writer, mrenclave []byte, mrsigner []byte, isvprodid []byte, isvsvn []byte) { + fmt.Fprintf(out, "UniqueID (MRENCLAVE) : %s\n", hex.EncodeToString(mrenclave)) + fmt.Fprintf(out, "SignerID (MRSIGNER) : %s\n", hex.EncodeToString(mrsigner[:])) + fmt.Fprintf(out, "ProductID (ISVPRODID) : %d\n", binary.LittleEndian.Uint16(isvprodid)) + fmt.Fprintf(out, "SecurityVersion (ISVSVN) : %d\n", binary.LittleEndian.Uint16(isvsvn)) } -func decodeOpenEnclaveSigStruct(path string) error { +func decodeOpenEnclaveSigStruct(out io.Writer, path string) error { // Open ELF file elfFile, err := elf.Open(path) if err != nil { @@ -251,8 +249,8 @@ func decodeOpenEnclaveSigStruct(path string) error { } // Print PackageProperties of detected SIGSTRUCT - color.Cyan("PackageProperties for '%s':\n", path) - printPackageProperties(mrenclave, mrsigner[:], isvprodid, isvsvn) + fmt.Fprintf(out, "PackageProperties for '%s':\n", path) + printPackageProperties(out, mrenclave, mrsigner[:], isvprodid, isvsvn) return nil } diff --git a/cli/cmd/packageInfo_test.go b/cli/internal/cmd/packageInfo_test.go similarity index 98% rename from cli/cmd/packageInfo_test.go rename to cli/internal/cmd/packageInfo_test.go index fafaf19e..06b4b11b 100644 --- a/cli/cmd/packageInfo_test.go +++ b/cli/internal/cmd/packageInfo_test.go @@ -12,7 +12,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/hex" - "io/ioutil" + "io" "testing" "github.com/stretchr/testify/assert" @@ -33,7 +33,7 @@ func TestParseSigStruct(t *testing.T) { r, err := zlib.NewReader(bytes.NewReader(sgxMetaDataCompressed)) require.NoError(err) defer r.Close() - sgxMetaData, err := ioutil.ReadAll(r) + sgxMetaData, err := io.ReadAll(r) require.NoError(err) // Parse SIGSTRUCT and verify against known results diff --git a/cli/cmd/precheck.go b/cli/internal/cmd/precheck.go similarity index 75% rename from cli/cmd/precheck.go rename to cli/internal/cmd/precheck.go index 6db6bcf0..497ef6c8 100644 --- a/cli/cmd/precheck.go +++ b/cli/internal/cmd/precheck.go @@ -7,9 +7,7 @@ package cmd import ( - "context" - "fmt" - + "github.com/edgelesssys/marblerun/cli/internal/kube" "github.com/edgelesssys/marblerun/util" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -17,27 +15,28 @@ import ( "k8s.io/client-go/kubernetes" ) -func newPrecheckCmd() *cobra.Command { +func NewPrecheckCmd() *cobra.Command { cmd := &cobra.Command{ Use: "precheck", Short: "Check if your Kubernetes cluster supports SGX", Long: `Check if your Kubernetes cluster supports SGX`, Args: cobra.NoArgs, - RunE: func(cobracmd *cobra.Command, args []string) error { - kubeClient, err := getKubernetesInterface() - if err != nil { - return err - } - return cliCheckSGXSupport(kubeClient) - }, - SilenceUsage: true, + RunE: runPrecheck, } return cmd } -func cliCheckSGXSupport(kubeClient kubernetes.Interface) error { - nodes, err := kubeClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) +func runPrecheck(cmd *cobra.Command, args []string) error { + kubeClient, err := kube.NewClient() + if err != nil { + return err + } + return cliCheckSGXSupport(cmd, kubeClient) +} + +func cliCheckSGXSupport(cmd *cobra.Command, kubeClient kubernetes.Interface) error { + nodes, err := kubeClient.CoreV1().Nodes().List(cmd.Context(), metav1.ListOptions{}) if err != nil { return err } @@ -52,17 +51,17 @@ func cliCheckSGXSupport(kubeClient kubernetes.Interface) error { } if supportedNodes == 0 { - fmt.Println("Cluster does not support SGX, you may still run MarbleRun in simulation mode") - fmt.Println("To install MarbleRun run [marblerun install --simulation]") - fmt.Println("If your nodes have SGX support you might be missing an SGX device plugin") - fmt.Println("Check https://edglss.cc/doc-mr-k8s-prereq for more information") + cmd.Println("Cluster does not support SGX, you may still run MarbleRun in simulation mode") + cmd.Println("To install MarbleRun run [marblerun install --simulation]") + cmd.Println("If your nodes have SGX support you might be missing an SGX device plugin") + cmd.Println("Check https://edglss.cc/doc-mr-k8s-prereq for more information") } else { nodeString := "node" if supportedNodes > 1 { nodeString = nodeString + "s" } - fmt.Printf("Cluster supports SGX on %d %s\n", supportedNodes, nodeString) - fmt.Println("To install MarbleRun run [marblerun install]") + cmd.Printf("Cluster supports SGX on %d %s\n", supportedNodes, nodeString) + cmd.Println("To install MarbleRun run [marblerun install]") } return nil diff --git a/cli/cmd/precheck_test.go b/cli/internal/cmd/precheck_test.go similarity index 66% rename from cli/cmd/precheck_test.go rename to cli/internal/cmd/precheck_test.go index 7514aaa1..c2f4b870 100644 --- a/cli/cmd/precheck_test.go +++ b/cli/internal/cmd/precheck_test.go @@ -7,10 +7,12 @@ package cmd import ( + "bytes" "context" "testing" "github.com/edgelesssys/marblerun/util" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -30,16 +32,18 @@ func TestNodeSupportsSGX(t *testing.T) { Name: "regular-node", }, } - _, err := testClient.CoreV1().Nodes().Create(context.TODO(), testNode, metav1.CreateOptions{}) + ctx := context.Background() + + _, err := testClient.CoreV1().Nodes().Create(ctx, testNode, metav1.CreateOptions{}) require.NoError(err) - nodes, err := testClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + nodes, err := testClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) require.NoError(err) supportsSGX := nodeSupportsSGX(nodes.Items[0].Status.Capacity) assert.False(supportsSGX, "function returned true for nodes not supporting SGX") - err = testClient.CoreV1().Nodes().Delete(context.TODO(), "regular-node", metav1.DeleteOptions{}) + err = testClient.CoreV1().Nodes().Delete(ctx, "regular-node", metav1.DeleteOptions{}) require.NoError(err) // Test Intel Device Plugin @@ -55,16 +59,16 @@ func TestNodeSupportsSGX(t *testing.T) { }, }, } - _, err = testClient.CoreV1().Nodes().Create(context.TODO(), intelSGXNode, metav1.CreateOptions{}) + _, err = testClient.CoreV1().Nodes().Create(ctx, intelSGXNode, metav1.CreateOptions{}) require.NoError(err) - nodes, err = testClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + nodes, err = testClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) require.NoError(err) supportsSGX = nodeSupportsSGX(nodes.Items[0].Status.Capacity) assert.True(supportsSGX, "function returned false for nodes supporting SGX") - err = testClient.CoreV1().Nodes().Delete(context.TODO(), "intel-sgx-node", metav1.DeleteOptions{}) + err = testClient.CoreV1().Nodes().Delete(ctx, "intel-sgx-node", metav1.DeleteOptions{}) require.NoError(err) // test Azure Device Plugin @@ -78,10 +82,10 @@ func TestNodeSupportsSGX(t *testing.T) { }, }, } - _, err = testClient.CoreV1().Nodes().Create(context.TODO(), azureSGXNode, metav1.CreateOptions{}) + _, err = testClient.CoreV1().Nodes().Create(ctx, azureSGXNode, metav1.CreateOptions{}) require.NoError(err) - nodes, err = testClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + nodes, err = testClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) require.NoError(err) supportsSGX = nodeSupportsSGX(nodes.Items[0].Status.Capacity) @@ -93,19 +97,27 @@ func TestCliCheckSGXSupport(t *testing.T) { require := require.New(t) testClient := fake.NewSimpleClientset() + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + testNode := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "regular-node", }, } - _, err := testClient.CoreV1().Nodes().Create(context.TODO(), testNode, metav1.CreateOptions{}) + ctx := context.Background() + + _, err := testClient.CoreV1().Nodes().Create(ctx, testNode, metav1.CreateOptions{}) require.NoError(err) - _, err = testClient.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + _, err = testClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) require.NoError(err) - err = cliCheckSGXSupport(testClient) + err = cliCheckSGXSupport(cmd, testClient) assert.NoError(err) + assert.Contains(out.String(), "--simulation") + out.Reset() // Test node supporting SGX testNodeSGX := &corev1.Node{ @@ -120,16 +132,20 @@ func TestCliCheckSGXSupport(t *testing.T) { }, }, } - _, err = testClient.CoreV1().Nodes().Create(context.TODO(), testNodeSGX, metav1.CreateOptions{}) + _, err = testClient.CoreV1().Nodes().Create(ctx, testNodeSGX, metav1.CreateOptions{}) require.NoError(err) - err = cliCheckSGXSupport(testClient) + err = cliCheckSGXSupport(cmd, testClient) assert.NoError(err) + assert.Contains(out.String(), "1 node") + out.Reset() testNodeSGX.ObjectMeta.Name = "sgx-node-2" - _, err = testClient.CoreV1().Nodes().Create(context.TODO(), testNodeSGX, metav1.CreateOptions{}) + _, err = testClient.CoreV1().Nodes().Create(ctx, testNodeSGX, metav1.CreateOptions{}) require.NoError(err) - err = cliCheckSGXSupport(testClient) + err = cliCheckSGXSupport(cmd, testClient) assert.NoError(err) + assert.Contains(out.String(), "2 nodes") + out.Reset() } diff --git a/cli/internal/cmd/recover.go b/cli/internal/cmd/recover.go new file mode 100644 index 00000000..e900329d --- /dev/null +++ b/cli/internal/cmd/recover.go @@ -0,0 +1,60 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "fmt" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/tidwall/gjson" +) + +func NewRecoverCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "recover ", + Short: "Recover the MarbleRun Coordinator from a sealed state", + Long: "Recover the MarbleRun Coordinator from a sealed state", + Example: "marblerun recover recovery_key_decrypted $MARBLERUN", + Args: cobra.ExactArgs(2), + RunE: runRecover, + } + + return cmd +} + +func runRecover(cmd *cobra.Command, args []string) error { + keyFile := args[0] + hostname := args[1] + + recoveryKey, err := file.New(keyFile, afero.NewOsFs()).Read() + if err != nil { + return err + } + + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + cmd.Println("Successfully verified Coordinator, now uploading key") + return cliRecover(cmd, recoveryKey, client) +} + +// cliRecover tries to unseal the Coordinator by uploading the recovery key. +func cliRecover(cmd *cobra.Command, key []byte, client poster) error { + resp, err := client.Post(cmd.Context(), rest.RecoverEndpoint, rest.ContentPlain, bytes.NewReader(key)) + if err != nil { + return fmt.Errorf("recovering Coordinator: %w", err) + } + + response := gjson.GetBytes(resp, "StatusMessage") + cmd.Println(response.String()) + return nil +} diff --git a/cli/internal/cmd/recover_test.go b/cli/internal/cmd/recover_test.go new file mode 100644 index 00000000..791985a5 --- /dev/null +++ b/cli/internal/cmd/recover_test.go @@ -0,0 +1,57 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "errors" + "testing" + + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCliRecover(t *testing.T) { + testCases := map[string]struct { + getter *stubPoster + wantErr bool + }{ + "success": { + getter: &stubPoster{ + response: []byte(`{"StatusMessage":"Success"}`), + }, + }, + "get error": { + getter: &stubPoster{ + err: errors.New("failed"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliRecover(cmd, []byte{0x00}, tc.getter) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal(rest.RecoverEndpoint, tc.getter.requestPath) + assert.Equal(rest.ContentPlain, tc.getter.header) + assert.Equal("Success\n", out.String()) + }) + } +} diff --git a/cli/internal/cmd/secret.go b/cli/internal/cmd/secret.go new file mode 100644 index 00000000..d5418a27 --- /dev/null +++ b/cli/internal/cmd/secret.go @@ -0,0 +1,31 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func NewSecretCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "secret", + Short: "Manage secrets for the MarbleRun Coordinator", + Long: ` +Manage secrets for the MarbleRun Coordinator. +Set or retrieve a secret defined in the manifest.`, + } + + cmd.PersistentFlags().StringP("cert", "c", "", "PEM encoded MarbleRun user certificate file (required)") + cmd.PersistentFlags().StringP("key", "k", "", "PEM encoded MarbleRun user key file (required)") + must(cmd.MarkPersistentFlagRequired("key")) + must(cmd.MarkPersistentFlagRequired("cert")) + + cmd.AddCommand(newSecretSet()) + cmd.AddCommand(newSecretGet()) + + return cmd +} diff --git a/cli/cmd/secretGet.go b/cli/internal/cmd/secretGet.go similarity index 53% rename from cli/cmd/secretGet.go rename to cli/internal/cmd/secretGet.go index 063937d5..50299853 100644 --- a/cli/cmd/secretGet.go +++ b/cli/internal/cmd/secretGet.go @@ -7,30 +7,19 @@ package cmd import ( - "crypto/tls" - "encoding/pem" "fmt" "io" - "io/ioutil" "net/http" - "net/url" + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" "github.com/edgelesssys/marblerun/coordinator/manifest" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/tidwall/gjson" ) -type secretGetOptions struct { - host string - secretIDs []string - output string - clCert tls.Certificate - caCert []*pem.Block -} - func newSecretGet() *cobra.Command { - options := &secretGetOptions{} - cmd := &cobra.Command{ Use: "get SECRETNAME ... ", Short: "Retrieve secrets from the MarbleRun Coordinator", @@ -41,96 +30,56 @@ and need permissions in the manifest to read the requested secrets. `, Example: "marblerun secret get genericSecret symmetricKeyShared $MARBLERUN -c admin.crt -k admin.key", Args: cobra.MinimumNArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - hostName := args[len(args)-1] - caCert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - - // Load client certificate and key - clCert, err := tls.LoadX509KeyPair(userCertFile, userKeyFile) - if err != nil { - return err - } - - options.secretIDs = args[0 : len(args)-1] - options.host = hostName - options.caCert = caCert - options.clCert = clCert - - return cliSecretGet(cmd.OutOrStdout(), options) - }, - SilenceUsage: true, + RunE: runSecretGet, } - cmd.Flags().StringVarP(&options.output, "output", "o", "", "File to save the secret to") + cmd.Flags().StringP("output", "o", "", "File to save the secret to") return cmd } -// cliSecretGet requests one or more secrets from the MarbleRun Coordinator. -func cliSecretGet(out io.Writer, o *secretGetOptions) error { - client, err := restClient(o.caCert, &o.clCert) +func runSecretGet(cmd *cobra.Command, args []string) error { + hostname := args[len(args)-1] + secretIDs := args[0 : len(args)-1] + + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + client, err := rest.NewAuthenticatedClient(cmd, hostname) if err != nil { return err } - secretQuery := url.Values{} + return cliSecretGet(cmd, secretIDs, file.New(output, afero.NewOsFs()), client) +} - for _, secret := range o.secretIDs { - secretQuery.Add("s", secret) +// cliSecretGet requests one or more secrets from the MarbleRun Coordinator. +func cliSecretGet(cmd *cobra.Command, secretIDs []string, file *file.Handler, client getter) error { + var query []string + for _, secretID := range secretIDs { + query = append(query, "s", secretID) } - url := url.URL{Scheme: "https", Host: o.host, Path: "secrets", RawQuery: secretQuery.Encode()} - resp, err := client.Get(url.String()) + + resp, err := client.Get(cmd.Context(), rest.SecretEndpoint, http.NoBody, query...) if err != nil { - return err + return fmt.Errorf("retrieving secret: %w", err) } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusOK: - // Everything went fine, print the secret or save to file - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - response := gjson.GetBytes(respBody, "data") - if len(response.String()) <= 0 { - return fmt.Errorf("received empty secret response") - } + response := gjson.ParseBytes(resp) + if len(response.Map()) != len(secretIDs) { + return fmt.Errorf("did not receive the same number of secrets as requested") + } - if len(response.Map()) != len(o.secretIDs) { - return fmt.Errorf("did not receive the same number of secrets as requested") - } + if file == nil { + return printSecrets(cmd.OutOrStdout(), response) + } - if o.output == "" { - return printSecrets(out, response) - } - if err := ioutil.WriteFile(o.output, []byte(response.String()), 0o644); err != nil { - return err - } - fmt.Fprintf(out, "Saved secret to: %s\n", o.output) - case http.StatusBadRequest: - // Something went wrong - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf("unable to retrieve secret: %s", response) - case http.StatusUnauthorized: - // User was not authorized - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf("unable to authorize user: %s", response) - default: - return fmt.Errorf("error connecting to server: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + if err := file.Write([]byte(response.String())); err != nil { + return err } + cmd.Printf("Saved secrets to: %s\n", file.Name()) return nil } diff --git a/cli/cmd/secretSet.go b/cli/internal/cmd/secretSet.go similarity index 53% rename from cli/cmd/secretSet.go rename to cli/internal/cmd/secretSet.go index 86990b02..ecc52aca 100644 --- a/cli/cmd/secretSet.go +++ b/cli/internal/cmd/secretSet.go @@ -8,24 +8,20 @@ package cmd import ( "bytes" - "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" "fmt" - "io/ioutil" - "net/http" - "net/url" "strings" + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" "github.com/edgelesssys/marblerun/coordinator/manifest" + "github.com/spf13/afero" "github.com/spf13/cobra" - "github.com/tidwall/gjson" ) func newSecretSet() *cobra.Command { - var pemSecretName string - cmd := &cobra.Command{ Use: "set ", Short: "Set a secret for the MarbleRun Coordinator", @@ -43,87 +39,57 @@ marblerun secret set secret.json $MARBLERUN -c admin.crt -k admin.key # Set a secret from a PEM encoded file marblerun secret set certificate.pem $MARBLERUN -c admin.crt -k admin.key --from-pem certificateSecret`, Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - secretFile := args[0] - hostName := args[1] - - caCert, err := verifyCoordinator(cmd.OutOrStdout(), hostName, eraConfig, insecureEra, acceptedTCBStatuses) - if err != nil { - return err - } - - // Load client certificate and key - clCert, err := tls.LoadX509KeyPair(userCertFile, userKeyFile) - if err != nil { - return err - } - - newSecrets, err := ioutil.ReadFile(secretFile) - if err != nil { - return err - } - - if len(pemSecretName) > 0 { - newSecrets, err = loadSecretFromPEM(pemSecretName, newSecrets) - if err != nil { - return err - } - } - - return cliSecretSet(hostName, newSecrets, clCert, caCert) - }, - SilenceUsage: true, + RunE: runSecretSet, } - cmd.Flags().StringVar(&pemSecretName, "from-pem", "", "name of the secret from a PEM encoded file") + cmd.Flags().String("from-pem", "", "name of the secret from a PEM encoded file") return cmd } -// cliSecretSet sets one or more secrets using a secrets manifest. -func cliSecretSet(host string, newSecrets []byte, clCert tls.Certificate, caCert []*pem.Block) error { - client, err := restClient(caCert, &clCert) +func runSecretSet(cmd *cobra.Command, args []string) error { + secretFile := args[0] + hostname := args[1] + + fromPem, err := cmd.Flags().GetString("from-pem") if err != nil { return err } - url := url.URL{Scheme: "https", Host: host, Path: "secrets"} - resp, err := client.Post(url.String(), "application/json", bytes.NewReader(newSecrets)) + newSecrets, err := file.New(secretFile, afero.NewOsFs()).Read() if err != nil { return err } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusOK: - // Everything went fine, just print the success message - fmt.Println("Secret successfully set") - case http.StatusBadRequest: - // Something went wrong - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf("unable to set secret: %s", response) - case http.StatusUnauthorized: - // User was not authorized - respBody, err := ioutil.ReadAll(resp.Body) + + if fromPem != "" { + newSecrets, err = createSecretFromPEM(fromPem, newSecrets) if err != nil { return err } - response := gjson.GetBytes(respBody, "message") - return fmt.Errorf("unable to authorize user: %s", response) - default: - return fmt.Errorf("error connecting to server: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) } + client, err := rest.NewAuthenticatedClient(cmd, hostname) + if err != nil { + return err + } + + return cliSecretSet(cmd, newSecrets, client) +} + +// cliSecretSet sets one or more secrets using a secrets manifest. +func cliSecretSet(cmd *cobra.Command, newSecrets []byte, client poster) error { + _, err := client.Post(cmd.Context(), rest.SecretEndpoint, rest.ContentJSON, bytes.NewReader(newSecrets)) + if err != nil { + return fmt.Errorf("setting secret: %w", err) + } + + cmd.Println("Secret successfully set") return nil } -// loadSecretFromPEM creates a JSON string from a certificate and/or private key in PEM format. +// createSecretFromPEM creates a JSON string from a certificate and/or private key in PEM format. // If the PEM data contains more than one cert of key only the first instance will be part of the secret. -func loadSecretFromPEM(secretName string, rawPEM []byte) ([]byte, error) { +func createSecretFromPEM(secretName string, rawPEM []byte) ([]byte, error) { newSecret := manifest.UserSecret{} for { block, rest := pem.Decode(rawPEM) diff --git a/cli/internal/cmd/secret_test.go b/cli/internal/cmd/secret_test.go new file mode 100644 index 00000000..6c518fd7 --- /dev/null +++ b/cli/internal/cmd/secret_test.go @@ -0,0 +1,175 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "testing" + + "github.com/edgelesssys/marblerun/cli/internal/file" + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/edgelesssys/marblerun/coordinator/manifest" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSetSecrets(t *testing.T) { + testCases := map[string]struct { + poster *stubPoster + wantErr bool + }{ + "success": { + poster: &stubPoster{}, + wantErr: false, + }, + "error": { + poster: &stubPoster{ + err: errors.New("failed"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliSecretSet(cmd, []byte{0x00}, tc.poster) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Contains(out.String(), "Secret successfully set") + assert.Equal(rest.SecretEndpoint, tc.poster.requestPath) + assert.Equal(rest.ContentJSON, tc.poster.header) + }) + } +} + +func TestGetSecrets(t *testing.T) { + someErr := errors.New("failed") + testCases := map[string]struct { + getter *stubGetter + file *file.Handler + secretIDs []string + wantErr bool + }{ + "success": { + getter: &stubGetter{response: []byte(`{"test": "ABCDEF"}`)}, + file: file.New("unit-test", afero.NewMemMapFs()), + secretIDs: []string{"test"}, + }, + "get error": { + getter: &stubGetter{err: someErr}, + file: file.New("unit-test", afero.NewMemMapFs()), + secretIDs: []string{"test"}, + wantErr: true, + }, + "write error": { + getter: &stubGetter{response: []byte(`{"test": "ABCDEF"}`)}, + file: file.New("unit-test", afero.NewReadOnlyFs(afero.NewMemMapFs())), + secretIDs: []string{"test"}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + + err := cliSecretGet(cmd, tc.secretIDs, tc.file, tc.getter) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Contains(tc.getter.requestPath, rest.SecretEndpoint) + + expectedResponse, err := tc.file.Read() + require.NoError(t, err) + assert.Equal(expectedResponse, tc.getter.response) + for _, id := range tc.secretIDs { + assert.Contains(tc.getter.query, id) + } + }) + } +} + +func TestSecretFromPEM(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + const testCert = ` +-----BEGIN CERTIFICATE----- +MIICpjCCAg+gAwIBAgIUS5FDU/DJnN3hDISm2eAu7hVWqSUwDQYJKoZIhvcNAQEL +BQAwZTELMAkGA1UEBhMCREUxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM +EEVkZ2VsZXNzIFN5c3RlbXMxEjAQBgNVBAsMCVVuaXQgVGVzdDESMBAGA1UEAwwJ +VW5pdCBUZXN0MB4XDTIxMDYyMzA3NTAxMVoXDTIyMDYyMzA3NTAxMVowZTELMAkG +A1UEBhMCREUxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoMEEVkZ2VsZXNz +IFN5c3RlbXMxEjAQBgNVBAsMCVVuaXQgVGVzdDESMBAGA1UEAwwJVW5pdCBUZXN0 +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDO+ZG7pGx+/poBhr5Zt2lX0+Nh +kcWpYbWdbt69tJXhSWYlZmLeo6FJbeV11bX8zEwVPaDxhYSmlDq2tu9t8o1j8N01 +FAoWy4NnDyGEyx1bJGyGGcMN01mVqD+PTbmKeOuVGYchyz8YBub+k5Eft9l6MxuN +kA7SuJGv9fU3lTpQpQIDAQABo1MwUTAdBgNVHQ4EFgQUmD/6vklf6UsdUcZvOB2x +FeymJU0wHwYDVR0jBBgwFoAUmD/6vklf6UsdUcZvOB2xFeymJU0wDwYDVR0TAQH/ +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCxHFf2dQ+6O/ntEQr6zbHgU4jMidM+ +foF2RSiG5icffjDcjpxttJtpIK+iGh3yguGfWaaMVo72DPFPNAVmqHutoEr80chV +yr93zz66XkRPyMhopTeF3Ld1K3qAQ0CqtWck1kblgHCWJBGYgyngawoxSGhUMkSD +i6zr19jszrNxzg== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAM75kbukbH7+mgGG +vlm3aVfT42GRxalhtZ1u3r20leFJZiVmYt6joUlt5XXVtfzMTBU9oPGFhKaUOra2 +723yjWPw3TUUChbLg2cPIYTLHVskbIYZww3TWZWoP49NuYp465UZhyHLPxgG5v6T +kR+32XozG42QDtK4ka/19TeVOlClAgMBAAECgYEAyEX3vUEJ9wx3ixiN4hQ2q9SN +BiFeyVqRuSfKAnjWOquiWngrHVHqRDpBuXa05UvuJvN+Y5YV2HZAJgL3xUTZh+jV +sBgj65evWTUE3daVJBPoTDtBRmZCoEXNvonXbUNFExUwWfDaYOraZCSupP9Yg/0q +m1To7ktkWmS84JuVukECQQDmbVibBLYqIClFsEdNVuVjAq0OHHSsN5FEyD3joQso +JZ5EmCUnp/GvJ+yDgOyKY/gOVK9s9BYKEd7WQQBQ3Vs1AkEA5fHsHtYryPMTl3s7 +aycxjEEJyvpDr3y1Pk5tSGdj2YSTvKdkVYP3pJmA0JaCRL/2rqJx3pKuOm1/kOS8 +71xdsQJBAJAbLmC0T6CEwIr+tXjesVJ8Z/H9RdI2ZjlX6aykGLAg5pwLcqEcXP+n +vjh3tnbOEmIUACnpdKcTigMAX8wyw0kCQBpYpro9xdSHbWY822kCm527UfjsxdaU +jluuNr1GA13H3/mMoGVf8n7si6Laq+Besk/+EtfyrH3LUAN1AeTXC3ECQQCua5+L +Ra6Yym8Tq+6I6YFqee2NFPKrsKw2xrExhHjx/vv+V0SMXU/zBfZudCbPUcLQoH3q +LuL049+D8bu8Z+Fe +-----END PRIVATE KEY-----` + + secretName := "test-secret" + secret, err := createSecretFromPEM(secretName, []byte(testCert)) + assert.NoError(err) + + var secretMap map[string]manifest.Secret + err = json.Unmarshal(secret, &secretMap) + require.NoError(err) + + _, ok := secretMap[secretName] + require.True(ok) + assert.True(len(secretMap[secretName].Cert.Raw) > 0) + assert.True(len(secretMap[secretName].Private) > 0) + assert.True(len(secretMap[secretName].Public) == 0) + + // no error here since we stop after finding the first cert-key pair + _, err = createSecretFromPEM(secretName, []byte(testCert+"\n-----BEGIN MESSAGE-----\ndGVzdA==\n-----END MESSAGE-----")) + assert.NoError(err) + // error since the first pem block contains an invalid type + _, err = createSecretFromPEM(secretName, []byte("-----BEGIN MESSAGE-----\ndGVzdA==\n-----END MESSAGE-----\n"+testCert)) + assert.Error(err) + _, err = createSecretFromPEM(secretName, []byte("no PEM data here")) + assert.Error(err) +} diff --git a/cli/internal/cmd/status.go b/cli/internal/cmd/status.go new file mode 100644 index 00000000..a5f302fc --- /dev/null +++ b/cli/internal/cmd/status.go @@ -0,0 +1,76 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/cobra" +) + +const statusDesc = ` +This command provides information about the currently running MarbleRun Coordinator. +Information is obtained from the /status endpoint of the Coordinators REST API. + +The Coordinator will be in one of these 4 states: + 0 recovery mode: Found a sealed state of an old seal key. Waiting for user input on /recovery. + The Coordinator is currently sealed, it can be recovered using the [marblerun recover] command. + + 1 uninitialized: Fresh start, initializing the Coordinator. + The Coordinator is in its starting phase. + + 2 waiting for manifest: Waiting for user input on /manifest. + Send a manifest to the Coordinator using [marblerun manifest set] to start. + + 3 accepting marble: The Coordinator is running, you can add marbles to the mesh or update the + manifest using [marblerun manifest update]. +` + +type statusResponse struct { + StatusCode int `json:"StatusCode"` + StatusMessage string `json:"StatusMessage"` +} + +func NewStatusCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "status ", + Short: "Retrieve information about the status of the MarbleRun Coordinator", + Long: statusDesc, + Args: cobra.ExactArgs(1), + RunE: runStatus, + } + + return cmd +} + +func runStatus(cmd *cobra.Command, args []string) error { + hostname := args[0] + client, err := rest.NewClient(cmd, hostname) + if err != nil { + return err + } + return cliStatus(cmd, client) +} + +// cliStatus requests the current status of the Coordinator. +func cliStatus(cmd *cobra.Command, client getter) error { + resp, err := client.Get(cmd.Context(), rest.StatusEndpoint, http.NoBody) + if err != nil { + return fmt.Errorf("querying Coordinator status: %w", err) + } + + var statusResp statusResponse + if err := json.Unmarshal(resp, &statusResp); err != nil { + return err + } + cmd.Printf("%d: %s\n", statusResp.StatusCode, statusResp.StatusMessage) + + return nil +} diff --git a/cli/internal/cmd/status_test.go b/cli/internal/cmd/status_test.go new file mode 100644 index 00000000..8d2d62da --- /dev/null +++ b/cli/internal/cmd/status_test.go @@ -0,0 +1,108 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "testing" + + "github.com/edgelesssys/marblerun/cli/internal/rest" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStatus(t *testing.T) { + marshalMsg := func(msg statusResponse) []byte { + bytes, err := json.Marshal(msg) + require.NoError(t, err) + return bytes + } + + testCases := map[string]struct { + getter *stubGetter + wantErr bool + }{ + "recovery mode": { + getter: &stubGetter{ + response: marshalMsg( + statusResponse{ + StatusCode: 0, + StatusMessage: "Recovery", + }, + ), + }, + }, + "uninitialized": { + getter: &stubGetter{ + response: marshalMsg( + statusResponse{ + StatusCode: 1, + StatusMessage: "Uninitialized", + }, + ), + }, + }, + "waiting for manifest": { + getter: &stubGetter{ + response: marshalMsg( + statusResponse{ + StatusCode: 2, + StatusMessage: "Waiting for manifest", + }, + ), + }, + }, + "accepting marbles": { + getter: &stubGetter{ + response: marshalMsg( + statusResponse{ + StatusCode: 3, + StatusMessage: "Accepting Marbles", + }, + ), + }, + }, + "get error": { + getter: &stubGetter{ + err: errors.New("failed"), + }, + wantErr: true, + }, + "unmarshal error": { + getter: &stubGetter{ + response: []byte("invalid"), + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + cmd := &cobra.Command{} + var out bytes.Buffer + cmd.SetOut(&out) + + err := cliStatus(cmd, tc.getter) + + if tc.wantErr { + assert.Error(err) + return + } + + assert.NoError(err) + var expected statusResponse + require.NoError(t, json.Unmarshal(tc.getter.response, &expected)) + assert.Contains(out.String(), expected.StatusMessage) + assert.Equal(rest.StatusEndpoint, tc.getter.requestPath) + }) + } +} diff --git a/cli/internal/cmd/uninstall.go b/cli/internal/cmd/uninstall.go new file mode 100644 index 00000000..09f95d59 --- /dev/null +++ b/cli/internal/cmd/uninstall.go @@ -0,0 +1,88 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "context" + + "github.com/edgelesssys/marblerun/cli/internal/helm" + "github.com/edgelesssys/marblerun/cli/internal/kube" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func NewUninstallCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "uninstall", + Short: "Remove MarbleRun from a Kubernetes cluster", + Long: `Remove MarbleRun from a Kubernetes cluster`, + Args: cobra.NoArgs, + RunE: runUninstall, + } + + cmd.Flags().Bool("wait", false, "Wait for the uninstallation to complete before returning") + + return cmd +} + +func runUninstall(cmd *cobra.Command, args []string) error { + kubeClient, err := kube.NewClient() + if err != nil { + return err + } + helmClient, err := helm.New() + if err != nil { + return err + } + return cliUninstall(cmd, helmClient, kubeClient) +} + +// cliUninstall uninstalls MarbleRun. +func cliUninstall(cmd *cobra.Command, helmClient *helm.Client, kubeClient kubernetes.Interface) error { + wait, err := cmd.Flags().GetBool("wait") + if err != nil { + return err + } + if err := helmClient.Uninstall(wait); err != nil { + return err + } + + // If we get a "not found" error the resource was already removed / never created + // and we can continue on without a problem + if err := cleanupSecrets(cmd.Context(), kubeClient); err != nil && !errors.IsNotFound(err) { + return err + } + + if err := cleanupCSR(cmd.Context(), kubeClient); err != nil && !errors.IsNotFound(err) { + return err + } + + cmd.Println("MarbleRun successfully removed from your cluster") + + return nil +} + +// cleanupSecrets removes secretes set for the Admission Controller. +func cleanupSecrets(ctx context.Context, kubeClient kubernetes.Interface) error { + return kubeClient.CoreV1().Secrets(helm.Namespace).Delete(ctx, "marble-injector-webhook-certs", metav1.DeleteOptions{}) +} + +// cleanupCSR removes a potentially leftover CSR from the Admission Controller. +func cleanupCSR(ctx context.Context, kubeClient kubernetes.Interface) error { + // in case of kubernetes version < 1.19 no CSR was created by the install command + isLegacy, err := checkLegacyKubernetesVersion(kubeClient) + if err != nil { + return err + } + if isLegacy { + return nil + } + + return kubeClient.CertificatesV1().CertificateSigningRequests().Delete(ctx, webhookName, metav1.DeleteOptions{}) +} diff --git a/cli/cmd/uninstall_test.go b/cli/internal/cmd/uninstall_test.go similarity index 76% rename from cli/cmd/uninstall_test.go rename to cli/internal/cmd/uninstall_test.go index cf9e7f89..5785a141 100644 --- a/cli/cmd/uninstall_test.go +++ b/cli/internal/cmd/uninstall_test.go @@ -10,6 +10,7 @@ import ( "context" "testing" + "github.com/edgelesssys/marblerun/cli/internal/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" certv1 "k8s.io/api/certificates/v1" @@ -30,12 +31,13 @@ func TestCleanupWebhook(t *testing.T) { Minor: "19", GitVersion: "v1.19.4", } + ctx := context.Background() // Try to remove non existent CSR using function - _, err := testClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), webhookName, metav1.GetOptions{}) + _, err := testClient.CertificatesV1().CertificateSigningRequests().Get(ctx, webhookName, metav1.GetOptions{}) require.Error(err) - err = cleanupCSR(testClient) + err = cleanupCSR(ctx, testClient) require.Error(err) assert.True(errors.IsNotFound(err), "function returned an error other than not found") @@ -53,17 +55,17 @@ func TestCleanupWebhook(t *testing.T) { }, } - _, err = testClient.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{}) + _, err = testClient.CertificatesV1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}) require.NoError(err) - _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), webhookName, metav1.GetOptions{}) + _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(ctx, webhookName, metav1.GetOptions{}) require.NoError(err) // Remove CSR using function - err = cleanupCSR(testClient) + err = cleanupCSR(ctx, testClient) require.NoError(err) - _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), webhookName, metav1.GetOptions{}) + _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(ctx, webhookName, metav1.GetOptions{}) require.Error(err) // try changing to version lower than 19 and removing CSR (this should always return nil) @@ -72,15 +74,15 @@ func TestCleanupWebhook(t *testing.T) { Minor: "18", GitVersion: "v1.18.4", } - err = cleanupCSR(testClient) + err = cleanupCSR(ctx, testClient) require.NoError(err) // try changing to version string containing non-digit characters and removing the CSR - _, err = testClient.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{}) + _, err = testClient.CertificatesV1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}) require.NoError(err) - _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), webhookName, metav1.GetOptions{}) + _, err = testClient.CertificatesV1().CertificateSigningRequests().Get(ctx, webhookName, metav1.GetOptions{}) require.NoError(err) testClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{ @@ -88,14 +90,14 @@ func TestCleanupWebhook(t *testing.T) { Minor: "24+", GitVersion: "v1.24.3-2+63243a96d1c393", } - err = cleanupCSR(testClient) + err = cleanupCSR(ctx, testClient) require.NoError(err) // Try to remove non existent Secret using function - _, err = testClient.CoreV1().Secrets(helmNamespace).Get(context.TODO(), "marble-injector-webhook-certs", metav1.GetOptions{}) + _, err = testClient.CoreV1().Secrets(helm.Namespace).Get(ctx, "marble-injector-webhook-certs", metav1.GetOptions{}) require.Error(err) - err = cleanupSecrets(testClient) + err = cleanupSecrets(ctx, testClient) require.Error(err) assert.True(errors.IsNotFound(err), "function returned an error other than not found") @@ -103,7 +105,7 @@ func TestCleanupWebhook(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "marble-injector-webhook-certs", - Namespace: helmNamespace, + Namespace: helm.Namespace, }, Data: map[string][]byte{ "tls.crt": {0xAA, 0xAA, 0xAA}, @@ -111,12 +113,12 @@ func TestCleanupWebhook(t *testing.T) { }, } - _, err = testClient.CoreV1().Secrets(helmNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + _, err = testClient.CoreV1().Secrets(helm.Namespace).Create(ctx, secret, metav1.CreateOptions{}) require.NoError(err) - _, err = testClient.CoreV1().Secrets(helmNamespace).Get(context.TODO(), "marble-injector-webhook-certs", metav1.GetOptions{}) + _, err = testClient.CoreV1().Secrets(helm.Namespace).Get(ctx, "marble-injector-webhook-certs", metav1.GetOptions{}) require.NoError(err) - err = cleanupSecrets(testClient) + err = cleanupSecrets(ctx, testClient) require.NoError(err) } diff --git a/cli/internal/cmd/version.go b/cli/internal/cmd/version.go new file mode 100644 index 00000000..8f741155 --- /dev/null +++ b/cli/internal/cmd/version.go @@ -0,0 +1,42 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package cmd + +import ( + "github.com/edgelesssys/marblerun/cli/internal/kube" + "github.com/spf13/cobra" +) + +// Version is the CLI version. +var Version = "0.0.0" // Don't touch! Automatically injected at build-time. + +// GitCommit is the git commit hash. +var GitCommit = "0000000000000000000000000000000000000000" // Don't touch! Automatically injected at build-time. + +// NewVersionCmd returns the version command. +func NewVersionCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Display version of this CLI and (if running) the MarbleRun Coordinator", + Long: `Display version of this CLI and (if running) the MarbleRun Coordinator`, + Args: cobra.NoArgs, + Run: runVersion, + } + + return cmd +} + +func runVersion(cmd *cobra.Command, args []string) { + cmd.Printf("CLI Version: v%s \nCommit: %s\n", Version, GitCommit) + + cVersion, err := kube.CoordinatorVersion(cmd.Context()) + if err != nil { + cmd.Println("Unable to find MarbleRun Coordinator") + return + } + cmd.Printf("Coordinator Version: %s\n", cVersion) +} diff --git a/cli/internal/file/file.go b/cli/internal/file/file.go new file mode 100644 index 00000000..d3c4d8e2 --- /dev/null +++ b/cli/internal/file/file.go @@ -0,0 +1,45 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package file + +import "github.com/spf13/afero" + +// Handler is a wrapper around afero.Afero, +// providing a simple interface for reading and writing files. +type Handler struct { + fs afero.Afero + filename string +} + +// New returns a new FileWriter for the given filename. +// +// Returns nil if filename is empty. +func New(filename string, fs afero.Fs) *Handler { + if filename == "" { + return nil + } + + return &Handler{ + fs: afero.Afero{Fs: fs}, + filename: filename, + } +} + +// Write writes the given data to the file. +func (f *Handler) Write(data []byte) error { + return f.fs.WriteFile(f.filename, data, 0o644) +} + +// Name returns the filename. +func (f *Handler) Name() string { + return f.filename +} + +// Read reads the file and returns its contents. +func (f *Handler) Read() ([]byte, error) { + return f.fs.ReadFile(f.filename) +} diff --git a/cli/internal/helm/client.go b/cli/internal/helm/client.go new file mode 100644 index 00000000..4ea1b723 --- /dev/null +++ b/cli/internal/helm/client.go @@ -0,0 +1,348 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package helm + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/edgelesssys/marblerun/util" + "github.com/gofrs/flock" + "gopkg.in/yaml.v2" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/strvals" +) + +type Options struct { + Hostname string + DCAPQPL string + PCCSURL string + UseSecureCert string + AccessToken string + SGXResourceKey string + WebhookSettings []string + SimulationMode bool + CoordinatorRESTPort int + CoordinatorGRPCPort int +} + +// Client provides functionality to install and uninstall Helm charts. +type Client struct { + config *action.Configuration + settings *cli.EnvSettings +} + +// New initializes a new helm client. +func New() (*Client, error) { + settings := cli.New() + // settings.KubeConfig = kubeConfigPath + + actionConfig := &action.Configuration{} + if err := actionConfig.Init(settings.RESTClientGetter(), Namespace, os.Getenv("HELM_DRIVER"), nopLog); err != nil { + return nil, err + } + + return &Client{ + config: actionConfig, + settings: settings, + }, nil +} + +// GetChart loads the helm chart from the given path or from the edgeless helm repo. +// This will add the edgeless helm repo if it is not already present on disk. +func (c *Client) GetChart(chartPath, version string, enterpriseRelease bool) (*chart.Chart, error) { + if chartPath == "" { + // No chart was specified -> add or update edgeless helm repo + installer := action.NewInstall(c.config) + installer.ChartPathOptions.Version = version + + err := c.getRepo(repoName, repoURI) + if err != nil { + return nil, fmt.Errorf("adding edgeless helm repo: %w", err) + } + + // Enterprise chart is used if an access token is provided + chartName := chartName + if enterpriseRelease { + chartName = chartNameEnterprise + } + + chartPath, err = installer.ChartPathOptions.LocateChart(chartName, c.settings) + if err != nil { + return nil, fmt.Errorf("locating chart: %w", err) + } + } + chart, err := loader.Load(chartPath) + if err != nil { + return nil, fmt.Errorf("loading chart from path %q: %w", chartPath, err) + } + return chart, nil +} + +// UpdateValues merges the provided options with the default values of the chart. +func (c *Client) UpdateValues(options Options, chartValues map[string]interface{}) (map[string]interface{}, error) { + stringValues := []string{} + stringValues = append(stringValues, fmt.Sprintf("coordinator.meshServerPort=%d", options.CoordinatorGRPCPort)) + stringValues = append(stringValues, fmt.Sprintf("coordinator.clientServerPort=%d", options.CoordinatorRESTPort)) + + if options.SimulationMode { + // simulation mode, disable tolerations and resources, set simulation to true + stringValues = append(stringValues, + fmt.Sprintf("tolerations=%s", "null"), + fmt.Sprintf("coordinator.simulation=%t", options.SimulationMode), + fmt.Sprintf("coordinator.resources.limits=%s", "null"), + fmt.Sprintf("coordinator.hostname=%s", options.Hostname), + fmt.Sprintf("dcap=%s", "null"), + ) + } else { + stringValues = append(stringValues, + fmt.Sprintf("coordinator.hostname=%s", options.Hostname), + fmt.Sprintf("dcap.qpl=%s", options.DCAPQPL), + fmt.Sprintf("dcap.pccsUrl=%s", options.PCCSURL), + fmt.Sprintf("dcap.useSecureCert=%s", options.UseSecureCert), + ) + + // Helms value merge function will overwrite any preset values for "tolerations" if we set new ones here + // To avoid this we set the new toleration for "resourceKey" and copy all preset tolerations + needToleration := true + idx := 0 + for _, toleration := range chartValues["tolerations"].([]interface{}) { + if key, ok := toleration.(map[string]interface{})["key"]; ok { + if key == options.SGXResourceKey { + needToleration = false + } + stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].key=%v", idx, key)) + } + if operator, ok := toleration.(map[string]interface{})["operator"]; ok { + stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].operator=%v", idx, operator)) + } + if effect, ok := toleration.(map[string]interface{})["effect"]; ok { + stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].effect=%v", idx, effect)) + } + if value, ok := toleration.(map[string]interface{})["value"]; ok { + stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].value=%v", idx, value)) + } + if tolerationSeconds, ok := toleration.(map[string]interface{})["tolerationSeconds"]; ok { + stringValues = append(stringValues, fmt.Sprintf("tolerations[%d].tolerationSeconds=%v", idx, tolerationSeconds)) + } + idx++ + } + if needToleration { + stringValues = append(stringValues, + fmt.Sprintf("tolerations[%d].key=%s", idx, options.SGXResourceKey), + fmt.Sprintf("tolerations[%d].operator=Exists", idx), + fmt.Sprintf("tolerations[%d].effect=NoSchedule", idx), + ) + } + } + + // Configure enterprise access token + if options.AccessToken != "" { + coordinatorCfg, ok := chartValues["coordinator"].(map[string]interface{}) + if !ok { + return nil, errors.New("coordinator not found in chart values") + } + repository, ok := coordinatorCfg["repository"].(string) + if !ok { + return nil, errors.New("coordinator.registry not found in chart values") + } + + pullSecret := fmt.Sprintf(`{"auths":{"%s":{"auth":"%s"}}}`, repository, options.AccessToken) + stringValues = append(stringValues, fmt.Sprintf("pullSecret.token=%s", base64.StdEncoding.EncodeToString([]byte(pullSecret)))) + } + + if len(options.WebhookSettings) > 0 { + stringValues = append(stringValues, options.WebhookSettings...) + stringValues = append(stringValues, fmt.Sprintf("marbleInjector.resourceKey=%s", options.SGXResourceKey)) + } + + finalValues := map[string]interface{}{} + for _, val := range stringValues { + if err := strvals.ParseInto(val, finalValues); err != nil { + return nil, fmt.Errorf("parsing value %q into final values: %w", val, err) + } + } + + if !options.SimulationMode { + setSGXValues(options.SGXResourceKey, finalValues, chartValues) + } + + return finalValues, nil +} + +// Install installs MarbleRun using the provided chart and values. +func (c *Client) Install(ctx context.Context, wait bool, chart *chart.Chart, values map[string]interface{}) error { + installer := action.NewInstall(c.config) + installer.Namespace = Namespace + installer.ReleaseName = release + installer.CreateNamespace = true + installer.Wait = wait + installer.Timeout = time.Minute * 5 + + if err := chartutil.ValidateAgainstSchema(chart, values); err != nil { + return err + } + + _, err := installer.RunWithContext(ctx, chart, values) + return err +} + +// Uninstall removes the MarbleRun deployment from the cluster. +func (c *Client) Uninstall(wait bool) error { + uninstaller := action.NewUninstall(c.config) + uninstaller.Wait = wait + uninstaller.Timeout = time.Minute * 5 + + _, err := uninstaller.Run(release) + return err +} + +// getRepo is a simplified repo_add from helm cli to add MarbleRun repo if it does not yet exist. +// To make sure we use the newest chart we always download the needed index file. +func (c *Client) getRepo(name string, url string) error { + repoFile := c.settings.RepositoryConfig + + // Ensure the file directory exists as it is required for file locking + err := os.MkdirAll(filepath.Dir(repoFile), 0o755) + if err != nil && !os.IsExist(err) { + return err + } + + // Acquire a file lock for process synchronization + fileLock := flock.New(strings.Replace(repoFile, filepath.Ext(repoFile), ".lock", 1)) + lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + locked, err := fileLock.TryLockContext(lockCtx, time.Second) + if err == nil && locked { + defer fileLock.Unlock() + } + if err != nil { + return err + } + + b, err := os.ReadFile(repoFile) + if err != nil && !os.IsNotExist(err) { + return err + } + + var f repo.File + if err := yaml.Unmarshal(b, &f); err != nil { + return err + } + + entry := &repo.Entry{ + Name: name, + URL: url, + } + + r, err := repo.NewChartRepository(entry, getter.All(c.settings)) + if err != nil { + return err + } + + if _, err := r.DownloadIndexFile(); err != nil { + return errors.New("chart repository cannot be reached") + } + + f.Update(entry) + + if err := f.WriteFile(repoFile, 0o644); err != nil { + return err + } + return nil +} + +// setSGXValues sets the needed values for the coordinator as a map[string]interface. +// strvals can't parse keys which include dots, e.g. setting as a resource limit key "sgx.intel.com/epc" will lead to errors. +func setSGXValues(resourceKey string, values, chartValues map[string]interface{}) { + values["coordinator"].(map[string]interface{})["resources"] = map[string]interface{}{ + "limits": map[string]interface{}{}, + "requests": map[string]interface{}{}, + } + + var needNewLimit bool + limit := util.GetEPCResourceLimit(resourceKey) + + // remove all previously set sgx resource limits + if presetLimits, ok := chartValues["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{}); ok { + for oldResourceKey := range presetLimits { + // Make sure the key we delete is an unwanted sgx resource and not a custom resource or common resource (cpu, memory, etc.) + if needsDeletion(oldResourceKey, resourceKey) { + values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[oldResourceKey] = nil + needNewLimit = true + } + } + } + + // remove all previously set sgx resource requests + if presetLimits, ok := chartValues["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{}); ok { + for oldResourceKey := range presetLimits { + if needsDeletion(oldResourceKey, resourceKey) { + values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{})[oldResourceKey] = nil + needNewLimit = true + } + } + } + + // Set the new sgx resource limit, kubernetes will automatically set a resource request equal to the limit + if needNewLimit { + values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[resourceKey] = limit + } + + // Make sure provision and enclave bit is set if the Intel plugin is used + if resourceKey == util.IntelEpc.String() { + values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[util.IntelProvision.String()] = 1 + values["coordinator"].(map[string]interface{})["resources"].(map[string]interface{})["limits"].(map[string]interface{})[util.IntelEnclave.String()] = 1 + } +} + +// needsDeletion checks if an existing key of a helm chart should be deleted. +// Choice is based on the resource key of the used SGX device plugin. +func needsDeletion(existingKey, sgxKey string) bool { + sgxResources := []string{ + util.AlibabaEpc.String(), util.AzureEpc.String(), util.IntelEpc.String(), + util.IntelProvision.String(), util.IntelEnclave.String(), + } + + switch sgxKey { + case util.AlibabaEpc.String(), util.AzureEpc.String(): + // Delete all non Alibaba/Azure SGX resources depending on the used SGX device plugin + return sgxKey != existingKey && keyInList(existingKey, sgxResources) + case util.IntelEpc.String(): + // Delete all non Intel SGX resources depending on the used SGX device plugin + // Keep Intel provision and enclave bit + return keyInList(existingKey, []string{util.AlibabaEpc.String(), util.AzureEpc.String()}) + default: + // Either no SGX plugin or a custom SGX plugin is used + // Delete all known SGX resources + return keyInList(existingKey, sgxResources) + } +} + +func keyInList(key string, list []string) bool { + for _, l := range list { + if key == l { + return true + } + } + return false +} + +func nopLog(format string, v ...interface{}) { +} diff --git a/cli/internal/helm/client_test.go b/cli/internal/helm/client_test.go new file mode 100644 index 00000000..8b524705 --- /dev/null +++ b/cli/internal/helm/client_test.go @@ -0,0 +1,92 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package helm + +import ( + "testing" + + "github.com/edgelesssys/marblerun/util" + "github.com/stretchr/testify/assert" +) + +func TestNeedsDeletion(t *testing.T) { + testCases := map[string]struct { + existingKey string + sgxKey string + wantDeletion bool + }{ + "intel key with azure plugin": { + existingKey: util.IntelEpc.String(), + sgxKey: util.AzureEpc.String(), + wantDeletion: true, + }, + "intel key with alibaba plugin": { + existingKey: util.IntelEpc.String(), + sgxKey: util.AlibabaEpc.String(), + wantDeletion: true, + }, + "azure key with intel plugin": { + existingKey: util.AzureEpc.String(), + sgxKey: util.IntelEpc.String(), + wantDeletion: true, + }, + "azure key with alibaba plugin": { + existingKey: util.AzureEpc.String(), + sgxKey: util.AlibabaEpc.String(), + wantDeletion: true, + }, + "alibaba key with intel plugin": { + existingKey: util.AlibabaEpc.String(), + sgxKey: util.IntelEpc.String(), + wantDeletion: true, + }, + "alibaba key with azure plugin": { + existingKey: util.AlibabaEpc.String(), + sgxKey: util.AzureEpc.String(), + wantDeletion: true, + }, + "same key": { + existingKey: util.IntelEpc.String(), + sgxKey: util.IntelEpc.String(), + wantDeletion: false, + }, + "intel provision with intel plugin": { + existingKey: util.IntelProvision.String(), + sgxKey: util.IntelEpc.String(), + wantDeletion: false, + }, + "intel enclave with intel plugin": { + existingKey: util.IntelEnclave.String(), + sgxKey: util.IntelEpc.String(), + wantDeletion: false, + }, + "regular resource with intel plugin": { + existingKey: "cpu", + sgxKey: util.IntelEpc.String(), + wantDeletion: false, + }, + "custom resource with intel plugin": { + existingKey: "custom-sgx-resource", + sgxKey: util.IntelEpc.String(), + wantDeletion: false, + }, + "intel provision with custom plugin": { + existingKey: util.IntelProvision.String(), + sgxKey: "custom-sgx-resource", + wantDeletion: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + delete := needsDeletion(tc.existingKey, tc.sgxKey) + assert.Equal(tc.wantDeletion, delete) + }) + } +} diff --git a/cli/internal/helm/helm.go b/cli/internal/helm/helm.go new file mode 100644 index 00000000..be27e339 --- /dev/null +++ b/cli/internal/helm/helm.go @@ -0,0 +1,20 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Package helm provides functions to install and uninstall the MarbleRun Helm chart. +package helm + +// Helm constants. +const ( + CoordinatorDeployment = "marblerun-coordinator" + InjectorDeployment = "marble-injector" + Namespace = "marblerun" + chartName = "edgeless/marblerun" + chartNameEnterprise = "edgeless/marblerun-enterprise" + release = "marblerun" + repoURI = "https://helm.edgeless.systems/stable" + repoName = "edgeless" +) diff --git a/cli/internal/kube/client.go b/cli/internal/kube/client.go new file mode 100644 index 00000000..f5d3a78b --- /dev/null +++ b/cli/internal/kube/client.go @@ -0,0 +1,64 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package kube + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/edgelesssys/marblerun/cli/internal/helm" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +const versionLabel = "app.kubernetes.io/version" + +// NewClient returns the kubernetes Clientset to interact with the k8s API. +func NewClient() (*kubernetes.Clientset, error) { + path := os.Getenv(clientcmd.RecommendedConfigPathEnvVar) + if path == "" { + homedir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + path = filepath.Join(homedir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName) + } + + kubeConfig, err := clientcmd.BuildConfigFromFlags("", path) + if err != nil { + return nil, err + } + + kubeClient, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, fmt.Errorf("creating client: %w", err) + } + + return kubeClient, nil +} + +// CoordinatorVersion returns the version of the Coordinator deployment. +func CoordinatorVersion(ctx context.Context) (string, error) { + kubeClient, err := NewClient() + if err != nil { + return "", err + } + + coordinatorDeployment, err := kubeClient.AppsV1().Deployments(helm.Namespace).Get(ctx, helm.CoordinatorDeployment, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("retrieving deployment information: %w", err) + } + + version := coordinatorDeployment.Labels[versionLabel] + if len(version) <= 0 { + return "", fmt.Errorf("deployment has no label %s", versionLabel) + } + return version, nil +} diff --git a/cli/internal/rest/rest.go b/cli/internal/rest/rest.go new file mode 100644 index 00000000..71cb4ef5 --- /dev/null +++ b/cli/internal/rest/rest.go @@ -0,0 +1,311 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Package rest provides methods and functions to communicate +// with the MarbleRun Coordinator using its REST API. +package rest + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + + "github.com/edgelesssys/ego/attestation" + "github.com/edgelesssys/era/era" + "github.com/edgelesssys/era/util" + "github.com/edgelesssys/marblerun/cli/internal/kube" + "github.com/spf13/cobra" + "github.com/tidwall/gjson" + "k8s.io/client-go/tools/clientcmd" +) + +// Endpoints of the MarbleRun Coordinator REST API. +const ( + ManifestEndpoint = "manifest" + UpdateEndpoint = "update" + UpdateCancelEndpoint = "update-cancel" + UpdateStatusEndpoint = "update-manifest" + RecoverEndpoint = "recover" + SecretEndpoint = "secrets" + StatusEndpoint = "status" + ContentJSON = "application/json" + ContentPlain = "text/plain" +) + +const ( + eraDefaultConfig = "era-config.json" + messageField = "message" + dataField = "data" +) + +// Flags are command line flags used to configure the REST client. +type Flags struct { + EraConfig string + Insecure bool + AcceptedTCBStatuses []string +} + +// ParseFlags parses the command line flags used to configure the REST client. +func ParseFlags(cmd *cobra.Command) (Flags, error) { + eraConfig, err := cmd.Flags().GetString("era-config") + if err != nil { + return Flags{}, err + } + insecure, err := cmd.Flags().GetBool("insecure") + if err != nil { + return Flags{}, err + } + acceptedTCBStatuses, err := cmd.Flags().GetStringSlice("accepted-tcb-statuses") + if err != nil { + return Flags{}, err + } + + return Flags{ + EraConfig: eraConfig, + Insecure: insecure, + AcceptedTCBStatuses: acceptedTCBStatuses, + }, nil +} + +type authenticatedFlags struct { + Flags + ClientCert tls.Certificate +} + +func parseAuthenticatedFlags(cmd *cobra.Command) (authenticatedFlags, error) { + flags, err := ParseFlags(cmd) + if err != nil { + return authenticatedFlags{}, err + } + certFile, err := cmd.Flags().GetString("cert") + if err != nil { + return authenticatedFlags{}, err + } + keyFile, err := cmd.Flags().GetString("key") + if err != nil { + return authenticatedFlags{}, err + } + clientCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return authenticatedFlags{}, err + } + + return authenticatedFlags{ + Flags: flags, + ClientCert: clientCert, + }, nil +} + +// Client is a REST client for the MarbleRun Coordinator. +type Client struct { + client *http.Client + host string +} + +// NewClient creates and returns an http client using the flags of cmd. +func NewClient(cmd *cobra.Command, host string) (*Client, error) { + flags, err := ParseFlags(cmd) + if err != nil { + return nil, fmt.Errorf("parsing flags: %w", err) + } + caCert, err := VerifyCoordinator( + cmd.Context(), cmd.OutOrStdout(), host, + flags.EraConfig, flags.Insecure, flags.AcceptedTCBStatuses, + ) + if err != nil { + return nil, fmt.Errorf("failed to verify Coordinator enclave: %w", err) + } + return newClient(host, caCert, nil) +} + +// NewAuthenticatedClient creates and returns an http client with client authentication using the flags of cmd. +func NewAuthenticatedClient(cmd *cobra.Command, host string) (*Client, error) { + flags, err := parseAuthenticatedFlags(cmd) + if err != nil { + return nil, fmt.Errorf("parsing flags: %w", err) + } + caCert, err := VerifyCoordinator( + cmd.Context(), cmd.OutOrStdout(), host, + flags.EraConfig, flags.Insecure, flags.AcceptedTCBStatuses, + ) + if err != nil { + return nil, fmt.Errorf("failed to verify Coordinator enclave: %w", err) + } + return newClient(host, caCert, &flags.ClientCert) +} + +func newClient(host string, caCert []*pem.Block, clCert *tls.Certificate) (*Client, error) { + // Set rootCA for connection to Coordinator + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(pem.EncodeToMemory(caCert[len(caCert)-1])); !ok { + return nil, errors.New("failed to parse certificate") + } + // Add intermediate cert if applicable + if len(caCert) > 1 { + if ok := certPool.AppendCertsFromPEM(pem.EncodeToMemory(caCert[0])); !ok { + return nil, errors.New("failed to parse certificate") + } + } + + var tlsConfig *tls.Config + if clCert != nil { + tlsConfig = &tls.Config{ + RootCAs: certPool, + Certificates: []tls.Certificate{*clCert}, + } + } else { + tlsConfig = &tls.Config{ + RootCAs: certPool, + } + } + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + return &Client{ + client: client, + host: host, + }, nil +} + +// Get sends a GET request to the Coordinator under the specified path. +// If body is non nil, it is sent as the request body. +// Query parameters can be provided as a list of strings, where each pair of strings is a key-value pair. +// On success, the data field of the JSON response is returned. +func (c *Client) Get(ctx context.Context, path string, body io.Reader, queryParameters ...string) ([]byte, error) { + if len(queryParameters)%2 != 0 { + return nil, errors.New("query parameters must be provided in pairs") + } + query := url.Values{} + for i := 0; i < len(queryParameters); i += 2 { + query.Add(queryParameters[i], queryParameters[i+1]) + } + + uri := url.URL{Scheme: "https", Host: c.host, Path: path, RawQuery: query.Encode()} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), body) + if err != nil { + return nil, err + } + + return c.do(req) +} + +// Post sends a POST request to the Coordinator under the specified path. +// Optionally, a body can be provided. +func (c *Client) Post(ctx context.Context, path, contentType string, body io.Reader) ([]byte, error) { + uri := url.URL{Scheme: "https", Host: c.host, Path: path} + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), body) + if err != nil { + return nil, err + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + return c.do(req) +} + +func (c *Client) do(req *http.Request) ([]byte, error) { + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + msg := gjson.GetBytes(respBody, messageField).String() + + switch resp.StatusCode { + case http.StatusOK: + // return data field of JSON response + data := gjson.GetBytes(respBody, dataField).String() + return []byte(data), nil + case http.StatusUnauthorized: + return nil, fmt.Errorf("%s %s: authorizing user: %s", req.Method, req.URL.String(), msg) + default: + return nil, fmt.Errorf("%s %s: %s %s", req.Method, req.URL.String(), resp.Status, msg) + } +} + +// VerifyCoordinator verifies the connection to the MarbleRun Coordinator. +func VerifyCoordinator( + ctx context.Context, out io.Writer, host, configFilename string, + insecure bool, acceptedTCBStatuses []string, +) ([]*pem.Block, error) { + // skip verification if specified + if insecure { + fmt.Fprintln(out, "Warning: skipping quote verification") + return era.InsecureGetCertificate(host) + } + + if configFilename == "" { + configFilename = eraDefaultConfig + + // reuse existing config from current working directory if none specified + // or try to get latest config from github if it does not exist + if _, err := os.Stat(configFilename); err == nil { + fmt.Fprintln(out, "Reusing existing config file") + } else if err := fetchLatestCoordinatorConfiguration(ctx, out); err != nil { + return nil, err + } + } + + pemBlock, tcbStatus, err := era.GetCertificate(host, configFilename) + if errors.Is(err, attestation.ErrTCBLevelInvalid) && + util.StringSliceContains(acceptedTCBStatuses, tcbStatus.String()) { + fmt.Fprintln(out, "Warning: TCB level invalid, but accepted by configuration") + return pemBlock, nil + } + return pemBlock, err +} + +func fetchLatestCoordinatorConfiguration(ctx context.Context, out io.Writer) error { + coordinatorVersion, err := kube.CoordinatorVersion(ctx) + eraURL := fmt.Sprintf("https://github.com/edgelesssys/marblerun/releases/download/%s/coordinator-era.json", coordinatorVersion) + if err != nil { + // if errors were caused by an empty kube config file or by being unable to connect to a cluster we assume the Coordinator is running as a standalone + // and we default to the latest era-config file + var dnsError *net.DNSError + if !clientcmd.IsEmptyConfig(err) && !errors.As(err, &dnsError) && !os.IsNotExist(err) { + return err + } + eraURL = "https://github.com/edgelesssys/marblerun/releases/latest/download/coordinator-era.json" + } + + fmt.Fprintf(out, "No era config file specified, getting config from %s\n", eraURL) + resp, err := http.Get(eraURL) + if err != nil { + return fmt.Errorf("downloading era config for version %s: %w", coordinatorVersion, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("downloading era config for version: %s: %d: %s", coordinatorVersion, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + era, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("downloading era config for version %s: %w", coordinatorVersion, err) + } + + if err := os.WriteFile(eraDefaultConfig, era, 0o644); err != nil { + return fmt.Errorf("writing era config file: %w", err) + } + + fmt.Fprintf(out, "Got era config for version %s\n", coordinatorVersion) + return nil +} diff --git a/cli/internal/rest/rest_test.go b/cli/internal/rest/rest_test.go new file mode 100644 index 00000000..2bc6dc84 --- /dev/null +++ b/cli/internal/rest/rest_test.go @@ -0,0 +1,175 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package rest + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/edgelesssys/marblerun/coordinator/server" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type roundTripFunc func(req *http.Request) *http.Response + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +// newTestClient returns *http.Client with Transport replaced to avoid making real calls. +func newTestClient(fn roundTripFunc) *http.Client { + return &http.Client{ + Transport: fn, + } +} + +func TestGet(t *testing.T) { + require := require.New(t) + defaultResponseFunc := func(req *http.Request) *http.Response { + response := server.GeneralResponse{ + Message: "response message", + Data: "response data", + } + res, err := json.Marshal(response) + require.NoError(err) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(res)), + Header: make(http.Header), + } + } + + testCases := map[string]struct { + roundTripFunc roundTripFunc + body io.Reader + queryParams []string + wantResponse []byte + wantErr bool + }{ + "success": { + roundTripFunc: defaultResponseFunc, + wantResponse: []byte("response data"), + }, + "success with query params": { + roundTripFunc: defaultResponseFunc, + queryParams: []string{"key1", "value1", "key2", "value2"}, + wantResponse: []byte("response data"), + }, + "success with body": { + roundTripFunc: defaultResponseFunc, + body: strings.NewReader("request body"), + wantResponse: []byte("response data"), + }, + "odd number of query params": { + roundTripFunc: defaultResponseFunc, + queryParams: []string{"key1", "value1", "key2"}, + wantErr: true, + }, + "server error": { + roundTripFunc: func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(&bytes.Reader{}), + Header: make(http.Header), + } + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + client := &Client{ + client: newTestClient(tc.roundTripFunc), + host: "unit-test", + } + + res, err := client.Get(context.Background(), "unit-test", tc.body, tc.queryParams...) + if tc.wantErr { + assert.Error(err) + return + } + + assert.NoError(err) + assert.Equal(tc.wantResponse, res) + }) + } +} + +func TestPost(t *testing.T) { + require := require.New(t) + defaultResponseFunc := func(req *http.Request) *http.Response { + response := server.GeneralResponse{ + Message: "response message", + Data: "response data", + } + res, err := json.Marshal(response) + require.NoError(err) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(res)), + Header: make(http.Header), + } + } + + testCases := map[string]struct { + roundTripFunc roundTripFunc + body io.Reader + wantResponse []byte + wantErr bool + }{ + "success": { + roundTripFunc: defaultResponseFunc, + wantResponse: []byte("response data"), + }, + "success with body": { + roundTripFunc: defaultResponseFunc, + body: strings.NewReader("request body"), + wantResponse: []byte("response data"), + }, + "server error": { + roundTripFunc: func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: io.NopCloser(&bytes.Reader{}), + Header: make(http.Header), + } + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + client := &Client{ + client: newTestClient(tc.roundTripFunc), + host: "unit-test", + } + + res, err := client.Post(context.Background(), "unit-test", "test", tc.body) + if tc.wantErr { + assert.Error(err) + return + } + + assert.NoError(err) + assert.Equal(tc.wantResponse, res) + }) + } +} diff --git a/go.mod b/go.mod index 1104979f..708e33a4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/edgelesssys/marblerun go 1.17 require ( - github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b github.com/edgelesssys/ego v1.0.1 github.com/edgelesssys/era v0.3.3 github.com/fatih/color v1.13.0 @@ -13,8 +12,6 @@ require ( github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 - github.com/jarcoal/httpmock v1.2.0 - github.com/pelletier/go-toml v1.9.5 github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_model v0.3.0 github.com/spacemonkeygo/openssl v0.0.0-20181017203307-c2dcc5cca94a diff --git a/go.sum b/go.sum index 8a4f7e8a..984cde8e 100644 --- a/go.sum +++ b/go.sum @@ -518,8 +518,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY= -github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -1098,8 +1096,6 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= -github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= -github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -1209,8 +1205,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk= -github.com/maxatome/go-testdeep v1.11.0/go.mod h1:011SgQ6efzZYAen6fDn4BqQ+lUR72ysdyKe7Dyogw70= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -1357,7 +1351,6 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= diff --git a/injector/injector.go b/injector/injector.go index 9b0e03b3..fb4f60e4 100644 --- a/injector/injector.go +++ b/injector/injector.go @@ -10,7 +10,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "log" "net/http" "strings" @@ -246,7 +246,7 @@ func checkRequest(w http.ResponseWriter, r *http.Request) []byte { return nil } - body, err := ioutil.ReadAll(r.Body) + body, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { http.Error(w, fmt.Sprintf("unable to read request: %v", err), http.StatusBadRequest) diff --git a/test/integration_test.go b/test/integration_test.go index 3481cf7e..2166b111 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -14,7 +14,7 @@ import ( "encoding/base64" "encoding/pem" "flag" - "io/ioutil" + "io" "log" "net" "net/http" @@ -192,7 +192,7 @@ func TestClientAPI(t *testing.T) { resp, err := client.Get(clientAPIURL.String()) require.NoError(err) require.Equal(http.StatusOK, resp.StatusCode) - quote, err := ioutil.ReadAll(resp.Body) + quote, err := io.ReadAll(resp.Body) resp.Body.Close() require.NoError(err) cert := gjson.Get(string(quote), "data.Cert").String() @@ -215,7 +215,7 @@ func TestClientAPI(t *testing.T) { resp, err = client.Get(clientAPIURL.String()) require.NoError(err) require.Equal(http.StatusOK, resp.StatusCode) - manifest, err := ioutil.ReadAll(resp.Body) + manifest, err := io.ReadAll(resp.Body) resp.Body.Close() require.NoError(err) assert.JSONEq(`{"status":"success","data":{"ManifestSignatureRootECDSA":null,"ManifestSignature":"","Manifest":null}}`, string(manifest)) @@ -231,7 +231,7 @@ func TestClientAPI(t *testing.T) { resp, err = client.Get(clientAPIURL.String()) require.NoError(err) require.Equal(http.StatusOK, resp.StatusCode) - secret, err := ioutil.ReadAll(resp.Body) + secret, err := io.ReadAll(resp.Body) resp.Body.Close() require.NoError(err) assert.Contains(string(secret), `{"status":"success","data":{"symmetricKeyShared":{"Type":"symmetric-key","Size":128,`)