diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml new file mode 100644 index 0000000..8eac048 --- /dev/null +++ b/.github/workflows/integ.yml @@ -0,0 +1,33 @@ +# GitHub Actions - CI for Go to build & test. See ci-go-cover.yml and linters.yml for code coverage and linters. +# Taken from: https://github.com/fxamacker/cbor/workflows/ci.yml (thanks!) +name: integration-tests +on: [push, pull_request] +jobs: + + # Test on various OS with specified Go version. + tests: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + - name: Install jq + run: + sudo apt-get install --yes jq + - name: Install Docker + run: | + curl -fsSL https://get.docker.com | sh + sudo usermod -aG docker $USER + sudo systemctl restart docker + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 1 + - name: Build project + run: make + - name: Run tests + run: | + make integ-test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b7efcb4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "test/bats"] + path = test/bats + url = https://github.com/bats-core/bats-core.git +[submodule "test/test_helper/bats-support"] + path = test/test_helper/bats-support + url = https://github.com/bats-core/bats-support.git +[submodule "test/test_helper/bats-assert"] + path = test/test_helper/bats-assert + url = https://github.com/bats-core/bats-assert.git diff --git a/Makefile b/Makefile index 72fb681..1d02d43 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,12 @@ build: test: $(GO) test -tags=test ./... $(TEST_ARGS) +.PHONY: integ-test +integ-test: + @scripts/integration-tests.sh setup + @scripts/integration-tests.sh run + @scripts/integration-tests.sh teardown + .PHONY: fmt format gofmt fmt format gofmt: */*/*.go */*/*/*.go $(GOFMT) -w */*/*.go */*/*/*.go @@ -43,8 +49,7 @@ cover-report coverage-report report: coverage.out .PHONY: presubmit presubmit: - # TODO: increase coverage - @$(MAKE) -s test && $(MAKE) -s lint && COVERAGE_THRESHOLD=30% $(MAKE) -s coverage && $(MAKE) -s format + @$(MAKE) -s test && $(MAKE) -s lint && $(MAKE) -s coverage && $(MAKE) -s format @if ! $(GIT) diff-index --quiet HEAD --; then \ echo -e "\033[1;31mUNCOMMITED CHANGES!\033[0m"; \ exit 2; \ @@ -60,15 +65,18 @@ Targets: Run unit tests. Use VERBOSE=1 for SQL traces. Set TEST_DB_FILE to specify the path for the sqlite DB file to be used for tests (by default in-memory DB is used). + integ-test: + Run integration tests. These rely on a Docker container running database + servers. lint: Run golangci-lint. fmt, format, gofmt: Run gofmt -w on Go sourcefiles, fixing any formatting issues. cover, coverage: Report overall test coverage percentage and generate coverage.out. You - can specify a space-separated list of packages for which coverage will - not be checked by setting IGNORE_COVERAGE. You can specify alternative - threshold by setting COVERAGE_THRESHOLD. + can specify a space-separated list of packages for which coverage will + not be checked by setting IGNORE_COVERAGE. You can specify alternative + threshold by setting COVERAGE_THRESHOLD. cover-report, coverage-report, report: Open a detailed HTML coverage report in default browser. presubmit: diff --git a/README.md b/README.md index 3bee8a0..866f171 100644 --- a/README.md +++ b/README.md @@ -143,3 +143,42 @@ variables. To get then environment variable name, change the setting name to be upper case, replace `-` with `_`, and prefix it with `CORIM_STORE_`. E.g. to set `no-color` via an environment variable, you would set `CORIM_STORE_NO_COLOR=1`. + +## Store Design Overview + +The store is intended for endorsements, reference values, and trust anchors +that will be used for attestation verification. These are stored as value and +key triples, which associate an attesting environment description with +corresponding measurements and cryptographic keys. + +Sets of triples are grouped under "module tags" (corresponding to CoMIDs), +which are, in turn, associated with "manifests" (corresponding to CoRIMs). +Module tags group claims (contained in triples) relating to a logical system +module (hardware, firmware, etc). A manifest acts as an "envelope" containing +additional metadata about the module tags it contains (such as the validity +period). + +![E-R diagram](misc/corim-store-er-simplified.drawio.png) + +The above diagram is a simplified entity-relation diagram for the major +entities used by the store. It omits some "meta" fields (e.g. foreign keys), +and only shows the main logical entities within the store. For a complete +overview of the SQL tables created by the store, please see the [schema +diagram](misc/corim-store-schema.drawio.png). + +### Triple Life Cycle + +By default, when triples are added, they are in an inactive state. They must be +activated in order to become available for use in verification. This decouples +provisioning values into the store from making them available for verification. +Triples may also be deactivated (e.g. when the associated attesters have been +decommissioned), but remain "archived" in the store. + +```mermaid +stateDiagram-v2 + [*] --> Inactive: add + Inactive --> Active: activate + Active --> Inactive: deactivate + Inactive --> [*]: delete + Active --> [*]: delete +``` diff --git a/cmd/corim-store/cmd/common.go b/cmd/corim-store/cmd/common.go index f55369a..4c8dc69 100644 --- a/cmd/corim-store/cmd/common.go +++ b/cmd/corim-store/cmd/common.go @@ -1,8 +1,16 @@ package cmd import ( + "encoding/base64" + "encoding/hex" "fmt" "os" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/veraison/corim-store/pkg/model" + "github.com/veraison/corim/comid" ) func CheckErr(msg interface{}) { @@ -35,3 +43,155 @@ func Green(msg string) string { return fmt.Sprintf("\033[1;32m%s\033[0m", msg) } + +func AddEnviromentFlags(cmd *cobra.Command) { + cmd.Flags().StringP("class-id", "C", "", "Environment class ID.") + cmd.Flags().StringP("vendor", "V", "", "Environment vendor.") + cmd.Flags().StringP("model", "M", "", "Environment model.") + cmd.Flags().Int64P("layer", "L", -1, "Environment layer.") + cmd.Flags().Int64P("index", "I", -1, "Environment index.") + cmd.Flags().StringP("instance-id", "i", "", "Environment instance ID") + cmd.Flags().StringP("group-id", "g", "", "Environment group ID") +} + +func BuildEnvironment(cmd *cobra.Command) (*model.Environment, error) { + var ret model.Environment + + vendor, err := cmd.Flags().GetString("vendor") + if err != nil { + return nil, fmt.Errorf("vendor: %w", err) + } + if vendor != "" { + ret.Vendor = &vendor + } + + model, err := cmd.Flags().GetString("model") + if err != nil { + return nil, fmt.Errorf("model: %w", err) + } + if model != "" { + ret.Model = &model + } + + layerInt, err := cmd.Flags().GetInt64("layer") + if err != nil { + return nil, fmt.Errorf("layer: %w", err) + } + if layerInt > -1 { + layer := uint64(layerInt) + ret.Layer = &layer + } + + indexInt, err := cmd.Flags().GetInt64("index") + if err != nil { + return nil, fmt.Errorf("index: %w", err) + } + if indexInt > -1 { + index := uint64(indexInt) + ret.Index = &index + } + + classIDText, err := cmd.Flags().GetString("class-id") + if err != nil { + return nil, fmt.Errorf("class-id: %w", err) + } + + if classIDText != "" { + classIDBytes, classIDType, err := parseID(classIDText) + if err != nil { + return nil, fmt.Errorf("class-id: %w", err) + } + + ret.ClassBytes = &classIDBytes + if classIDType != "" { + ret.ClassType = &classIDType + } + } + + instanceIDText, err := cmd.Flags().GetString("instance-id") + if err != nil { + return nil, fmt.Errorf("instance-id: %w", err) + } + + if instanceIDText != "" { + instanceIDBytes, instanceIDType, err := parseID(instanceIDText) + if err != nil { + return nil, fmt.Errorf("instance-id: %w", err) + } + + ret.InstanceBytes = &instanceIDBytes + if instanceIDType != "" { + ret.InstanceType = &instanceIDType + } + } + + groupIDText, err := cmd.Flags().GetString("group-id") + if err != nil { + return nil, fmt.Errorf("group-id: %w", err) + } + + if groupIDText != "" { + groupIDBytes, groupIDType, err := parseID(groupIDText) + if err != nil { + return nil, fmt.Errorf("group-id: %w", err) + } + + ret.GroupBytes = &groupIDBytes + if groupIDType != "" { + ret.GroupType = &groupIDType + } + } + + return &ret, nil +} + +func RenderEnviroment(env *model.Environment) (string, error) { + parts, err := env.RenderParts() + if err != nil { + return "", err + } + + ret := "" + for _, part := range parts { + ret += fmt.Sprintf("%s: %s\n", part[0], part[1]) + } + + return ret, nil +} + +func parseID(text string) ([]byte, string, error) { + var typeText string + var valueText string + + parts := strings.SplitN(text, ":", 2) + if len(parts) == 2 { + typeText = parts[0] + valueText = parts[1] + } else { + valueText = text + } + + switch typeText { + case "uuid": + ret, err := uuid.Parse(valueText) + return ret[:], "uuid", err + case "oid": + var ret comid.OID + if err := ret.FromString(valueText); err != nil { + return nil, "oid", err + } + return []byte(ret), "oid", nil + case "hex": + ret, err := hex.DecodeString(valueText) + return ret, "hex", err + default: // assume base64 + // remove padding + valueText = strings.Trim(valueText, "=") + // if URL, convert to standard + valueText = strings.ReplaceAll(valueText, "-", "+") + valueText = strings.ReplaceAll(valueText, "_", "/") + + ret, err := base64.RawStdEncoding.DecodeString(valueText) + return ret, typeText, err + } +} diff --git a/cmd/corim-store/cmd/corim.go b/cmd/corim-store/cmd/corim.go index cd12ed5..496b792 100644 --- a/cmd/corim-store/cmd/corim.go +++ b/cmd/corim-store/cmd/corim.go @@ -14,6 +14,11 @@ import ( var corimCmd = &cobra.Command{ Use: "corim", Short: "CoRIM-related operations.", + Long: `CoRIM-related operations. + +Subcommands allow adding and removing CoRIMs from the store. See help for the +individual subcommands. + `, Run: func(cmd *cobra.Command, args []string) { CheckErr(runAddCommand(cmd, args)) @@ -23,7 +28,13 @@ var corimCmd = &cobra.Command{ var addCmd = &cobra.Command{ Use: "add PATH [PATH ...]", Short: "Add a CoRIM's contents to the store.", - Args: cobra.MinimumNArgs(1), + Long: `Add a CoRIM's contents to the store. + +The specified CoRIM(s) will be parsed and added as a "manifest" to the store. +Currently, CoRIMs containing only CoMID tags, and CoMID tags containing only +reference-triple's, endorsed-triple's, and attest-key-triple's, are supported. + `, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { CheckErr(runAddCommand(cmd, args)) @@ -33,7 +44,13 @@ var addCmd = &cobra.Command{ var deleteCmd = &cobra.Command{ Use: "delete PATH_OR_MANIFEST_ID", Short: "Delete data associated with the specified CoRIM or manifest ID.", - Args: cobra.MinimumNArgs(1), + Long: `Delete data associated with the specified CoRIM or manifest ID. + +You can specify the manifest ID directly, or you can specify a path to a CoRIM, in which case +the ID will be extracted from it (note: either way, the matching is done based on the ID so +the CoRIM specified does not literally have to be the same file that was previously added). + `, + Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { CheckErr(runDeleteCommand(cmd, args)) @@ -43,7 +60,13 @@ var deleteCmd = &cobra.Command{ var dumpCmd = &cobra.Command{ Use: "dump MANIFEST_ID", Short: "Write a CoRIM containing data associated with the specified manifest ID.", - Args: cobra.ExactArgs(1), + Long: `Write a CoRIM containing data associated with the specified manifest ID. + +This produces an unsigned CoRIM token containing the data associated with the +specified MANIFEST_ID. It is a way to easily "retrieve" a previously added +CoRIM. + `, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { CheckErr(runDumpCommand(cmd, args)) @@ -56,6 +79,11 @@ func runAddCommand(cmd *cobra.Command, args []string) error { return err } + activate, err := cmd.Flags().GetBool("activate") + if err != nil { + return err + } + store, err := store.Open(context.Background(), cliConfig.Store()) if err != nil { return err @@ -68,7 +96,7 @@ func runAddCommand(cmd *cobra.Command, args []string) error { return fmt.Errorf("error reading %s: %w", path, err) } - if err := store.AddBytes(bytes, label); err != nil { + if err := store.AddBytes(bytes, label, activate); err != nil { return fmt.Errorf("error adding %s: %w", path, err) } @@ -191,6 +219,8 @@ func init() { corimCmd.PersistentFlags().StringP("label", "l", "", "Label that will be applied to the manifest in the store.") + addCmd.Flags().BoolP("activate", "a", false, "Activate added triples.") + deleteCmd.Flags().BoolP("corim", "C", false, "force interpretation the positional argument as a path to CoRIM") diff --git a/cmd/corim-store/cmd/get.go b/cmd/corim-store/cmd/get.go index dda4a35..47c861f 100644 --- a/cmd/corim-store/cmd/get.go +++ b/cmd/corim-store/cmd/get.go @@ -2,13 +2,9 @@ package cmd import ( "context" - "encoding/base64" - "encoding/hex" "errors" "fmt" - "strings" - "github.com/google/uuid" "github.com/spf13/cobra" "github.com/veraison/corim-store/pkg/model" "github.com/veraison/corim-store/pkg/store" @@ -18,7 +14,21 @@ import ( var getCmd = &cobra.Command{ Use: "get", Short: "Get triples matching specified environment.", - Args: cobra.NoArgs, + Long: `Get triples matching specified environment. + +Flags are used to specify the elements of the environment. Multiple flags can +be used together (e.g. you can specify a class ID and a model). If a particular +environment element is not specified, it can be any value in the matched +environments; unless --exact flag is also used, in which case unspecified elements +must also be unset in the matched environments. + +In addition to environment matching, flags can be used to specify that you only +want to get active triples, and/or only reference values or only trust anchors +(by default, all triples with matching environments will be returned). + +The triples are returned encoded as JSON. + `, + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { CheckErr(runGetCommand(cmd, args)) @@ -36,7 +46,7 @@ func runGetCommand(cmd *cobra.Command, args []string) error { return err } - env, err := buildEnvironment(cmd) + env, err := BuildEnvironment(cmd) if err != nil { return err } @@ -50,6 +60,11 @@ func runGetCommand(cmd *cobra.Command, args []string) error { return err } + activeOnly, err := cmd.Flags().GetBool("active") + if err != nil { + return err + } + store, err := store.Open(context.Background(), cliConfig.Store()) if err != nil { return err @@ -59,7 +74,15 @@ func runGetCommand(cmd *cobra.Command, args []string) error { var result comid.Triples if selector.Endorsements || selector.ReferenceValues { - found, err := store.GetValueTriples(env, label, exact) + var found []*model.ValueTriple + var err error + + if activeOnly { + found, err = store.GetActiveValueTriples(env, label, exact) + } else { + found, err = store.GetValueTriples(env, label, exact) + } + if err != nil { return err } @@ -76,7 +99,15 @@ func runGetCommand(cmd *cobra.Command, args []string) error { } if selector.TrustAnchors { - found, err := store.GetKeyTriples(env, label, exact) + var found []*model.KeyTriple + var err error + + if activeOnly { + found, err = store.GetActiveKeyTriples(env, label, exact) + } else { + found, err = store.GetKeyTriples(env, label, exact) + } + if err != nil { return err } @@ -132,143 +163,8 @@ func buildSelector(cmd *cobra.Command) (*LookupMap, error) { return &ret, nil } -func buildEnvironment(cmd *cobra.Command) (*model.Environment, error) { - var ret model.Environment - - vendor, err := cmd.Flags().GetString("vendor") - if err != nil { - return nil, fmt.Errorf("vendor: %w", err) - } - if vendor != "" { - ret.Vendor = &vendor - } - - model, err := cmd.Flags().GetString("model") - if err != nil { - return nil, fmt.Errorf("model: %w", err) - } - if model != "" { - ret.Model = &model - } - - layerInt, err := cmd.Flags().GetInt64("layer") - if err != nil { - return nil, fmt.Errorf("layer: %w", err) - } - if layerInt > -1 { - layer := uint64(layerInt) - ret.Layer = &layer - } - - indexInt, err := cmd.Flags().GetInt64("index") - if err != nil { - return nil, fmt.Errorf("index: %w", err) - } - if indexInt > -1 { - index := uint64(indexInt) - ret.Index = &index - } - - classIDText, err := cmd.Flags().GetString("class-id") - if err != nil { - return nil, fmt.Errorf("class-id: %w", err) - } - - if classIDText != "" { - classIDBytes, classIDType, err := parseID(classIDText) - if err != nil { - return nil, fmt.Errorf("class-id: %w", err) - } - - ret.ClassBytes = &classIDBytes - if classIDType != "" { - ret.ClassType = &classIDType - } - } - - instanceIDText, err := cmd.Flags().GetString("instance-id") - if err != nil { - return nil, fmt.Errorf("instance-id: %w", err) - } - - if instanceIDText != "" { - instanceIDBytes, instanceIDType, err := parseID(instanceIDText) - if err != nil { - return nil, fmt.Errorf("instance-id: %w", err) - } - - ret.InstanceBytes = &instanceIDBytes - if instanceIDType != "" { - ret.InstanceType = &instanceIDType - } - } - - groupIDText, err := cmd.Flags().GetString("group-id") - if err != nil { - return nil, fmt.Errorf("group-id: %w", err) - } - - if groupIDText != "" { - groupIDBytes, groupIDType, err := parseID(groupIDText) - if err != nil { - return nil, fmt.Errorf("group-id: %w", err) - } - - ret.GroupBytes = &groupIDBytes - if groupIDType != "" { - ret.GroupType = &groupIDType - } - } - - return &ret, nil -} - -func parseID(text string) ([]byte, string, error) { - var typeText string - var valueText string - - parts := strings.SplitN(text, ":", 2) - if len(parts) == 2 { - typeText = parts[0] - valueText = parts[1] - } else { - valueText = text - } - - switch typeText { - case "uuid": - ret, err := uuid.Parse(valueText) - return ret[:], "uuid", err - case "oid": - var ret comid.OID - if err := ret.FromString(valueText); err != nil { - return nil, "oid", err - } - return []byte(ret), "oid", nil - case "hex": - ret, err := hex.DecodeString(valueText) - return ret, "hex", err - default: // assume base64 - // remove padding - valueText = strings.Trim(valueText, "=") - // if URL, convert to standard - valueText = strings.ReplaceAll(valueText, "-", "+") - valueText = strings.ReplaceAll(valueText, "_", "/") - - ret, err := base64.RawStdEncoding.DecodeString(valueText) - return ret, typeText, err - } -} - func init() { - getCmd.Flags().StringP("class-id", "C", "", "Environment class ID.") - getCmd.Flags().StringP("vendor", "V", "", "Environment vendor.") - getCmd.Flags().StringP("model", "M", "", "Environment model.") - getCmd.Flags().Int64P("layer", "L", -1, "Environment layer.") - getCmd.Flags().Int64P("index", "I", -1, "Environment index.") - getCmd.Flags().StringP("instance-id", "i", "", "Environment instance ID") - getCmd.Flags().StringP("group-id", "g", "", "Environment group ID") - + AddEnviromentFlags(getCmd) getCmd.Flags().BoolP("reference-values", "R", false, "Look up reference values.") getCmd.Flags().BoolP("endorsements", "E", false, "Look up endorsements.") getCmd.Flags().BoolP("trust-anchors", "T", false, "Look up trust anchors.") @@ -279,6 +175,7 @@ func init() { getCmd.Flags().BoolP("exact", "e", false, "Match environments exactly, including null fields. The default is to assume that "+ "null fields (i.e. fields not explicitly specified) can match any value.") + getCmd.Flags().BoolP("active", "a", false, "Only look up active triples.") rootCmd.AddCommand(getCmd) } diff --git a/cmd/corim-store/cmd/lifecycle.go b/cmd/corim-store/cmd/lifecycle.go new file mode 100644 index 0000000..72da5d7 --- /dev/null +++ b/cmd/corim-store/cmd/lifecycle.go @@ -0,0 +1,201 @@ +package cmd + +import ( + "context" + "database/sql" + "fmt" + + "github.com/spf13/cobra" + "github.com/uptrace/bun" + "github.com/veraison/corim-store/pkg/model" + "github.com/veraison/corim-store/pkg/store" +) + +var activateCmd = &cobra.Command{ + Use: "activate", + Short: "Activate a (set of) triple(s), making them available to the verifier.", + Args: cobra.NoArgs, + + Run: func(cmd *cobra.Command, args []string) { + CheckErr(setActiveCommand(cmd, true)) + }, +} + +var deactivateCmd = &cobra.Command{ + Use: "deactivate", + Short: "Deactivate a (set of) triple(s), making them unavailable to the verifier.", + Args: cobra.NoArgs, + + Run: func(cmd *cobra.Command, args []string) { + CheckErr(setActiveCommand(cmd, false)) + }, +} + +func setActiveCommand(cmd *cobra.Command, value bool) error { + store, err := store.Open(context.Background(), cliConfig.Store()) + if err != nil { + return err + } + defer func() { CheckErr(store.Close()) }() + + keyTripleIDs, err := cmd.Flags().GetInt64Slice("key-triple") + if err != nil { + return err + } + + if len(keyTripleIDs) != 0 { + if err := keyTriplesSetActive(store, keyTripleIDs, value); err != nil { + return fmt.Errorf("key triples: %w", err) + } + } + + valueTripleIDs, err := cmd.Flags().GetInt64Slice("value-triple") + if err != nil { + return err + } + + if len(valueTripleIDs) != 0 { + if err := valueTriplesSetActive(store, valueTripleIDs, value); err != nil { + return fmt.Errorf("value triples: %w", err) + } + } + + moduleTagTextIDs, err := cmd.Flags().GetStringSlice("module-tag") + if err != nil { + return err + } + + if len(moduleTagTextIDs) != 0 { + var moduleTagIDs []int64 + for _, idText := range moduleTagTextIDs { + var ids []int64 + err := store.DB.NewSelect(). + TableExpr("module_tags as mod"). + ColumnExpr("mod.id as id"). + Where("mod.tag_id = ?", idText). + Scan(store.Ctx, &ids) + + if err != nil { + return err + } + + moduleTagIDs = append(moduleTagIDs, ids...) + } + + if err := moduleTagsSetActive(store, moduleTagIDs, value); err != nil { + return fmt.Errorf("module tags: %w", err) + } + } + + manifestTextIDs, err := cmd.Flags().GetStringSlice("manifest") + if err != nil { + return err + } + + if len(manifestTextIDs) != 0 { + var moduleTagIDs []int64 + for _, idText := range manifestTextIDs { + var ids []int64 + err = store.DB.NewSelect(). + TableExpr("module_tags as mod"). + ColumnExpr("mod.id as id"). + Join("JOIN manifests AS man ON man.id = mod.manifest_id"). + Where("man.manifest_id = ?", idText). + Scan(store.Ctx, &ids) + + if err != nil { + return err + } + + moduleTagIDs = append(moduleTagIDs, ids...) + } + + if err := moduleTagsSetActive(store, moduleTagIDs, value); err != nil { + return fmt.Errorf("manifests: %w", err) + } + } + + return nil +} + +func moduleTagsSetActive(store *store.Store, ids []int64, value bool) error { + for _, id := range ids { + var keyTripleIDs []int64 + noKeyTriples := false + + err := store.DB.NewSelect(). + Model(&keyTripleIDs). + TableExpr("key_triples as kt"). + Column("id"). + Where("kt.module_id = ?", id). + Scan(store.Ctx) + + if err != nil && err != sql.ErrNoRows { // nolint:gocritic + return fmt.Errorf("scanning key triples: %w", err) + } else if err == sql.ErrNoRows || len(keyTripleIDs) == 0 { + noKeyTriples = true + } else { + if err := keyTriplesSetActive(store, keyTripleIDs, true); err != nil { + return err + } + } + + var valueTripleIDs []int64 + noValueTriples := false + + err = store.DB.NewSelect(). + Model(&valueTripleIDs). + TableExpr("value_triples as vt"). + Column("id"). + Where("vt.module_id = ?", id). + Scan(store.Ctx) + + if err != nil && err != sql.ErrNoRows { // nolint:gocritic + return fmt.Errorf("scanning value triples: %w", err) + } else if err == sql.ErrNoRows || len(valueTripleIDs) == 0 { + noValueTriples = true + } else { + if err := valueTriplesSetActive(store, valueTripleIDs, true); err != nil { + return err + } + } + + if noKeyTriples && noValueTriples { + return fmt.Errorf("no triples associated with module tag ID %d", id) + } + } + + return nil +} + +func keyTriplesSetActive(store *store.Store, ids []int64, value bool) error { + return setActive[model.KeyTriple](store, ids, value) +} + +func valueTriplesSetActive(store *store.Store, ids []int64, value bool) error { + return setActive[model.ValueTriple](store, ids, value) +} + +func setActive[T any](store *store.Store, ids []int64, value bool) error { + _, err := store.DB.NewUpdate(). + Model((*T)(nil)). + Set("is_active = ?", value). + Where("id IN (?)", bun.In(ids)). + Exec(store.Ctx) + + return err +} + +func addActivateFlags(cmd *cobra.Command) { + cmd.Flags().Int64Slice("key-triple", []int64{}, "Specify the key triple database ID.") + cmd.Flags().Int64Slice("value-triple", []int64{}, "Specify the key triple database ID.") + cmd.Flags().StringSlice("module-tag", []string{}, "Specify the module tag UUID.") + cmd.Flags().StringSlice("manifest", []string{}, "Specify the manifest ID.") +} + +func init() { + addActivateFlags(activateCmd) + addActivateFlags(deactivateCmd) + rootCmd.AddCommand(activateCmd) + rootCmd.AddCommand(deactivateCmd) +} diff --git a/cmd/corim-store/cmd/list.go b/cmd/corim-store/cmd/list.go index 7ab5ab6..7e47488 100644 --- a/cmd/corim-store/cmd/list.go +++ b/cmd/corim-store/cmd/list.go @@ -2,20 +2,29 @@ package cmd import ( "context" + "database/sql" + "errors" "fmt" "strings" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" "github.com/spf13/cobra" + "github.com/veraison/corim-store/pkg/model" "github.com/veraison/corim-store/pkg/store" "github.com/veraison/corim-store/pkg/util" ) var listCmd = &cobra.Command{ - Use: "list", + Use: "list WHAT", Short: "List entries of a particular type.", - Args: cobra.ExactArgs(1), + Long: `List all entries of a particular type in the store. + +The WHAT can be "manifests"/"corims", "modules"/"module_tags"/"comids", +"entities", or "triples" (slashes indicate alternate names for the same type +of entry). When the WHAT is \"triples\", flags can be used to filter the +results by environment elements (e.g. by model or instance ID)."`, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { CheckErr(runListCommand(cmd, args)) @@ -26,6 +35,19 @@ func runListCommand(cmd *cobra.Command, args []string) error { var err error what := util.Normalize(args[0]) + env, err := BuildEnvironment(cmd) + CheckErr(err) + + label, err := cmd.Flags().GetString("label") + if err != nil { + return err + } + + exact, err := cmd.Flags().GetBool("exact") + if err != nil { + return err + } + store, err := store.Open(context.Background(), cliConfig.Store()) if err != nil { return err @@ -35,6 +57,10 @@ func runListCommand(cmd *cobra.Command, args []string) error { var header []any var rows [][]any + if what != "triples" && !env.IsEmpty() { + return errors.New("environment specifiers are only allowed for triples") + } + switch what { case "manifests", "corims": header, rows, err = listManifests(store) @@ -42,6 +68,8 @@ func runListCommand(cmd *cobra.Command, args []string) error { header, rows, err = listModuleTags(store) case "entities": header, rows, err = listEntities(store) + case "triples": + header, rows, err = listTriples(store, env, label, exact) default: return fmt.Errorf("unsupported list target: %s", what) } @@ -61,6 +89,7 @@ func runListCommand(cmd *cobra.Command, args []string) error { colConfigs = append(colConfigs, table.ColumnConfig{ Name: h.(string), AlignHeader: text.AlignCenter, + VAlign: text.VAlignMiddle, }) } tw.SetColumnConfigs(colConfigs) @@ -103,7 +132,7 @@ func listManifests(store *store.Store) ([]any, [][]any, error) { for _, match := range matches { retRow := make([]any, 0, len(retCols)+1) for _, col := range retCols { - retRow = append(retRow, match[col.(string)]) + retRow = append(retRow, unwrapNullableTypes(match[col.(string)])) } ret = append(ret, retRow) @@ -117,14 +146,14 @@ func listModuleTags(store *store.Store) ([]any, [][]any, error) { retCols := make([]any, 0, len(columns)+1) var matches []map[string]any - err := store.DB.NewSelect().TableExpr("module_tags AS mod"). + err := store.DB.NewSelect().TableExpr("module_tags AS mt"). ColumnExpr("tag_id"). ColumnExpr("language"). ColumnExpr(fmt.Sprintf("%s AS entities", store.StringAggregatorExpr("ent.name"))). ColumnExpr("man.manifest_id as manifest"). ColumnExpr("man.label as label"). - Join("LEFT JOIN entities as ent ON ent.owner_id = mod.id AND ent.owner_type = 'module_tag'"). - Join("LEFT JOIN manifests as man ON man.id = mod.manifest_id"). + Join("LEFT JOIN entities as ent ON ent.owner_id = mt.id AND ent.owner_type = 'module_tag'"). + Join("LEFT JOIN manifests as man ON man.id = mt.manifest_id"). GroupExpr(strings.Join(columns, ", ")). Scan(store.Ctx, &matches) @@ -141,7 +170,7 @@ func listModuleTags(store *store.Store) ([]any, [][]any, error) { for _, match := range matches { retRow := make([]any, 0, len(retCols)+1) for _, col := range retCols { - retRow = append(retRow, match[col.(string)]) + retRow = append(retRow, unwrapNullableTypes(match[col.(string)])) } ret = append(ret, retRow) @@ -178,9 +207,9 @@ func listEntities(store *store.Store) ([]any, [][]any, error) { for _, match := range matches { retRow := make([]any, 0, len(retCols)+1) for _, col := range columns { - retRow = append(retRow, match[col]) + retRow = append(retRow, unwrapNullableTypes(match[col])) } - retRow = append(retRow, match["roles"]) + retRow = append(retRow, unwrapNullableTypes(match["roles"])) ret = append(ret, retRow) } @@ -188,6 +217,141 @@ func listEntities(store *store.Store) ([]any, [][]any, error) { return retCols, ret, nil } +func listTriples( + store *store.Store, + env *model.Environment, + label string, + exact bool, +) ([]any, [][]any, error) { + var matches []map[string]any + err := store.DB.NewSelect().TableExpr("module_tags AS mt"). + ColumnExpr("mt.id as id"). + ColumnExpr("tag_id as module"). + ColumnExpr("man.manifest_id as manifest"). + ColumnExpr("man.label as label"). + Join("LEFT JOIN manifests as man ON man.id = mt.manifest_id"). + Scan(store.Ctx, &matches) + + if err != nil { + return nil, nil, err + } + + lookup := make(map[int64]map[string]any) + for _, match := range matches { + lookup[match["id"].(int64)] = match + } + + keyTriples, err := store.GetKeyTriples(env, label, exact) + if err != nil { + return nil, nil, fmt.Errorf("getting key triples: %w", err) + } + + valueTriples, err := store.GetValueTriples(env, label, exact) + if err != nil { + return nil, nil, fmt.Errorf("getting value triples: %w", err) + } + + columns := []any{"id", "active", "label", "manifest", "module", "type", "environment"} + rows := make([][]any, 0, len(valueTriples)+len(keyTriples)) + + for _, kt := range keyTriples { + module, ok := lookup[kt.ModuleID] + if !ok { + return nil, nil, fmt.Errorf("orphan key triple: %d", kt.ID) + } + + envText, err := RenderEnviroment(kt.Environment) + if err != nil { + return nil, nil, fmt.Errorf("environment for key triple %d: %w", kt.ID, err) + } + + rows = append(rows, []any{ + kt.ID, + kt.IsActive, + unwrapNullableTypes(module["label"]), + unwrapNullableTypes(module["manifest"]), + unwrapNullableTypes(module["module"]), + fmt.Sprintf("%s key", kt.Type), + envText, + }) + } + + for _, vt := range valueTriples { + module, ok := lookup[vt.ModuleID] + if !ok { + return nil, nil, fmt.Errorf("orphan value triple: %d", vt.ID) + } + + envText, err := RenderEnviroment(vt.Environment) + if err != nil { + return nil, nil, fmt.Errorf("environment for value triple %d: %w", vt.ID, err) + } + + rows = append(rows, []any{ + vt.ID, + vt.IsActive, + unwrapNullableTypes(module["label"]), + unwrapNullableTypes(module["manifest"]), + unwrapNullableTypes(module["module"]), + fmt.Sprintf("%s value", vt.Type), + envText, + }) + } + + return columns, rows, nil +} + +func unwrapNullableTypes(val any) any { + switch t := val.(type) { + case sql.NullString: + if t.Valid { + return t.String + } else { + return nil + } + case sql.NullInt64: + if t.Valid { + return t.Int64 + } else { + return nil + } + case sql.NullInt32: + if t.Valid { + return t.Int32 + } else { + return nil + } + case sql.NullInt16: + if t.Valid { + return t.Int16 + } else { + return nil + } + case sql.NullByte: + if t.Valid { + return t.Byte + } else { + return nil + } + case sql.NullBool: + if t.Valid { + return t.Bool + } else { + return nil + } + default: + return t + } +} + func init() { + AddEnviromentFlags(listCmd) + listCmd.Flags().StringP("label", "l", "", + "Label that will be applied to the manifest in the store.") + + listCmd.Flags().BoolP("exact", "e", false, + "Match environments exactly, including null fields. The default is to assume that "+ + "null fields (i.e. fields not explicitly specified) can match any value.") + rootCmd.AddCommand(listCmd) } diff --git a/cmd/corim-store/cmd/root.go b/cmd/corim-store/cmd/root.go index e6f3879..7df6da8 100644 --- a/cmd/corim-store/cmd/root.go +++ b/cmd/corim-store/cmd/root.go @@ -40,7 +40,8 @@ func init() { ) rootCmd.PersistentFlags().Bool( - "insecure", false, "Allow insecure operations.", + "insecure", false, "Allow insecure operations. Currently, this allows processing signed "+ + "commits without verifying their signature (unsigned commits are always processed).", ) rootCmd.PersistentFlags().Bool( @@ -48,15 +49,18 @@ func init() { ) rootCmd.PersistentFlags().Bool( - "force", false, "Force an operation that would otherwise fail (use with care!).", + "force", false, "Force an operation that would otherwise fail potentially overwriting "+ + "exiting artefacts (use with care!).", ) rootCmd.PersistentFlags().StringP( - "dbms", "D", "sqlite", "DataBase Management System type", + "dbms", "D", "sqlite", "DataBase Management System type. Allowed values are \"sqlite\", "+ + "\"mysql\"/\"mariadb\", and \"postgres\"/\"pg\"/\"pq\".", ) rootCmd.PersistentFlags().StringP( - "dsn", "N", "file:store.db?cache=shared", "Datadase System Name", + "dsn", "N", "file:store.db?cache=shared", "Database System Name. This is used to connect to "+ + "the database server. The format of this string is DBMS-specific.", ) rootCmd.PersistentFlags().VisitAll(func(flag *pflag.Flag) { diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..1b499e0 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,54 @@ +FROM ubuntu:24.04 AS corim-store-db + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install \ + --assume-yes \ + --no-install-recommends \ + mariadb-server \ + postgresql \ + supervisor \ + vim \ + && \ + apt-get clean && \ + apt-get autoremove --assume-yes && \ + rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp* + +RUN mkdir -p /var/run/mysqld && chown -R mysql:mysql /var/run/mysqld && \ + mkdir -p /tmp && chmod 1777 /tmp && \ + mariadb-install-db --user=mysql --ldata=/var/lib/mysql && \ + sed -i "s/^bind-address\s*=.*/bind-address = 0.0.0.0/" /etc/mysql/mariadb.conf.d/50-server.cnf + +RUN mariadbd-safe --datadir=/var/lib/mysql & \ + sleep 2 && \ + until mariadb -uroot -e "SELECT 1" &>/dev/null; do sleep 1; done && \ + mariadb -uroot <> /var/lib/postgresql/data/pg_hba.conf && \ + /usr/lib/postgresql/16/bin/postgres --single -D /var/lib/postgresql/data postgres <") +} + +func TestMeasurementsToCoRIM_nok(t *testing.T) { + testKT := "foo" + _, err := MeasurementsToCoRIM([]*Measurement{{KeyType: &testKT}}) + assert.ErrorContains(t, err, "could not convert measurement at index 0") +} + +func TestParseVersionScheme(t *testing.T) { + testCases := []struct { + scheme string + err string + }{ + {scheme: "multipartnumeric"}, + {scheme: "multipartnumeric+suffix"}, + {scheme: "alphanumeric"}, + {scheme: "decimal"}, + {scheme: "semver"}, + {scheme: "version-scheme(1)"}, + { + scheme: "foo", + err: "invalid version scheme: foo", + }, + } + + for _, tc := range testCases { + t.Run(tc.scheme, func(t *testing.T) { + _, err := parseVersionScheme(tc.scheme) + if tc.err == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + }) + } +} diff --git a/pkg/model/moduletag.go b/pkg/model/moduletag.go index 46be977..b27d194 100644 --- a/pkg/model/moduletag.go +++ b/pkg/model/moduletag.go @@ -13,7 +13,7 @@ import ( ) type ModuleTag struct { - bun.BaseModel `bun:"table:module_tags,alias:mod"` + bun.BaseModel `bun:"table:module_tags,alias:mt"` ID int64 `bun:",pk,autoincrement"` @@ -61,7 +61,7 @@ func (o *ModuleTag) FromCoRIM(origin *comid.Comid) error { var err error if origin.Triples.CondEndorseSeries != nil && len(origin.Triples.CondEndorseSeries.Values) != 0 { - return errors.New("conditional endosement series are not supported") // TODO + return errors.New("conditional endorsement series are not supported") // TODO } o.Language = origin.Language @@ -194,6 +194,16 @@ func (o *ModuleTag) ToCoRIM() (*comid.Comid, error) { return ret, nil } +func (o *ModuleTag) SetActive(value bool) { + for _, kt := range o.KeyTriples { + kt.IsActive = value + } + + for _, vt := range o.ValueTriples { + vt.IsActive = value + } +} + func (o *ModuleTag) Insert(ctx context.Context, db bun.IDB) error { if err := o.Validate(); err != nil { return err @@ -270,7 +280,7 @@ func (o *ModuleTag) Select(ctx context.Context, db bun.IDB) error { Relation("KeyTriples"). Relation("Extensions"). Relation("TriplesExtensions"). - Where("mod.id = ?", o.ID). + Where("mt.id = ?", o.ID). Scan(ctx) if err != nil { diff --git a/pkg/model/moduletag_test.go b/pkg/model/moduletag_test.go index f1afb1e..206235d 100644 --- a/pkg/model/moduletag_test.go +++ b/pkg/model/moduletag_test.go @@ -21,7 +21,7 @@ func TestModuleTag_round_trip(t *testing.T) { Name: comid.MustNewEntityName("foo", "string"), Roles: *comid.NewRoles().Add(comid.RoleCreator), }) - test_cases := []struct { + testCases := []struct { title string mt comid.Comid }{ @@ -90,7 +90,7 @@ func TestModuleTag_round_trip(t *testing.T) { }, } - for _, tc := range test_cases { + for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { mt, err := NewModuleTagFromCoRIM(&tc.mt) assert.NoError(t, err) @@ -112,7 +112,7 @@ func TestModuleTag_round_trip(t *testing.T) { func TestModuleTag_Validate(t *testing.T) { testType := comid.BytesType testBytes := comid.MustHexDecode(t, "deadbeefdeadbeefdeadbeefdeadbeef") - test_cases := []struct { + testCases := []struct { title string mt ModuleTag err string @@ -156,7 +156,7 @@ func TestModuleTag_Validate(t *testing.T) { }, } - for _, tc := range test_cases { + for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { err := tc.mt.Validate() if tc.err == "" { @@ -167,3 +167,94 @@ func TestModuleTag_Validate(t *testing.T) { }) } } + +func TestModuleTag_ToCoRIM(t *testing.T) { + testCases := []struct { + title string + mt ModuleTag + err string + }{ + { + title: "ok UUID tag ID", + mt: ModuleTag{ + TagIDType: comid.UUIDType, + TagID: comid.TestUUID.String(), + }, + }, + { + title: "nok bad UUID tag ID", + mt: ModuleTag{ + TagIDType: comid.UUIDType, + TagID: "foo", + }, + err: "invalid UUID length", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + _, err := tc.mt.ToCoRIM() + if tc.err == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + }) + } +} + +func TestModuleTag_Insert_extensions(t *testing.T) { + db := test.NewTestDB(t) + testType := comid.BytesType + testBytes := comid.MustHexDecode(t, "deadbeefdeadbeefdeadbeefdeadbeef") + mt := ModuleTag{ + TagIDType: StringTagID, + TagID: "foo", + KeyTriples: []*KeyTriple{ + { + Type: AttestKeyTriple, + Environment: &Environment{ + ClassType: &testType, + ClassBytes: &testBytes, + }, + KeyList: []*CryptoKey{ + &CryptoKey{ + KeyType: comid.PKIXBase64KeyType, + KeyBytes: []byte(comid.TestECPubKey), + }, + }, + }, + }, + Extensions: []*ExtensionValue{{}}, + TriplesExtensions: []*ExtensionValue{{}}, + } + + err := mt.Insert(context.Background(), db) + assert.NoError(t, err) + assert.Equal(t, mt.Extensions[0].ID, int64(1)) + assert.Equal(t, mt.TriplesExtensions[0].ID, int64(2)) +} + +func TestModuleTag_Select(t *testing.T) { + var mt ModuleTag + db := test.NewTestDB(t) + + err := mt.Select(context.Background(), db) + assert.ErrorContains(t, err, "ID not set") + + mt.ID = 1 + err = mt.Select(context.Background(), db) + assert.ErrorContains(t, err, "no rows in result") +} + +func TestModuleTag_Delete(t *testing.T) { + var mt ModuleTag + db := test.NewTestDB(t) + + err := mt.Delete(context.Background(), db) + assert.ErrorContains(t, err, "ID not set") + + mt.ID = 1 + err = mt.Delete(context.Background(), db) + assert.NoError(t, err) +} diff --git a/pkg/model/role.go b/pkg/model/role.go index 2ace02d..fe8e059 100644 --- a/pkg/model/role.go +++ b/pkg/model/role.go @@ -43,8 +43,8 @@ func ParseCoRIMRole(text string) (corim.Role, error) { default: matches := roleRegex.FindStringSubmatch(text) switch len(matches) { - case 1: - role, err := strconv.Atoi(matches[0]) + case 2: + role, err := strconv.Atoi(matches[1]) return corim.Role(role), err default: return corim.Role(0), fmt.Errorf("invalid CoRIM role: %s", text) @@ -63,8 +63,8 @@ func ParseCoMIDRole(text string) (comid.Role, error) { default: matches := roleRegex.FindStringSubmatch(text) switch len(matches) { - case 1: - role, err := strconv.Atoi(matches[0]) + case 2: + role, err := strconv.Atoi(matches[1]) return comid.Role(role), err default: return comid.Role(0), fmt.Errorf("invalid CoMID role: %s", text) @@ -83,13 +83,12 @@ type RoleEntry struct { } func MustNewCoRIMRoleEntry(text string) *RoleEntry { - if _, err := ParseCoRIMRole(text); err != nil { + ret, err := NewCoRIMRoleEntry(text) + if err != nil { panic(err) } - ret := RoleEntry{Role: text} - - return &ret + return ret } func NewCoRIMRoleEntry(text string) (*RoleEntry, error) { @@ -103,13 +102,12 @@ func NewCoRIMRoleEntry(text string) (*RoleEntry, error) { } func MustNewCoMIDRoleEntry(text string) *RoleEntry { - if _, err := ParseCoMIDRole(text); err != nil { + ret, err := NewCoMIDRoleEntry(text) + if err != nil { panic(err) } - ret := RoleEntry{Role: text} - - return &ret + return ret } func NewCoMIDRoleEntry(text string) (*RoleEntry, error) { diff --git a/pkg/model/role_test.go b/pkg/model/role_test.go new file mode 100644 index 0000000..f6816bc --- /dev/null +++ b/pkg/model/role_test.go @@ -0,0 +1,97 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/veraison/corim/comid" + "github.com/veraison/corim/corim" +) + +func TestParseCoRIMRole(t *testing.T) { + testCases := []struct { + role string + expected corim.Role + err string + }{ + { + role: "manifestCreator", + expected: corim.RoleManifestCreator, + }, + { + role: "manifestSigner", + expected: corim.RoleManifestSigner, + }, + { + role: "Role(3)", + expected: corim.Role(3), + }, + { + role: "foo", + err: "invalid CoRIM role: foo", + }, + } + + for _, tc := range testCases { + t.Run(tc.role, func(t *testing.T) { + role, err := ParseCoRIMRole(tc.role) + if tc.err == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expected, role) + } else { + assert.ErrorContains(t, err, tc.err) + } + }) + } +} + +func TestParseCoMIDRole(t *testing.T) { + testCases := []struct { + role string + expected comid.Role + err string + }{ + { + role: "creator", + expected: comid.RoleCreator, + }, + { + role: "maintainer", + expected: comid.RoleMaintainer, + }, + { + role: "Role(3)", + expected: comid.Role(3), + }, + { + role: "foo", + err: "invalid CoMID role: foo", + }, + } + + for _, tc := range testCases { + t.Run(tc.role, func(t *testing.T) { + role, err := ParseCoMIDRole(tc.role) + if tc.err == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expected, role) + } else { + assert.ErrorContains(t, err, tc.err) + } + }) + } +} + +func TestMustNewCoRIMRoleEntry(t *testing.T) { + entry := MustNewCoRIMRoleEntry("manifestCreator") + assert.Equal(t, "manifestCreator", entry.Role) + + assert.Panics(t, func() { MustNewCoRIMRoleEntry("foo") }) +} + +func TestMustNewCoMIDRoleEntry(t *testing.T) { + entry := MustNewCoMIDRoleEntry("creator") + assert.Equal(t, "creator", entry.Role) + + assert.Panics(t, func() { MustNewCoMIDRoleEntry("foo") }) +} diff --git a/pkg/model/valuetriple.go b/pkg/model/valuetriple.go index c978f43..89ec4d1 100644 --- a/pkg/model/valuetriple.go +++ b/pkg/model/valuetriple.go @@ -69,7 +69,10 @@ type ValueTriple struct { Type ValueTripleType Measurements []*Measurement `bun:"rel:has-many,join:id=owner_id,join:type=owner_type,polymorphic:value_triple"` - ModuleID int64 `bun:",nullzero"` + + IsActive bool + + ModuleID int64 `bun:",nullzero"` } func NewValueTripleFromCoRIM(origin *comid.ValueTriple) (*ValueTriple, error) { @@ -213,7 +216,13 @@ func (o *ValueTriple) Delete(ctx context.Context, db bun.IDB) error { return err } - return o.Environment.DeleteIfOrphaned(ctx, db) + if o.Environment != nil { + if err := o.Environment.DeleteIfOrphaned(ctx, db); err != nil { + return fmt.Errorf("environment: %w", err) + } + } + + return nil } func (o *ValueTriple) TripleType() string { diff --git a/pkg/model/valuetriple_test.go b/pkg/model/valuetriple_test.go index 33305b2..9fb3798 100644 --- a/pkg/model/valuetriple_test.go +++ b/pkg/model/valuetriple_test.go @@ -19,7 +19,7 @@ func TestValueTriple_round_trip(t *testing.T) { testSvn, err := comid.NewTaggedSVN(42) require.NoError(t, err) - test_cases := []struct { + testCases := []struct { title string vt comid.ValueTriple }{ @@ -39,7 +39,7 @@ func TestValueTriple_round_trip(t *testing.T) { }, } - for _, tc := range test_cases { + for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { vt, err := NewValueTripleFromCoRIM(&tc.vt) assert.NoError(t, err) @@ -66,7 +66,7 @@ func TestValueTriple_round_trip(t *testing.T) { func TestValueTriple_Validate(t *testing.T) { testType := comid.BytesType testBytes := comid.MustHexDecode(t, "deadbeefdeadbeefdeadbeefdeadbeef") - test_cases := []struct { + testCases := []struct { title string vt ValueTriple err string @@ -117,7 +117,7 @@ func TestValueTriple_Validate(t *testing.T) { }, } - for _, tc := range test_cases { + for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { err := tc.vt.Validate() if tc.err == "" { @@ -128,3 +128,32 @@ func TestValueTriple_Validate(t *testing.T) { }) } } + +func TestValueTriple_Delete(t *testing.T) { + var vt ValueTriple + db := test.NewTestDB(t) + + err := vt.Delete(context.Background(), db) + assert.ErrorContains(t, err, "ID not set") + + vt = ValueTriple{ + ID: 1, + Measurements: []*Measurement{{ID: 1}}, + Environment: &Environment{ID: 1}, + } + err = vt.Delete(context.Background(), db) + assert.NoError(t, err) +} + +func TestValueTriple_TripleType(t *testing.T) { + var vt ValueTriple + assert.Equal(t, "value", vt.TripleType()) +} + +func TestValueTriple_DatabaseID(t *testing.T) { + var vt ValueTriple + assert.Equal(t, int64(0), vt.DatabaseID()) + + vt.ID = 1 + assert.Equal(t, int64(1), vt.DatabaseID()) +} diff --git a/pkg/store/config.go b/pkg/store/config.go index 8206b5a..9778370 100644 --- a/pkg/store/config.go +++ b/pkg/store/config.go @@ -52,7 +52,7 @@ func (o *Config) WithOptions(options ...ConfigOption) *Config { func (o *Config) Validate() error { if !slices.Contains([]string{ - "sqlite", "sqlite3", + "sqlite", "sqlite3", "mysql", "mariadb", "postgres", "pq", "pgx", }, o.DBMS) { return fmt.Errorf("invalid DBMS: %s", o.DBMS) } diff --git a/pkg/store/config_test.go b/pkg/store/config_test.go new file mode 100644 index 0000000..e411c1c --- /dev/null +++ b/pkg/store/config_test.go @@ -0,0 +1,76 @@ +package store + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_Validate(t *testing.T) { + testCases := []struct { + title string + dbms string + opts []ConfigOption + err string + }{ + { + title: "ok multi-opt", + dbms: "mysql", + opts: []ConfigOption{ + OptionMD5, + OptionTraceSQL, + OptionInsecure, + OptionForce, + OptionRequireLabel, + }, + }, + { + title: "ok sha256", + dbms: "mysql", + opts: []ConfigOption{OptionSHA256}, + }, + { + title: "ok sha512", + dbms: "mysql", + opts: []ConfigOption{OptionSHA512}, + }, + { + title: "invalid DBMS", + dbms: "foo", + err: "invalid DBMS: foo", + }, + { + title: "invalid hash alg", + dbms: "mysql", + opts: []ConfigOption{func(c *Config) { + c.HashAlg = "bar" + }}, + err: "invalid hash algorithm: bar", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + testConfig := NewConfig(tc.dbms, "", tc.opts...) + err := testConfig.Validate() + + if tc.err == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + }) + } +} + +func TestConfig_DB(t *testing.T) { + dbConfig := NewConfig("postgres", "foo").DB() + assert.Equal(t, "postgres", dbConfig.DBMS) + assert.Equal(t, "foo", dbConfig.DSN) + assert.False(t, dbConfig.TraceSQL) + + dbConfig = NewConfig("mysql", "bar", OptionTraceSQL).DB() + assert.Equal(t, "mysql", dbConfig.DBMS) + assert.Equal(t, "bar", dbConfig.DSN) + assert.True(t, dbConfig.TraceSQL) +} diff --git a/pkg/store/store.go b/pkg/store/store.go index 271ec4c..6b06e56 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -32,6 +32,10 @@ type Store struct { // Open a Store configured according to provided Config that will use the // provided Context for its transactions. func Open(ctx context.Context, cfg *Config) (*Store, error) { + if cfg == nil { + return nil, errors.New("nil config") + } + if err := cfg.Validate(); err != nil { return nil, err } @@ -84,8 +88,9 @@ func (o *Store) Migrate() error { // Signature validation of signed CoRIMs is not supported. If insecure // transactions are allowed by the Store's configuration, signed corims will // be added without validating their signatures. Otherwise, an error will be -// returned. -func (o *Store) AddBytes(buf []byte, label string) error { +// returned. If activate is true, the contained triples will be activated +// before they are added. +func (o *Store) AddBytes(buf []byte, label string, activate bool) error { if len(buf) < 3 { return fmt.Errorf("input too short") } @@ -105,7 +110,7 @@ func (o *Store) AddBytes(buf []byte, label string) error { return err } - return o.AddCoRIM(&signed.UnsignedCorim, digest, label) + return o.AddCoRIM(&signed.UnsignedCorim, digest, label, activate) } else if slices.Equal(buf[:3], []byte{0xd9, 0x01, 0xf5}) { // tag 501 -> unsigned corim var unsigned corim.UnsignedCorim @@ -113,7 +118,7 @@ func (o *Store) AddBytes(buf []byte, label string) error { return err } - return o.AddCoRIM(&unsigned, digest, label) + return o.AddCoRIM(&unsigned, digest, label, activate) } else { return fmt.Errorf("unrecognized input format") } @@ -121,8 +126,9 @@ func (o *Store) AddBytes(buf []byte, label string) error { // AddCoRIM adds the provided CoRIM to the store. The digest, if not nil, // should be the digest of the CBOR token the provided CoRIM was decoded -// from. -func (o *Store) AddCoRIM(c *corim.UnsignedCorim, digest []byte, label string) error { +// from. If activate is true, the contained triples will be activated +// before they are added. +func (o *Store) AddCoRIM(c *corim.UnsignedCorim, digest []byte, label string, activate bool) error { m, err := model.NewManifestFromCoRIM(c) if err != nil { return err @@ -131,6 +137,10 @@ func (o *Store) AddCoRIM(c *corim.UnsignedCorim, digest []byte, label string) er m.Digest = digest m.Label = label + if activate { + m.SetActive(true) + } + return o.AddManifest(m) } @@ -237,6 +247,18 @@ func (o *Store) DeleteManifest(manifestID string, label string) error { return manifest.Delete(o.Ctx, o.DB) } +// GetActiveValueTriples returns a slice of ValueTriple's whose environment +// matches the one provided. If exact is true, any unset fields in the provided +// environment must be NULL in the database; otherwise, unset fields will +// match any value. Only active triples are returned. +func (o *Store) GetActiveValueTriples( + env *model.Environment, + label string, + exact bool, +) ([]*model.ValueTriple, error) { + return getTriples[*model.ValueTriple](o, env, label, exact, true) +} + // GetValueTriples returns a slice of ValueTriple's whose environment matches // the one provided. If exact is true, any unset fields in the provided // environment must be NULL in the database; otherwise, unset fields will @@ -246,7 +268,19 @@ func (o *Store) GetValueTriples( label string, exact bool, ) ([]*model.ValueTriple, error) { - return getTriples[*model.ValueTriple](o, env, label, exact) + return getTriples[*model.ValueTriple](o, env, label, exact, false) +} + +// GetActiveKeyTriples returns a slice of KeyTriple's whose environment matches +// the one provided. If exact is true, any unset fields in the provided +// environment must be NULL in the database; otherwise, unset fields will +// match any value. Only active triples are returned. +func (o *Store) GetActiveKeyTriples( + env *model.Environment, + label string, + exact bool, +) ([]*model.KeyTriple, error) { + return getTriples[*model.KeyTriple](o, env, label, exact, true) } // GetKeyTriples returns a slice of KeyTriple's whose environment matches @@ -258,7 +292,7 @@ func (o *Store) GetKeyTriples( label string, exact bool, ) ([]*model.KeyTriple, error) { - return getTriples[*model.KeyTriple](o, env, label, exact) + return getTriples[*model.KeyTriple](o, env, label, exact, false) } func (o *Store) FindEnvironmentIDs(env *model.Environment, exact bool) ([]int64, error) { @@ -309,7 +343,10 @@ func (o *Store) FindModuleTagIDsForLabel(label string) ([]int64, error) { // comma-separated list. func (o *Store) StringAggregatorExpr(columnName string) string { dialect := o.DB.Dialect().Name().String() + return StringAggregatorExprForDialect(dialect, columnName) +} +func StringAggregatorExprForDialect(dialect, columnName string) string { switch dialect { case "pg": return fmt.Sprintf("STRING_AGG(%s, ', ')", columnName) @@ -327,12 +364,15 @@ func (o *Store) StringAggregatorExpr(columnName string) string { // ConcatExpr returns dialect-specific expression concatenated provided // strings. func (o *Store) ConcatExpr(tokens ...string) string { + dialect := o.DB.Dialect().Name().String() + return ConcatExprForDialect(dialect, tokens...) +} + +func ConcatExprForDialect(dialect string, tokens ...string) string { if len(tokens) == 0 { return "''" } - dialect := o.DB.Dialect().Name().String() - switch dialect { case "mysql": return fmt.Sprintf("CONCAT(%s)", strings.Join(tokens, ", ")) @@ -347,6 +387,10 @@ func (o *Store) ConcatExpr(tokens ...string) string { func (o *Store) HexExpr(columnName string) string { dialect := o.DB.Dialect().Name().String() + return HexExprForDialect(dialect, columnName) +} + +func HexExprForDialect(dialect, columnName string) string { switch dialect { case "mysql", "sqlite": @@ -393,21 +437,29 @@ func getTriples[T triple]( env *model.Environment, label string, exact bool, + activeOnly bool, ) ([]T, error) { // nolint:dupl - envIDs, err := store.FindEnvironmentIDs(env, exact) - if err != nil { - return nil, err - } - var ret []T query := store.DB.NewSelect().Model(&ret) - query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { - for _, envID := range envIDs { - q.WhereOr("environment_id = ?", envID) + + if activeOnly { + query.Where("is_active = true") + } + + if env != nil && !env.IsEmpty() { + envIDs, err := store.FindEnvironmentIDs(env, exact) + if err != nil { + return nil, err } - return q - }) + query.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { + for _, envID := range envIDs { + q.WhereOr("environment_id = ?", envID) + } + + return q + }) + } if label != "" { modIDs, err := store.FindModuleTagIDsForLabel(label) @@ -428,7 +480,7 @@ func getTriples[T triple]( if err := query.Scan(store.Ctx); err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, fmt.Errorf("orphaned evironment(s)?: %v", envIDs) + return nil, fmt.Errorf("no triples matched") } return nil, err diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go index dd901c5..f0f1820 100644 --- a/pkg/store/store_test.go +++ b/pkg/store/store_test.go @@ -10,6 +10,7 @@ import ( "github.com/veraison/corim-store/pkg/model" "github.com/veraison/corim-store/pkg/test" "github.com/veraison/corim/comid" + "github.com/veraison/corim/corim" ) func TestStore_roundtrip(t *testing.T) { @@ -26,7 +27,7 @@ func TestStore_roundtrip(t *testing.T) { bytes, err := os.ReadFile(path) require.NoError(t, err) - err = store.AddBytes(bytes, "cca") + err = store.AddBytes(bytes, "cca", false) require.NoError(t, err) } @@ -46,3 +47,372 @@ func TestStore_roundtrip(t *testing.T) { assert.NoError(t, err) assert.Len(t, keyTriples, 1) } + +func TestStore_Open(t *testing.T) { + testCases := []struct { + title string + cfg *Config + err string + }{ + { + title: "ok", + cfg: NewConfig("sqlite", "file::memory:?cache=shared"), + }, + { + title: "bad DSN", + cfg: NewConfig("mysql", "foo"), + err: "invalid DSN: missing the slash separating the database name", + }, + { + title: "nil config", + err: "nil config", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + _, err := Open(context.Background(), tc.cfg) + + if tc.err == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + }) + } +} + +func TestStore_initialization(t *testing.T) { + testStore, err := Open(context.Background(), NewConfig("sqlite", "file::memory:?cache=shared")) + require.NoError(t, err) + + _, err = testStore.DB.DB.Exec("select * from measurements") + assert.ErrorContains(t, err, "no such table") + + err = testStore.Init() + require.NoError(t, err) + + err = testStore.Migrate() + require.NoError(t, err) + + _, err = testStore.DB.DB.Exec("select * from measurements") + assert.NoError(t, err) +} + +func TestStore_AddBytes(t *testing.T) { + testCases := []struct { + title string + path string + bytes []byte + opts []ConfigOption + label string + activate bool + err string + }{ + { + title: "ok unsigned/no label/no activate", + path: "../../sample/corim/unsigned-cca-ta.cbor", + }, + { + title: "ok unsigned/label/activate", + path: "../../sample/corim/unsigned-cca-ta.cbor", + label: "foo", + activate: true, + }, + { + title: "ok signed", + path: "../../sample/corim/signed-cca-ta.cose", + opts: []ConfigOption{OptionInsecure}, + }, + { + title: "nok signed without insecure", + path: "../../sample/corim/signed-cca-ta.cose", + err: "signed CoRIM validation not supported", + }, + { + title: "nok input too short", + bytes: []byte{0x01, 0x02}, + err: "input too short", + }, + { + title: "nok unrecognized input format", + bytes: []byte{0x01, 0x02, 0x03, 0x04}, + err: "unrecognized input format", + }, + { + title: "nok bad unsigned", + bytes: []byte{0xd9, 0x01, 0xf5, 0x01}, + err: "found Major Type 0", + }, + { + title: "nok bad signed", + bytes: []byte{0xd2, 0x01, 0x02, 0x03}, + opts: []ConfigOption{OptionInsecure}, + err: "failed CBOR decoding for COSE-Sign1", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + var err error + var bytes []byte + + if tc.path != "" { + bytes, err = os.ReadFile(tc.path) + } else { + bytes = tc.bytes + } + require.NoError(t, err) + + cfg := NewConfig("sqlite", "file::memory:", tc.opts...) + store, err := Open(context.Background(), cfg) + require.NoError(t, err) + require.NoError(t, store.Init()) + require.NoError(t, store.Migrate()) + + err = store.AddBytes(bytes, tc.label, tc.activate) + + if tc.err == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.err) + } + + require.NoError(t, store.Close()) + }) + } +} + +func TestStore_Manifest_CRUD(t *testing.T) { + bytes, err := os.ReadFile("../../sample/corim/unsigned-cca-ta.cbor") + require.NoError(t, err) + + var unsigned corim.UnsignedCorim + err = unsigned.FromCBOR(bytes) + require.NoError(t, err) + + manifest, err := model.NewManifestFromCoRIM(&unsigned) + require.NoError(t, err) + manifest.Digest = []byte{0xde, 0xad, 0xbe, 0xef} + manifest.Label = "label" + + store, err := OpenWithDB(context.Background(), test.NewTestDB(t)) + require.NoError(t, err) + + err = store.AddManifest(manifest) + assert.NoError(t, err) + + otherManifest, err := store.GetManifest(manifest.ManifestID, "label") + assert.NoError(t, err) + + // note: we cannot simply compare ModuleTag's for equality be cause the + // original is not going to have database-internal files (e.g. ID, + // OwnerID, etc) set. + + modOrig := manifest.ModuleTags[0] + modDB := otherManifest.ModuleTags[0] + assert.Equal(t, modOrig.TagIDType, modDB.TagIDType) + assert.Equal(t, modOrig.TagID, modDB.TagID) + assert.Equal(t, modOrig.TagID, modDB.TagID) + + envOrig := modOrig.KeyTriples[0].Environment + envDB := modDB.KeyTriples[0].Environment + assert.Equal(t, envOrig.ClassType, envDB.ClassType) + assert.Equal(t, envOrig.ClassBytes, envDB.ClassBytes) + assert.Equal(t, envOrig.Vendor, envDB.Vendor) + assert.Equal(t, envOrig.Model, envDB.Model) + assert.Equal(t, envOrig.Layer, envDB.Layer) + assert.Equal(t, envOrig.Index, envDB.Index) + assert.Equal(t, envOrig.InstanceType, envDB.InstanceType) + assert.Equal(t, envOrig.InstanceBytes, envDB.InstanceBytes) + assert.Equal(t, envOrig.GroupType, envDB.GroupType) + assert.Equal(t, envOrig.GroupBytes, envDB.GroupBytes) + + _, err = store.GetActiveKeyTriples(envOrig, "", false) + assert.NoError(t, err) + + _, err = store.GetActiveKeyTriples(envOrig, "label", false) + assert.NoError(t, err) + + _, err = store.GetActiveValueTriples(envOrig, "label", false) + assert.NoError(t, err) + + err = store.AddManifest(otherManifest) + assert.ErrorContains(t, err, "already in store (digests match)") + + otherManifest.Digest = []byte{0x01, 0x02, 0x03, 0x04} + err = store.AddManifest(otherManifest) + assert.ErrorContains(t, err, "already in store but digests differ") + + store.cfg.Force = true + err = store.AddManifest(otherManifest) + assert.NoError(t, err) + + store.cfg.RequireLabel = true + _, err = store.GetManifest(manifest.ManifestID, "") + assert.ErrorContains(t, err, "a label must be specified") + + err = store.DeleteManifest(manifest.ManifestID, "label") + assert.NoError(t, err) + + err = store.DeleteManifest(manifest.ManifestID, "label") + assert.ErrorContains(t, err, "manifest with ID \"cca-ta\" not found") +} + +func TestStore_Find_bad(t *testing.T) { + store, err := OpenWithDB(context.Background(), test.NewTestDB(t)) + require.NoError(t, err) + + testLayer := uint64(1) + lookupEnv := model.Environment{Layer: &testLayer} + _, err = store.FindEnvironmentIDs(&lookupEnv, false) + assert.ErrorContains(t, err, "no matching environments found") + + _, err = store.FindModuleTagIDsForLabel("") + assert.ErrorContains(t, err, "no label specified") +} + +func TestStore_StringAggregatorExpr(t *testing.T) { + store, err := OpenWithDB(context.Background(), test.NewTestDB(t)) + require.NoError(t, err) + + ret := store.StringAggregatorExpr("foo") + assert.Equal(t, "GROUP_CONCAT(foo, ', ')", ret) + + testCases := []struct { + title string + expected string + }{ + { + title: "mysql", + expected: "GROUP_CONCAT(foo SEPARATOR ', ')", + }, + { + title: "pg", + expected: "STRING_AGG(foo, ', ')", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + ret := StringAggregatorExprForDialect(tc.title, "foo") + assert.Equal(t, tc.expected, ret) + }) + } + + assert.Panics(t, func() { StringAggregatorExprForDialect("foo", "bar") }) +} + +func TestStore_ConcatExpr(t *testing.T) { + store, err := OpenWithDB(context.Background(), test.NewTestDB(t)) + require.NoError(t, err) + + ret := store.ConcatExpr("foo", "bar") + assert.Equal(t, "foo || bar", ret) + + testCases := []struct { + title string + expected string + }{ + { + title: "mysql", + expected: "CONCAT(foo, bar)", + }, + { + title: "pg", + expected: "foo || bar", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + ret := ConcatExprForDialect(tc.title, "foo", "bar") + assert.Equal(t, tc.expected, ret) + }) + } + + assert.Panics(t, func() { ConcatExprForDialect("foo", "bar", "qux") }) +} + +func TestStore_HexExpr(t *testing.T) { + store, err := OpenWithDB(context.Background(), test.NewTestDB(t)) + require.NoError(t, err) + + ret := store.HexExpr("foo") + assert.Equal(t, "hex(foo)", ret) + + testCases := []struct { + title string + expected string + }{ + { + title: "mysql", + expected: "hex(foo)", + }, + { + title: "pg", + expected: "encode(foo, 'hex')", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + ret := HexExprForDialect(tc.title, "foo") + assert.Equal(t, tc.expected, ret) + }) + } + + assert.Panics(t, func() { HexExprForDialect("foo", "bar") }) +} + +func TestStore_Digest(t *testing.T) { + test_input := []byte{0xde, 0xad, 0xbe, 0xef} + testCases := []struct { + title string + expected []byte + }{ + { + title: "md5", + expected: []byte{ + 0x2f, 0x24, 0x92, 0x30, 0xa8, 0xe7, 0xc2, 0xbf, + 0x60, 0x05, 0xcc, 0xd2, 0x67, 0x92, 0x59, 0xec, + }, + }, + { + title: "sha256", + expected: []byte{ + 0x5f, 0x78, 0xc3, 0x32, 0x74, 0xe4, 0x3f, 0xa9, + 0xde, 0x56, 0x59, 0x26, 0x5c, 0x1d, 0x91, 0x7e, + 0x25, 0xc0, 0x37, 0x22, 0xdc, 0xb0, 0xb8, 0xd2, + 0x7d, 0xb8, 0xd5, 0xfe, 0xaa, 0x81, 0x39, 0x53, + }, + }, + { + title: "sha512", + expected: []byte{ + 0x12, 0x84, 0xb2, 0xd5, 0x21, 0x53, 0x51, 0x96, + 0xf2, 0x21, 0x75, 0xd5, 0xf5, 0x58, 0x10, 0x42, + 0x20, 0xa6, 0xad, 0x76, 0x80, 0xe7, 0x8b, 0x49, + 0xfa, 0x6f, 0x20, 0xe5, 0x7e, 0xa7, 0xb1, 0x85, + 0xd7, 0x1e, 0xc1, 0xed, 0xb1, 0x37, 0xe7, 0x0e, + 0xba, 0x52, 0x8d, 0xed, 0xb1, 0x41, 0xf5, 0xd2, + 0xf8, 0xbb, 0x53, 0x14, 0x9d, 0x26, 0x29, 0x32, + 0xb2, 0x7c, 0xf4, 0x1f, 0xed, 0x96, 0xaa, 0x7f, + }, + }, + } + + store, err := OpenWithDB(context.Background(), test.NewTestDB(t)) + require.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + store.cfg.HashAlg = tc.title + ret := store.Digest(test_input) + assert.Equal(t, tc.expected, ret) + }) + } + + store.cfg.HashAlg = "foo" + assert.Panics(t, func() { store.Digest(test_input) }) +} diff --git a/pkg/test/test_helpers.go b/pkg/test/test_helpers.go index fce8071..b2e17a3 100644 --- a/pkg/test/test_helpers.go +++ b/pkg/test/test_helpers.go @@ -30,7 +30,7 @@ func NewTestDB(t *testing.T) *bun.DB { testDB, err := db.Open(&db.Config{ DBMS: "sqlite", - DSN: fmt.Sprintf("file:%s?cache=shared", testDbFile), + DSN: fmt.Sprintf("file:%s", testDbFile), TraceSQL: trace, }) require.NoError(t, err) diff --git a/sample/config/mariadb.yaml b/sample/config/mariadb.yaml new file mode 100644 index 0000000..44c0fc0 --- /dev/null +++ b/sample/config/mariadb.yaml @@ -0,0 +1,2 @@ +dbms: mariadb +dsn: store_user:L3tM31n@tcp(localhost:33306)/corim_store?parseTime=true diff --git a/sample/config/postgres.yaml b/sample/config/postgres.yaml new file mode 100644 index 0000000..05408f7 --- /dev/null +++ b/sample/config/postgres.yaml @@ -0,0 +1,2 @@ +dbms: postgres +dsn: postgres://store_user:L3tM31n@localhost:55432/corim_store?sslmode=disable diff --git a/sample/config/sqlite3.yaml b/sample/config/sqlite3.yaml new file mode 100644 index 0000000..a4d3d7c --- /dev/null +++ b/sample/config/sqlite3.yaml @@ -0,0 +1,2 @@ +dbms: sqlite +dsn: file:/tmp/test-corim-store-db.sqlite?cache=shared diff --git a/sample/corim/key.priv.jwk b/sample/corim/key.priv.jwk new file mode 100644 index 0000000..65bd5ac --- /dev/null +++ b/sample/corim/key.priv.jwk @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "P-256", + "x": "_gPssLIiLnF0XrTGU73XMKlTIk4QhU80ttXzJ7waTpo", + "y": "HgibD8RtoczLlJBzDi62cTacMR9NOL8mh6RfU2E3lwk", + "d": "ZxfIqWVgn8uXSNQj0t8r_u6iS8WJuKxbmUzwNlpE760" +} diff --git a/sample/corim/key.priv.pem b/sample/corim/key.priv.pem new file mode 100644 index 0000000..a2713b3 --- /dev/null +++ b/sample/corim/key.priv.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIGcXyKllYJ/Ll0jUI9LfK/7uokvFibisW5lM8DZaRO+toAoGCCqGSM49 +AwEHoUQDQgAE/gPssLIiLnF0XrTGU73XMKlTIk4QhU80ttXzJ7waTpoeCJsPxG2h +zMuUkHMOLrZxNpwxH004vyaHpF9TYTeXCQ== +-----END EC PRIVATE KEY----- diff --git a/sample/corim/key.pub.jwk b/sample/corim/key.pub.jwk new file mode 100644 index 0000000..4b700cd --- /dev/null +++ b/sample/corim/key.pub.jwk @@ -0,0 +1,6 @@ +{ + "kty": "EC", + "crv": "P-256", + "x": "_gPssLIiLnF0XrTGU73XMKlTIk4QhU80ttXzJ7waTpo", + "y": "HgibD8RtoczLlJBzDi62cTacMR9NOL8mh6RfU2E3lwk", +} diff --git a/sample/corim/key.pub.pem b/sample/corim/key.pub.pem new file mode 100644 index 0000000..3fb4852 --- /dev/null +++ b/sample/corim/key.pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/gPssLIiLnF0XrTGU73XMKlTIk4Q +hU80ttXzJ7waTpoeCJsPxG2hzMuUkHMOLrZxNpwxH004vyaHpF9TYTeXCQ== +-----END PUBLIC KEY----- diff --git a/sample/corim/signed-cca-ref-plat.cose b/sample/corim/signed-cca-ref-plat.cose new file mode 100644 index 0000000..8e13fbb Binary files /dev/null and b/sample/corim/signed-cca-ref-plat.cose differ diff --git a/sample/corim/signed-cca-ref-realm.cose b/sample/corim/signed-cca-ref-realm.cose new file mode 100644 index 0000000..8630295 Binary files /dev/null and b/sample/corim/signed-cca-ref-realm.cose differ diff --git a/sample/corim/signed-cca-ta.cose b/sample/corim/signed-cca-ta.cose new file mode 100644 index 0000000..ab52b45 Binary files /dev/null and b/sample/corim/signed-cca-ta.cose differ diff --git a/scripts/compile-sample-corims.sh b/scripts/compile-sample-corims.sh index 8aaa30b..16cf4c6 100755 --- a/scripts/compile-sample-corims.sh +++ b/scripts/compile-sample-corims.sh @@ -15,8 +15,12 @@ SAMPLE_DIR=${THIS_DIR}/../sample/corim for file in "${SAMPLE_DIR}"/*.json; do outfile=$(basename "$file") outfile=${outfile//corim-/unsigned-} + signed_outfile=${outfile//unsigned-/signed-} outfile=${SAMPLE_DIR}/${outfile%.json}.cbor + signed_outfile=${SAMPLE_DIR}/${signed_outfile%.json}.cose + key="${SAMPLE_DIR}"/key.priv.pem echo "Compiling ${outfile}..." ${CORIM_TOOL} compile --force "${file}" --output "${outfile}" + ${CORIM_TOOL} compile --force "${file}" --key "${key}" --output "${signed_outfile}" done diff --git a/scripts/db-container.sh b/scripts/db-container.sh new file mode 100755 index 0000000..915d8ce --- /dev/null +++ b/scripts/db-container.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +set -ueo pipefail + +TEST_DB_CONTAINER_NAME=${TEST_DB_CONTAINER_NAME:-corim-store-db} +TEST_DB_MYSQL_PORT=${TEST_DB_MYSQL_PORT:-33306} +TEST_DB_POSTGRES_PORT=${TEST_DB_POSTGRES_PORT:-55432} +TEST_DB_LOG_FILE=${TEST_DB_LOG_FILE:-/tmp/corim-store-db-output.log} + +error='\e[0;31mERROR\e[0m' +this_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +root_dir=$this_dir/.. +mysql_args="--ssl=0 -h 127.0.0.1 -P $TEST_DB_MYSQL_PORT -u store_user --password=L3tM31n corim_store" +psql_args="-U store_user -h localhost -p 55432 -d corim_store" + +function check_image() { + if [[ "$(docker images -q "$TEST_DB_CONTAINER_NAME")" == "" ]]; then + exit 1 + fi +} + +function build() { + if [[ "$(type -p docker)" == "" ]]; then + echo -e "$error: docker must be installed." + exit 1 + fi + + if ! (docker buildx version &>/dev/null); then + echo -e "$error: docker-buildx Docker plugin must be installed." + exit 1 + fi + + pushd "$root_dir/docker" &>/dev/null + trap 'popd &>/dev/null' RETURN + + echo "building $TEST_DB_CONTAINER_NAME..." + docker build . -t "$TEST_DB_CONTAINER_NAME" + echo "done." +} + +function run() { + if [[ "$(docker images -q "$TEST_DB_CONTAINER_NAME")" == "" ]]; then + echo -e "$error: container $TEST_DB_CONTAINER_NAME does not exist " \ + "(run db-container.sh build first)." + exit 1 + fi + + echo "running $TEST_DB_CONTAINER_NAME..." + echo "----------------------------------------------------" >>"$TEST_DB_LOG_FILE" + nohup docker run -p "${TEST_DB_MYSQL_PORT}":3306 -p "${TEST_DB_POSTGRES_PORT}":5432 \ + "$TEST_DB_CONTAINER_NAME" &>>"$TEST_DB_LOG_FILE" & + + if [[ "$(type -p mariadb)" != "" ]]; then + sleep 2 + count=0 + # shellcheck disable=SC2086 + until mariadb $mysql_args -e "SELECT 1" &>/dev/null; do + sleep 1 + ((count += 1)) + + if [[ $count -gt 20 ]]; then + echo -e "$error: timed out waiting for MariaDB server." + exit 1 + fi + done + else + # don't have MariaDB client not installed on the host, so just use a constant delay + # to give the server time to start. + sleep 10 + fi + + echo "done." +} + +function stop() { + echo "stopping $TEST_DB_CONTAINER_NAME..." + for cid in $(docker ps -q --filter "ancestor=$TEST_DB_CONTAINER_NAME"); do + docker stop "$cid" + done + echo "done." +} + +function shell() { + for cid in $(docker ps -q --filter "ancestor=$TEST_DB_CONTAINER_NAME"); do + docker exec -it "$cid" /bin/bash + break + done +} + +function mariadb_shell() { + if [[ "$(type -p mariadb)" == "" ]]; then + echo -e "$error: MariaDB client (mariadb) must be installed." + exit 1 + fi + + echo "mariadb $mysql_args" + # shellcheck disable=SC2086 + mariadb $mysql_args +} + +function psql_shell() { + if [[ "$(type -p psql)" == "" ]]; then + echo -e "$error: PosogreSQL client (psql) must be installed." + exit 1 + fi + + export PGPASSWORD='L3tM31n' + echo "PGPASSWORD=$PGPASSWORD psql $psql_args" + # shellcheck disable=SC2086 + psql $psql_args +} + +function help() { + set +e + local usage + read -r -d '' usage <<-EOF + Usage: db-container.sh COMMAND + + Commands: + + help + Print this message and exist (the same as -h). + + check-image + Check whether the Docker container image has been created. Non-zero exit code + indicates that it was not. + + build + Build Docker image that runs MariaDB and PosogreSQL servers with appropriate + database and users created. + + run | start + Run a Docker container built using the build command (see above). + + stop + Stop the container started with run/start command. + + shell + Start a (root) shell inside the container. + + mariadb | mysql + Start an interactive session shell on the MariaDB server running inside the container. + + postgres | psql + Start an interactive session shell on the PostgreSQL server running inside the container. + + Environment Variables: + + A number of environment variables control the Docker image/container. Please make sure + the values of these variables remain consisten across command invocations. + + TEST_DB_CONTAINER_NAME + The name of the Docker container image name that will be built. (Defaults to + corim-store-db). + + TEST_DB_MYSQL_PORT + The host post that the MariaDB servier will be listening on. (Defaults to 33306). + + TEST_DB_POSTGRES_PORT + The host post that the PostgreSQL servier will be listening on. (Defaults to 55432). + + TEST_DB_LOG_FILE + The file that the output from the running container will be logged to (Defaults to + /tmp/corim-store-db-output.log). + + EOF + set -e + + echo "$usage" +} + +while getopts "h" opt; do + case "$opt" in + h) help; exit 0;; + *) break;; + esac +done + +shift $((OPTIND-1)) +[ "${1:-}" = "--" ] && shift + +if [[ "${1:-}" == "" ]]; then + echo -e "$error: a command must be specified (see -h output)." + exit 1 +fi + +command=${1:-}; shift +command=$(echo "$command" | tr -- _ -) +case $command in + help) help;; + check-image) check_image;; + build) build;; + run | start) run;; + mariadb | mysql) mariadb_shell;; + postgres | psql) psql_shell;; + shell) shell;; + stop) stop;; + *) echo -e "$error: unexpected command: \"$command\"";; +esac diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh new file mode 100755 index 0000000..4e1c0f5 --- /dev/null +++ b/scripts/integration-tests.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +error='\e[0;31mERROR\e[0m' +this_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +root_dir=$this_dir/.. +corim_store=$root_dir/corim-store + +function check_installed() { + what=$1 + + if [[ "$(type -p "$what")" == "" ]]; then + echo -e "$error: $what must be installed." + exit 1 + fi +} + +function check_requirements() { + check_installed docker + check_installed jq +} + +function setup() { + echo "updating submods..." + git submodule update --init + + echo "building corim-store..." + make -s build + + if ! "$root_dir"/scripts/db-container.sh check-image; then + echo "building DB container..." + "$root_dir"/scripts/db-container.sh build >/dev/null + fi + + echo "starting DB container..." + "$root_dir"/scripts/db-container.sh start >/dev/null + + echo "initializing store..." + $corim_store --config "$root_dir"/sample/config/sqlite3.yaml db init + $corim_store --config "$root_dir"/sample/config/mariadb.yaml db init + $corim_store --config "$root_dir"/sample/config/postgres.yaml db init +} + +function teardown() { + echo "stopping DB container..." + "$root_dir"/scripts/db-container.sh stop >/dev/null + + echo "done." +} + +function run() { + "$root_dir"/test/bats/bin/bats test +} + +function help() { + set +e + local usage + read -r -d '' usage <<-EOF + Usage: db-container.sh COMMAND + + Commands: + + help + Print this message and exist (the same as -h). + + setup + Set up integration tests infrastructure. + + run + Run integration tests. + + teardown + Tear down integartion tests infrastructure. + EOF + set -e + + echo "$usage" +} + +check_requirements + +while getopts "h" opt; do + case "$opt" in + h) help; exit 0;; + *) break;; + esac +done + +shift $((OPTIND-1)) +[ "${1:-}" = "--" ] && shift + +if [[ "${1:-}" == "" ]]; then + echo -e "$error: a command must be specified (see -h output)." + exit 1 +fi + +command=${1:-}; shift +command=$(echo "$command" | tr -- _ -) +case $command in + help) help;; + setup) setup;; + run ) run;; + teardown) teardown;; + *) echo -e "$error: unexpected command: \"$command\"";; +esac diff --git a/test/bats b/test/bats new file mode 160000 index 0000000..33ae886 --- /dev/null +++ b/test/bats @@ -0,0 +1 @@ +Subproject commit 33ae8862cd058e004f9f04e1f9ad6197763a1cb2 diff --git a/test/test.bats b/test/test.bats new file mode 100644 index 0000000..68337db --- /dev/null +++ b/test/test.bats @@ -0,0 +1,47 @@ +function setup_file() { + this_dir=$( cd -- "$( dirname -- "${BATS_TEST_FILENAME}" )" &> /dev/null && pwd ) + ROOT_DIR=$this_dir/.. + CORIM_STORE=$ROOT_DIR/corim-store + + export ROOT_DIR + export CORIM_STORE +} + +function setup() { + load 'test_helper/bats-support/load' + load 'test_helper/bats-assert/load' + + $CORIM_STORE --config "$ROOT_DIR"/sample/config/mariadb.yaml db migrate + $CORIM_STORE --config "$ROOT_DIR"/sample/config/postgres.yaml db migrate +} + +function teardown() { + $CORIM_STORE --config "$ROOT_DIR"/sample/config/mariadb.yaml db rollback + $CORIM_STORE --config "$ROOT_DIR"/sample/config/postgres.yaml db rollback +} + + +function do_get() { + config_path=$1 + + $CORIM_STORE --config "$config_path" corim add "$ROOT_DIR"/sample/corim/*cbor &>/dev/null + $CORIM_STORE --config "$config_path" get --class-id 2QJYWCB/RUxGAgEBAAAAAAAAAAAAAwA+AAEAAABQWAAAAAAAAA== +} + +@test "SQLite3 get" { + run bats_pipe \ + do_get "$ROOT_DIR"/sample/config/sqlite3.yaml \| jq ".\"reference-values\" | length" + assert_output "1" +} + +@test "MariaDB get" { + run bats_pipe \ + do_get "$ROOT_DIR"/sample/config/mariadb.yaml \| jq ".\"reference-values\" | length" + assert_output "1" +} + +@test "PostgreSQL get" { + run bats_pipe \ + do_get "$ROOT_DIR"/sample/config/postgres.yaml \| jq ".\"reference-values\" | length" + assert_output "1" +} diff --git a/test/test_helper/bats-assert b/test/test_helper/bats-assert new file mode 160000 index 0000000..697471b --- /dev/null +++ b/test/test_helper/bats-assert @@ -0,0 +1 @@ +Subproject commit 697471b7a89d3ab38571f38c6c7c4b460d1f5e35 diff --git a/test/test_helper/bats-support b/test/test_helper/bats-support new file mode 160000 index 0000000..0954abb --- /dev/null +++ b/test/test_helper/bats-support @@ -0,0 +1 @@ +Subproject commit 0954abb9925cad550424cebca2b99255d4eabe96