From cf7c6f58dec38f64df0e55aa334c87c8eec7ec36 Mon Sep 17 00:00:00 2001 From: Priyanshu Thapliyal Date: Sun, 19 Jan 2025 22:59:29 +0530 Subject: [PATCH] Add CoSWID command and display functionality with CBOR support Signed-off-by: Priyanshu Thapliyal --- cmd/coswid.go | 19 +++ cmd/coswidCreate.go | 146 +++++++++++++++++++++ cmd/coswidDisplay.go | 134 +++++++++++++++++++ cmd/coswidValidate.go | 208 ++++++++++++++++++++++++++++++ data/coswid/coswid-example.cbor | Bin 0 -> 115 bytes data/coswid/coswid-full.cbor | Bin 0 -> 235 bytes data/coswid/coswid-meta-full.cbor | Bin 0 -> 126 bytes data/coswid/coswid-meta-mini.cbor | Bin 0 -> 126 bytes go.mod | 5 +- go.sum | 6 + 10 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 cmd/coswid.go create mode 100644 cmd/coswidCreate.go create mode 100644 cmd/coswidDisplay.go create mode 100644 cmd/coswidValidate.go create mode 100644 data/coswid/coswid-example.cbor create mode 100644 data/coswid/coswid-full.cbor create mode 100644 data/coswid/coswid-meta-full.cbor create mode 100644 data/coswid/coswid-meta-mini.cbor diff --git a/cmd/coswid.go b/cmd/coswid.go new file mode 100644 index 0000000..0aeffab --- /dev/null +++ b/cmd/coswid.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "fmt" +) + +var coswidCmd = &cobra.Command{ + Use: "coswid", + Short: "A brief description of your command", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("coswid command executed") + return nil + }, +} + +func init() { + rootCmd.AddCommand(coswidCmd) +} \ No newline at end of file diff --git a/cmd/coswidCreate.go b/cmd/coswidCreate.go new file mode 100644 index 0000000..cc02c8c --- /dev/null +++ b/cmd/coswidCreate.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/xeipuuv/gojsonschema" + "github.com/fxamacker/cbor/v2" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/veraison/swid" +) + +type CustomSoftwareIdentity struct { + swid.SoftwareIdentity + Evidence struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"evidence"` +} + +var ( + coswidCreateTemplate string + coswidCreateOutputDir string +) + +var coswidCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a CBOR-encoded CoSWID from the supplied JSON template", + Long: `Create a CBOR-encoded CoSWID from the supplied JSON template. + +Create a CoSWID from template t1.json and save it to the current directory. + + cocli coswid create --template=t1.json + +Create a CoSWID from template t1.json and save it to the specified directory. +`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkCoswidCreateArgs(); err != nil { + return err + } + + // Validate JSON against schema before processing + schemaPath := "D:/opensource/cocli/data/coswid/templates/coswid-schema.json" + err := validateJSON(coswidCreateTemplate, schemaPath) + if err != nil { + return fmt.Errorf("JSON validation failed: %v", err) + } + + cborFile, err := coswidTemplateToCBOR(coswidCreateTemplate, coswidCreateOutputDir) + if err != nil { + return fmt.Errorf("error creating CBOR: %v", err) + } + fmt.Printf(">> created %q from %q\n", cborFile, coswidCreateTemplate) + + return nil + }, +} + +func checkCoswidCreateArgs() error { + if coswidCreateTemplate == "" { + return fmt.Errorf("template file is required") + } + return nil +} + +func coswidTemplateToCBOR(tmplFile, outputDir string) (string, error) { + var ( + tmplData []byte + coswidCBOR []byte + s CustomSoftwareIdentity + coswidFile string + err error + ) + + // Read the template file + tmplData, err = afero.ReadFile(afero.NewOsFs(), tmplFile) + if err != nil { + return "", fmt.Errorf("unable to read template file: %v", err) + } + + // Parse the JSON into the custom struct + err = json.Unmarshal(tmplData, &s) + if err != nil { + return "", fmt.Errorf("error decoding template from %s: %v", tmplFile, err) + } + + // Debugging: Print the parsed CustomSoftwareIdentity object + fmt.Println("Decoded CustomSoftwareIdentity object:") + fmt.Printf("%+v\n", s) + + // Encode the struct to CBOR using fxamacker/cbor + coswidCBOR, err = cbor.Marshal(s) + if err != nil { + return "", fmt.Errorf("error encoding to CBOR: %v", err) + } + + // Generate the output file name + coswidFile = makeFileName(outputDir, tmplFile, ".cbor") + + // Write the CBOR data to the output file + err = afero.WriteFile(afero.NewOsFs(), coswidFile, coswidCBOR, 0644) + if err != nil { + return "", fmt.Errorf("error writing CBOR file: %v", err) + } + + return coswidFile, nil +} + + +// validateJSON validates the JSON template against the provided schema +func validateJSON(tmplFile, schemaFile string) error { + schemaLoader := gojsonschema.NewReferenceLoader("file://" + filepath.ToSlash(schemaFile)) + documentLoader := gojsonschema.NewReferenceLoader("file://" + filepath.ToSlash(tmplFile)) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("error during JSON validation: %v", err) + } + + if !result.Valid() { + for _, desc := range result.Errors() { + fmt.Printf("- %s\n", desc) + } + return fmt.Errorf("schema validation failed: JSON does not conform to schema") + } + + fmt.Println("JSON validation successful.") + return nil +} + +func init() { + coswidCmd.AddCommand(coswidCreateCmd) + coswidCreateCmd.Flags().StringVarP(&coswidCreateTemplate, "template", "t", "", "a CoSWID template file (in JSON format)") + coswidCreateCmd.Flags().StringVarP(&coswidCreateOutputDir, "output-dir", "o", ".", "output directory for CBOR file") + + // Handle required flag errors + if err := coswidCreateCmd.MarkFlagRequired("template"); err != nil { + // Since we're in init(), we can only panic on critical errors + panic(fmt.Sprintf("Failed to mark 'template' flag as required: %v", err)) + } + if err := coswidCreateCmd.MarkFlagRequired("output-dir"); err != nil { + panic(fmt.Sprintf("Failed to mark 'output-dir' flag as required: %v", err)) + } +} \ No newline at end of file diff --git a/cmd/coswidDisplay.go b/cmd/coswidDisplay.go new file mode 100644 index 0000000..d88fbde --- /dev/null +++ b/cmd/coswidDisplay.go @@ -0,0 +1,134 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "encoding/json" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/veraison/swid" +) + +var ( + coswidDisplayFile string + coswidDisplayDir string +) + +var coswidDisplayCmd = &cobra.Command{ + Use: "display", + Short: "Display one or more CBOR-encoded CoSWID(s) in human-readable (JSON) format", + Long: `Display one or more CBOR-encoded CoSWID(s) in human-readable (JSON) format. +You can supply individual CoSWID files or directories containing CoSWID files. + +Display CoSWID in file s.cbor. + + cocli coswid display --file=s.cbor + +Display CoSWIDs in files s1.cbor, s2.cbor and any cbor file in the coswids/ directory. + + cocli coswid display --file=s1.cbor --file=s2.cbor --dir=coswids +`, + RunE: func(cmd *cobra.Command, args []string) error { + // Validate input arguments + if err := checkCoswidDisplayArgs(); err != nil { + return err + } + + filesList := gatherFiles([]string{coswidDisplayFile}, []string{coswidDisplayDir}, ".cbor") + if len(filesList) == 0 { + return fmt.Errorf("no CoSWID files found") + } + + for _, file := range filesList { + if err := displayCoswid(file); err != nil { + fmt.Printf("Error displaying %s: %v\n", file, err) + } + } + + return nil + }, +} + +func checkCoswidDisplayArgs() error { + if coswidDisplayFile == "" && coswidDisplayDir == "" { + return fmt.Errorf("no CoSWID file or directory supplied") + } + return nil +} + +func gatherFiles(files []string, dirs []string, ext string) []string { + collectedMap := make(map[string]struct{}) + var collected []string + var walkErr error + + // Collect files from specified file paths + for _, file := range files { + if filepath.Ext(file) == ext { + collectedMap[file] = struct{}{} + } + } + + // Collect files from specified directories + for _, dir := range dirs { + if dir != "" { + exists, err := afero.Exists(fs, dir) + if err == nil && exists { + walkErr = afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error accessing path %s: %v", path, err) + } + if !info.IsDir() && filepath.Ext(path) == ext { + collectedMap[path] = struct{}{} + } + return nil + }) + if walkErr != nil { + fmt.Printf("Warning: error walking directory %s: %v\n", dir, walkErr) + } + } + } + } + + // Convert map keys to slice + for file := range collectedMap { + collected = append(collected, file) + } + + return collected +} + +func displayCoswid(file string) error { + fmt.Printf("Processing file: %s\n", file) + var ( + coswidCBOR []byte + s swid.SoftwareIdentity + err error + ) + + // Read the CBOR file + if coswidCBOR, err = afero.ReadFile(fs, file); err != nil { + return fmt.Errorf("error reading file %s: %w", file, err) + } + + // Decode CBOR to SoftwareIdentity + if err = s.FromCBOR(coswidCBOR); err != nil { + return fmt.Errorf("error decoding CoSWID from %s: %w", file, err) + } + + // Convert to JSON + coswidJSON, err := json.MarshalIndent(&s, "", " ") + if err != nil { + return fmt.Errorf("error marshaling CoSWID to JSON: %w", err) + } + + fmt.Printf(">> [%s]\n%s\n", file, string(coswidJSON)) + return nil +} + +func init() { + coswidCmd.AddCommand(coswidDisplayCmd) + coswidDisplayCmd.Flags().StringVarP(&coswidDisplayFile, "file", "f", "", "a CoSWID file (in CBOR format)") + coswidDisplayCmd.Flags().StringVarP(&coswidDisplayDir, "dir", "d", "", "a directory containing CoSWID files") +} \ No newline at end of file diff --git a/cmd/coswidValidate.go b/cmd/coswidValidate.go new file mode 100644 index 0000000..0bd5de9 --- /dev/null +++ b/cmd/coswidValidate.go @@ -0,0 +1,208 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "encoding/base64" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/xeipuuv/gojsonschema" + "github.com/fxamacker/cbor/v2" // Added CBOR package +) + +var ( + coswidValidateFile string + coswidValidateSchema string +) + +var coswidKeyMap = map[uint64]string{ + 0: "schema-version", + 1: "tag-id", + 2: "software-name", + 3: "tag-version", + 4: "patch-level", + 5: "version", + 6: "version-scheme", + 7: "lang", + 8: "directory", + 9: "file", + 10: "process", + 11: "resource", + 12: "size", + 13: "file-version", + 14: "entity", + 15: "evidence", + 16: "link", + 17: "payload", + 18: "hash", + 19: "hash-alg-id", + 20: "hash-value", +} + +var coswidValidateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate a CBOR-encoded CoSWID against the provided JSON schema", + Long: `Validate a CBOR-encoded CoSWID against the provided JSON schema + + Validate the CoSWID in file s.cbor against the schema schema.json. + + cocli coswid validate --file=s.cbor --schema=schema.json + `, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkCoswidValidateArgs(); err != nil { + return err + } + + if err := validateCoswid(coswidValidateFile, coswidValidateSchema); err != nil { + return err + } + + fmt.Printf(">> validated %q against %q\n", coswidValidateFile, coswidValidateSchema) + return nil + }, +} + +func checkCoswidValidateArgs() error { + if coswidValidateFile == "" { + return fmt.Errorf("no CoSWID file supplied") + } + if coswidValidateSchema == "" { + return fmt.Errorf("no schema supplied") + } + return nil +} + +func validateCoswid(file, schema string) error { + var ( + coswidCBOR []byte + coswidJSON []byte + err error + ) + + if coswidCBOR, err = afero.ReadFile(fs, file); err != nil { + return fmt.Errorf("error loading CoSWID from %s: %w", file, err) + } + + // Decode CBOR with numeric key handling + var data map[interface{}]interface{} + if err = cbor.Unmarshal(coswidCBOR, &data); err != nil { + return fmt.Errorf("error decoding CBOR from %s: %w", file, err) + } + + // Convert map[interface{}]interface{} to map[string]interface{} + stringMap := make(map[string]interface{}) + for key, value := range data { + strKey := convertKeyToString(key) + convertedValue := convertValue(value) + stringMap[strKey] = convertedValue + } + + // Debug: Iterate and print types + for key, value := range stringMap { + fmt.Printf("Field: %s, Type: %T, Value: %v\n", key, value, value) + + switch key { + case "tag-id", "device-id", "location": + if str, ok := value.(string); !ok { + return fmt.Errorf("field %s is expected to be string, but got %T", key, value) + } else { + _ = str + } + + case "software-name": + // Accept either string or map + switch v := value.(type) { + case string: + // OK + case map[string]interface{}: + // Handle it as a nested object if needed + _ = v + default: + return fmt.Errorf("field %s has unexpected type %T", key, value) + } + + case "tag-version", "hash-alg-id": + switch v := value.(type) { + case int, int32, int64: + case float64: + intValue := int(v) + stringMap[key] = intValue + default: + return fmt.Errorf("field %s is expected to be integer, but got %T", key, value) + } + + default: + // Other fields + } + } + + // Marshal the decoded data to JSON + if coswidJSON, err = json.Marshal(stringMap); err != nil { + return fmt.Errorf("error marshaling CoSWID to JSON: %w", err) + } + + schemaLoader := gojsonschema.NewReferenceLoader("file:///" + schema) + documentLoader := gojsonschema.NewBytesLoader(coswidJSON) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("error validating CoSWID from %s: %w", file, err) + } + + if !result.Valid() { + return fmt.Errorf("CoSWID from %s is invalid: %v", file, result.Errors()) + } + + return nil +} + +func convertKeyToString(key interface{}) string { + switch k := key.(type) { + case string: + return k + case int: + if mappedKey, ok := coswidKeyMap[uint64(k)]; ok { + return mappedKey + } + return fmt.Sprintf("%d", k) + case uint64: + if mappedKey, ok := coswidKeyMap[k]; ok { + return mappedKey + } + return fmt.Sprintf("%d", k) + default: + return fmt.Sprintf("%v", k) + } +} + +func convertValue(value interface{}) interface{} { + switch v := value.(type) { + case map[interface{}]interface{}: + // Convert nested maps + m := make(map[string]interface{}) + for k, val := range v { + strKey := convertKeyToString(k) + m[strKey] = convertValue(val) + } + return m + case []interface{}: + // Convert slice elements + slice := make([]interface{}, len(v)) + for i, val := range v { + slice[i] = convertValue(val) + } + return slice + case []uint8: + // Convert byte arrays to base64 + return base64.StdEncoding.EncodeToString(v) + default: + return v + } +} + +func init() { + coswidCmd.AddCommand(coswidValidateCmd) + coswidValidateCmd.Flags().StringVarP(&coswidValidateFile, "file", "f", "", "a CoSWID file (in CBOR format)") + coswidValidateCmd.Flags().StringVarP(&coswidValidateSchema, "schema", "s", "", "a JSON schema file") +} diff --git a/data/coswid/coswid-example.cbor b/data/coswid/coswid-example.cbor new file mode 100644 index 0000000000000000000000000000000000000000..18b2abf99195a85d7f94c5827aa0e3c3217dc783 GIT binary patch literal 115 zcmZ3=5Fljdn*L(8(B&mzPH7_I4h%dDj0LV0iMa(isS3gQX(i=}MX5}SCFFBpf<7fF zdJ+o786_nJ#a8AJ~9sfi`|MH#7OnJKAx$*GG{N-7Id6H?0( Ib4pVa0NUm$A^-pY literal 0 HcmV?d00001 diff --git a/data/coswid/coswid-full.cbor b/data/coswid/coswid-full.cbor new file mode 100644 index 0000000000000000000000000000000000000000..3fcdce7fe8d4b2f00c71e3c12dd48a40be3c0f80 GIT binary patch literal 235 zcmZ3%5Fljdn*L(8(B&mzPH7_I4h%euj0LV0iMa(isS3gQX(i=}MX7v@Op7Jt%VEMk zB`FHUndy0%dFcw-sg)86#Tg|f1;tkS`l%3&ddc~@5{lU+iRrq@MX8A;`9&;?B-AP# zV9E-BX6BXXK~?J)W#$&^CnqNvCnj2$>Y5oQndq7rq#5d3m{_Fh8W<&}n59}I8kw3T xNocUKEyzeM%S=hlOHN&sQc_uvS^zag7bcuqmY7qTTHp#*rI3o`B_##LR{Hv>5N&$N`MDB`*(HhTy2(YUi6!|(8L4HNDXDqM Tsf$udDhpB*Qp*x^N>dX6!sIP@ literal 0 HcmV?d00001 diff --git a/data/coswid/coswid-meta-mini.cbor b/data/coswid/coswid-meta-mini.cbor new file mode 100644 index 0000000000000000000000000000000000000000..7b42dc9c44810b2dccac61a331a3b219d38a316f GIT binary patch literal 126 zcmZ3=5Fljdn*L(8(B&mzPH7_I4h%euj0LV0iMa(isS3gQX(i=}MX5}SCFILtf<7fF z3dNb}d6{|X3fZZZ5(>o`B_##LR{Hv>5N&$N`MDB`*(HhTy2(YUi6!|(8L4HNDXDqM Tsf$udDhpB*Qp*x^N>dX6!sIP@ literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 1a993d3..c73e68c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/veraison/cocli go 1.22 require ( + github.com/fxamacker/cbor/v2 v2.5.0 github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/spf13/afero v1.9.2 @@ -14,13 +15,13 @@ require ( github.com/veraison/corim v1.1.3-0.20241003171039-fe09de9f3764 github.com/veraison/go-cose v1.3.0 github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca + github.com/xeipuuv/gojsonschema v1.2.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -42,6 +43,8 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/veraison/eat v0.0.0-20210331113810-3da8a4dd42ff // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect diff --git a/go.sum b/go.sum index 95dd6cc..64e5570 100644 --- a/go.sum +++ b/go.sum @@ -331,6 +331,12 @@ github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca h1:osmCKwWO/xM68Kz github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca/go.mod h1:d5jt76uMNbTfQ+f2qU4Lt8RvWOTsv6PFgstIM1QdMH0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=