diff --git a/Makefile b/Makefile index e97d294..6903976 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ $(VENVDIR): py-venv: $(VENVDIR) $(PYTHON_DIR) go-mod: - cd ./go && go build && go install + cd ./scai-gen && go build && go install clean: @echo REMOVE SCAI VENV AND PYTHON LIB DIRS diff --git a/docs/usage.md b/docs/usage.md index 977c63b..0a9e241 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -18,9 +18,9 @@ For information on how to use our CLI tools in [Python] or [Go] environments, please refer to their instructions. [in-toto Statement]: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md -[Resource Descriptors]: https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md -[Attribute Assertions]: https://github.com/in-toto/attestation/blob/main/protos/in_toto_attestation/predicates/scai/v0/scai.proto#L16 +[ResourceDescriptors]: https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md +[AttributeAssertions]: https://github.com/in-toto/attestation/blob/main/protos/in_toto_attestation/predicates/scai/v0/scai.proto#L16 [Report]: https://github.com/in-toto/attestation/blob/main/protos/in_toto_attestation/predicates/scai/v0/scai.proto#L28 [SCAI specification]: https://github.com/in-toto/attestation/blob/main/spec/predicates/scai.md -[Go]: ../go/README.md +[Go]: ../scai-gen/README.md [Python]: ../python/README.md diff --git a/scai-gen/cmd/assert.go b/scai-gen/cmd/assert.go index 08f1a65..66179ab 100644 --- a/scai-gen/cmd/assert.go +++ b/scai-gen/cmd/assert.go @@ -3,9 +3,9 @@ package cmd import ( "fmt" - "github.com/in-toto/scai-demos/scai-gen/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/generators" - scai "github.com/in-toto/attestation/go/predicates/scai/v0" ita "github.com/in-toto/attestation/go/v1" "github.com/spf13/cobra" "google.golang.org/protobuf/types/known/structpb" @@ -94,16 +94,9 @@ func genAttrAssertion(_ *cobra.Command, args []string) error { } } - aa := &scai.AttributeAssertion{ - Attribute: attribute, - Target: target, - Conditions: conditions, - Evidence: evidence, - } - - err := aa.Validate() + aa, err := generators.NewSCAIAssertion(attribute, target, conditions, evidence) if err != nil { - return fmt.Errorf("invalid SCAI attribute assertion: %w", err) + return fmt.Errorf("unable to generate SCAI attribute assertion: %w", err) } return fileio.WritePbToFile(aa, outFile, false) diff --git a/scai-gen/cmd/check.go b/scai-gen/cmd/check.go index 2d6b676..5458075 100644 --- a/scai-gen/cmd/check.go +++ b/scai-gen/cmd/check.go @@ -8,8 +8,8 @@ import ( "path/filepath" "strings" - "github.com/in-toto/scai-demos/scai-gen/fileio" - "github.com/in-toto/scai-demos/scai-gen/policy" + "github.com/in-toto/scai-demos/scai-gen/pkg/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/policy" "github.com/in-toto/attestation-verifier/verifier" scai "github.com/in-toto/attestation/go/predicates/scai/v0" diff --git a/scai-gen/cmd/rd.go b/scai-gen/cmd/rd.go index 85aa909..aedd8e4 100644 --- a/scai-gen/cmd/rd.go +++ b/scai-gen/cmd/rd.go @@ -1,15 +1,11 @@ package cmd import ( - "encoding/hex" "fmt" - "os" - "strings" - "github.com/in-toto/scai-demos/scai-gen/fileio" - "github.com/in-toto/scai-demos/scai-gen/policy" + "github.com/in-toto/scai-demos/scai-gen/pkg/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/generators" - ita "github.com/in-toto/attestation/go/v1" "github.com/spf13/cobra" "google.golang.org/protobuf/types/known/structpb" ) @@ -163,41 +159,15 @@ func genRdFromFile(_ *cobra.Command, args []string) error { } filename := args[0] - fileBytes, err := os.ReadFile(filename) - if err != nil { - return fmt.Errorf("error reading resource file: %w", err) - } - - var content []byte - if withContent { - content = fileBytes - } - - sha256Digest := hex.EncodeToString(policy.GenSHA256(fileBytes)) - - rdName := filename - if len(name) > 0 { - rdName = name - } annotations, err := readAnnotations(annotationsFile) if err != nil { - return fmt.Errorf("error reading annotations file: %w", err) - } - - rd := &ita.ResourceDescriptor{ - Name: rdName, - Uri: uri, - Digest: map[string]string{"sha256": strings.ToLower(sha256Digest)}, - Content: content, - DownloadLocation: downloadLocation, - MediaType: mediaType, - Annotations: annotations, + return fmt.Errorf("unable to read annotations file: %w", err) } - err = rd.Validate() + rd, err := generators.NewRdForFile(filename, name, uri, hashAlg, withContent, mediaType, downloadLocation, annotations) if err != nil { - return fmt.Errorf("invalid resource descriptor: %w", err) + return fmt.Errorf("unable to generate RD: %w", err) } return fileio.WritePbToFile(rd, outFile, false) @@ -211,35 +181,14 @@ func genRdForRemote(_ *cobra.Command, args []string) error { remoteURI := args[0] - digestSet := make(map[string]string) - if len(digest) > 0 { - // the in-toto spec expects a hex-encoded string in DigestSets - // https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md - _, err := hex.DecodeString(digest) - if err != nil { - return fmt.Errorf("digest is not valid hex-encoded string: %w", err) - } - - // we can assume that we have both variables set at this point - digestSet = map[string]string{hashAlg: strings.ToLower(digest)} - } - annotations, err := readAnnotations(annotationsFile) if err != nil { - return fmt.Errorf("error reading annotations file: %w", err) - } - - rd := &ita.ResourceDescriptor{ - Name: name, - Uri: remoteURI, - Digest: digestSet, - DownloadLocation: downloadLocation, - Annotations: annotations, + return fmt.Errorf("unable to read annotations file: %w", err) } - err = rd.Validate() + rd, err := generators.NewRdForRemote(remoteURI, name, hashAlg, digest, downloadLocation, annotations) if err != nil { - return fmt.Errorf("invalid resource descriptor: %w", err) + return fmt.Errorf("unable to generate RD: %w", err) } return fileio.WritePbToFile(rd, outFile, false) diff --git a/scai-gen/cmd/report.go b/scai-gen/cmd/report.go index f05b27a..046060e 100644 --- a/scai-gen/cmd/report.go +++ b/scai-gen/cmd/report.go @@ -3,7 +3,8 @@ package cmd import ( "fmt" - "github.com/in-toto/scai-demos/scai-gen/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/generators" scai "github.com/in-toto/attestation/go/predicates/scai/v0" ita "github.com/in-toto/attestation/go/v1" @@ -87,14 +88,10 @@ func genAttrReport(_ *cobra.Command, args []string) error { } // first, generate the SCAI Report - ar := &scai.AttributeReport{ - Attributes: attrAsserts, - Producer: producer, - } - err := ar.Validate() + ar, err := generators.NewSCAIReport(attrAsserts, producer) if err != nil { - return fmt.Errorf("invalid SCAI attribute report: %w", err) + return fmt.Errorf("unable to generate SCAI Report: %w", err) } // then, plug the Report into an in-toto Statement @@ -118,16 +115,9 @@ func genAttrReport(_ *cobra.Command, args []string) error { return err } - statement := &ita.Statement{ - Type: ita.StatementTypeUri, - Subject: []*ita.ResourceDescriptor{subject}, - PredicateType: "https://in-toto.io/attestation/scai/attribute-report/v0.2", - Predicate: reportStruct, - } - - err = statement.Validate() + statement, err := generators.NewStatement([]*ita.ResourceDescriptor{subject}, "https://in-toto.io/attestation/scai/attribute-report/v0.2", reportStruct) if err != nil { - return fmt.Errorf("invalid in-toto Statement: %w", err) + return fmt.Errorf("unable to generate in-toto Statement: %w", err) } return fileio.WritePbToFile(statement, outFile, prettyPrint) diff --git a/scai-gen/cmd/sigstore.go b/scai-gen/cmd/sigstore.go index 89a4f70..94b8c5b 100644 --- a/scai-gen/cmd/sigstore.go +++ b/scai-gen/cmd/sigstore.go @@ -7,7 +7,7 @@ import ( "context" "fmt" - "github.com/in-toto/scai-demos/scai-gen/fileio" + "github.com/in-toto/scai-demos/scai-gen/pkg/fileio" ita "github.com/in-toto/attestation/go/v1" "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" diff --git a/scai-gen/fileio/common.go b/scai-gen/pkg/fileio/common.go similarity index 100% rename from scai-gen/fileio/common.go rename to scai-gen/pkg/fileio/common.go diff --git a/scai-gen/fileio/dsse.go b/scai-gen/pkg/fileio/dsse.go similarity index 100% rename from scai-gen/fileio/dsse.go rename to scai-gen/pkg/fileio/dsse.go diff --git a/scai-gen/fileio/map.go b/scai-gen/pkg/fileio/map.go similarity index 100% rename from scai-gen/fileio/map.go rename to scai-gen/pkg/fileio/map.go diff --git a/scai-gen/fileio/pb.go b/scai-gen/pkg/fileio/pb.go similarity index 100% rename from scai-gen/fileio/pb.go rename to scai-gen/pkg/fileio/pb.go diff --git a/scai-gen/pkg/generators/scai.go b/scai-gen/pkg/generators/scai.go new file mode 100644 index 0000000..9fcff41 --- /dev/null +++ b/scai-gen/pkg/generators/scai.go @@ -0,0 +1,43 @@ +package generators + +import ( + "fmt" + + scai "github.com/in-toto/attestation/go/predicates/scai/v0" + ita "github.com/in-toto/attestation/go/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +// Generates a SCAI v0 AttributeAssertion struct. +// Throws an error if the resulting AttributeAssertion does not meet the spec. +func NewSCAIAssertion(attribute string, target *ita.ResourceDescriptor, conditions *structpb.Struct, evidence *ita.ResourceDescriptor) (*scai.AttributeAssertion, error) { + aa := &scai.AttributeAssertion{ + Attribute: attribute, + Target: target, + Conditions: conditions, + Evidence: evidence, + } + + err := aa.Validate() + if err != nil { + return nil, fmt.Errorf("invalid SCAI attribute assertion: %w", err) + } + + return aa, nil +} + +// Generates a SCAI v0 AttributeReport struct to be used as an in-toto attestation predicate. +// Throws an error if the resulting AttributeReport does not meet the spec. +func NewSCAIReport(attrAssertions []*scai.AttributeAssertion, producer *ita.ResourceDescriptor) (*scai.AttributeReport, error) { + ar := &scai.AttributeReport{ + Attributes: attrAssertions, + Producer: producer, + } + + err := ar.Validate() + if err != nil { + return nil, fmt.Errorf("invalid SCAI attribute report: %w", err) + } + + return ar, nil +} diff --git a/scai-gen/pkg/generators/v1.go b/scai-gen/pkg/generators/v1.go new file mode 100644 index 0000000..6598c8f --- /dev/null +++ b/scai-gen/pkg/generators/v1.go @@ -0,0 +1,101 @@ +package generators + +import ( + "encoding/hex" + "fmt" + "os" + "strings" + + "github.com/in-toto/scai-demos/scai-gen/pkg/policy" + + ita "github.com/in-toto/attestation/go/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +// Generates an in-toto Attestation Framework v1 ResourceDescriptor for a local file, including its digest (default sha256). +// Throws an error if the resulting ResourceDescriptor does not meet the spec. +func NewRdForFile(filename, name, uri, hashAlg string, withContent bool, mediaType, downloadLocation string, annotations *structpb.Struct) (*ita.ResourceDescriptor, error) { + fileBytes, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading resource file: %w", err) + } + + var content []byte + if withContent { + content = fileBytes + } + + var digest string + var alg string + if hashAlg == "sha256" || hashAlg == "" { + digest = hex.EncodeToString(policy.GenSHA256(fileBytes)) + alg = "sha256" + } else { + return nil, fmt.Errorf("hash algorithm %s not supported", hashAlg) + } + + rdName := filename + if len(name) > 0 { + rdName = name + } + + rd := &ita.ResourceDescriptor{ + Name: rdName, + Uri: uri, + Digest: map[string]string{alg: strings.ToLower(digest)}, + Content: content, + DownloadLocation: downloadLocation, + MediaType: mediaType, + Annotations: annotations, + } + + err = rd.Validate() + if err != nil { + return nil, fmt.Errorf("invalid resource descriptor: %w", err) + } + + return rd, nil +} + +// Generates an in-toto Attestation Framework v1 ResourceDescriptor for a remote resource identified by a name or URI). +// Does not check if the URI resolves to a valid remote location. +// Throws an error if the resulting ResourceDescriptor does not meet the spec. +func NewRdForRemote(name, uri, hashAlg, digest, downloadLocation string, annotations *structpb.Struct) (*ita.ResourceDescriptor, error) { + digestSet := make(map[string]string) + if len(hashAlg) > 0 && len(digest) > 0 { + digestSet = map[string]string{hashAlg: strings.ToLower(digest)} + } + + rd := &ita.ResourceDescriptor{ + Name: name, + Uri: uri, + Digest: digestSet, + DownloadLocation: downloadLocation, + Annotations: annotations, + } + + err := rd.Validate() + if err != nil { + return nil, fmt.Errorf("invalid resource descriptor: %w", err) + } + + return rd, nil +} + +// Generates an in-toto Attestation Framework v1 Statement including a given predicate. +// Throws an error if the resulting Statement does not meet the spec. +func NewStatement(subjects []*ita.ResourceDescriptor, predicateType string, predicate *structpb.Struct) (*ita.Statement, error) { + statement := &ita.Statement{ + Type: ita.StatementTypeUri, + Subject: subjects, + PredicateType: predicateType, + Predicate: predicate, + } + + err := statement.Validate() + if err != nil { + return nil, fmt.Errorf("invalid in-toto Statement: %w", err) + } + + return statement, nil +} diff --git a/scai-gen/policy/attestation.go b/scai-gen/pkg/policy/attestation.go similarity index 100% rename from scai-gen/policy/attestation.go rename to scai-gen/pkg/policy/attestation.go diff --git a/scai-gen/policy/checks.go b/scai-gen/pkg/policy/checks.go similarity index 100% rename from scai-gen/policy/checks.go rename to scai-gen/pkg/policy/checks.go diff --git a/scai-gen/policy/plaintext.go b/scai-gen/pkg/policy/plaintext.go similarity index 100% rename from scai-gen/policy/plaintext.go rename to scai-gen/pkg/policy/plaintext.go