diff --git a/.github/actions/scai-gen-assert/action.yml b/.github/actions/scai-gen-assert/action.yml index 54deff8..036ad4b 100644 --- a/.github/actions/scai-gen-assert/action.yml +++ b/.github/actions/scai-gen-assert/action.yml @@ -43,7 +43,8 @@ runs: - name: Generate ResourceDescriptor for evidence id: gen-rd - uses: ./.github/actions/scai-gen-rd + # change to v0.2 tag when released + uses: in-toto/scai-demos/.github/actions/scai-gen-rd@main with: name: "${{ inputs.evidence-file }}" path: "${{ inputs.evidence-path }}" @@ -54,6 +55,5 @@ runs: id: scai-gen-assert shell: bash run: | - mkdir -p ${{ inputs.assertion-path }} - scai-gen assert -e ${{ steps.gen-rd.outputs.file-rd-name }} -o ${{ inputs.assertion-path }}/${{ inputs.assertion-name }} ${{ inputs.attribute}} + scai-gen assert --evidence ${{ steps.gen-rd.outputs.file-rd-name }} --out-file ${{ inputs.assertion-path }}/${{ inputs.assertion-name }} ${{ inputs.attribute}} echo "assertion-name=${{ inputs.assertion-path }}/${{ inputs.assertion-name }}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/scai-gen-rd/action.yml b/.github/actions/scai-gen-rd/action.yml index 57fbdde..9404fa0 100644 --- a/.github/actions/scai-gen-rd/action.yml +++ b/.github/actions/scai-gen-rd/action.yml @@ -54,8 +54,7 @@ runs: if: ${{ inputs.is-file == 'true' }} shell: bash run: | - mkdir -p ${{ inputs.rd-path }} - scai-gen rd file -n ${{ inputs.name }} -l ${{ inputs.location }} -t ${{ inputs.media-type }} -o ${{ inputs.rd-path }}/${{ inputs.rd-name }} ${{ inputs.path }}/${{ inputs.name }} + scai-gen rd file --name ${{ inputs.name }} --download-location ${{ inputs.location }} --media-type ${{ inputs.media-type }} --out-file ${{ inputs.rd-path }}/${{ inputs.rd-name }} ${{ inputs.path }}/${{ inputs.name }} echo "rd-name=${{ inputs.rd-path }}/${{ inputs.rd-name }}" >> "$GITHUB_OUTPUT" - name: Run scai-gen rd remote @@ -63,6 +62,5 @@ runs: if: ${{ inputs.is-file == 'false' }} shell: bash run: | - mkdir -p ${{ inputs.rd-path }} - scai-gen rd remote -n ${{ inputs.name }} -d ${{ inputs.digest }} -g ${{ inputs.hash-alg }} -o ${{ inputs.rd-path }}/${{ inputs.rd-name }} ${{ inputs.uri }} + scai-gen rd remote --name ${{ inputs.name }} --digest ${{ inputs.digest }} --hash-alg ${{ inputs.hash-alg }} --out-file ${{ inputs.rd-path }}/${{ inputs.rd-name }} ${{ inputs.uri }} echo "rd-name=${{ inputs.rd-path }}/${{ inputs.rd-name }}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/scai-gen-report/action.yml b/.github/actions/scai-gen-report/action.yml index 0ce78f1..ed32aad 100644 --- a/.github/actions/scai-gen-report/action.yml +++ b/.github/actions/scai-gen-report/action.yml @@ -27,10 +27,8 @@ runs: id: scai-gen-report shell: bash run: | - mkdir -p ${{ inputs.report-path }} - scai-gen report -s ${{ inputs.subject }} -o ${{ inputs.report-path }}/${{ inputs.report-name }} ${{ inputs.attr-assertions }} + scai-gen report --subject ${{ inputs.subject }} --out-file ${{ inputs.report-path }}/${{ inputs.report-name }} --pretty-print ${{ inputs.attr-assertions }} echo "report-name=${{ inputs.report-path }}/${{ inputs.report-name }}" >> "$GITHUB_OUTPUT" - ls ${{ inputs.report-path }} - name: Upload the signed SCAI AttributeReport id: upload-assert diff --git a/.github/actions/scai-gen-sigstore/action.yml b/.github/actions/scai-gen-sigstore/action.yml index cbfb91d..27d0a1d 100644 --- a/.github/actions/scai-gen-sigstore/action.yml +++ b/.github/actions/scai-gen-sigstore/action.yml @@ -32,8 +32,7 @@ runs: id: sign shell: bash run: | - mkdir -p ${{ inputs.attestation-path }} - scai-gen sigstore -o ${{ inputs.attestation-path}}/${{ inputs.attestation-name }} ${{ inputs.statement-path }}/${{ inputs.statement-name }} + scai-gen sigstore --out-file ${{ inputs.attestation-path}}/${{ inputs.attestation-name }} ${{ inputs.statement-path }}/${{ inputs.statement-name }} echo "attestation-name=${{ inputs.attestation-path }}/${{ inputs.attestation-name }}" >> "$GITHUB_OUTPUT" - name: Save the signed in-toto Attestation diff --git a/README.md b/README.md index 8421a2d..9e6f2fa 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,19 @@ for Python and Go environments. We encourage you to gain a basic understanding of the [SCAI specification] before using the scai-generator CLI tools in this repo. +For a full demo of how to use the Go [scai-gen](scai-gen/) tools, read our +[KubeCon + CloudNativeCon NA '23 doc]. + ## Disclaimer While the tools in this repo are conformant to the [in-toto Attestation Framework], they do not generate **authenticated** SCAI attestations. The example use cases in this repo are only provided for -illustrative purposes. +illustrative purposes, and should not be used in production. [in-toto Attestation Framework]: https://github.com/in-toto/attestation/tree/main/spec [intro doc]: docs/intro.md +[KubeCon + CloudNativeCon NA '23]: docs/kccncna2023.md [usage doc]: docs/usage.md [SCAI specification]: https://github.com/in-toto/attestation/blob/main/spec/predicates/scai.md [SCAI spec doc]: https://arxiv.org/pdf/2210.05813.pdf diff --git a/docs/images/intoto-kccncna2023-demo.png b/docs/images/intoto-kccncna2023-demo.png new file mode 100644 index 0000000..bf8a3f8 Binary files /dev/null and b/docs/images/intoto-kccncna2023-demo.png differ diff --git a/docs/kccncna2023.md b/docs/kccncna2023.md new file mode 100644 index 0000000..8eaaed1 --- /dev/null +++ b/docs/kccncna2023.md @@ -0,0 +1,45 @@ +# KubeCon + CloudNativeCon NA '23 Demo + +As part of the [in-toto Maintainer Track talk] at KubeCon + CloudNativeCon NA +'23, we present a demo of the in-toto Attestation Framework, SCAI, and the +in-toto Attestation Verifier. + +## Demo Setup + +The overall flow implemented in the demo is as follows: + +in-toto demo flow + +This demo setup is implemented using the [scai-gen GitHub Actions] in a Docker +container build [demo workflow] for the Hyperledger Labs Private Data Objects +project. + +### Generated Attestations + +This demo generates the follow _authenticated_ in-toto attestations: + +* [SLSA Provenance] attestation for the container build +* [SCAI Attribute Report] attestation for additional integrity metadata about +the build + +These two attestations are signed using cosign OIDC-based keyless signing, +and uploaded to the public Rekor log. + +### Additional Tools + +This demo makes use of the following additional tools: + +* in-toto [attestation-verifier] +* [Anchore SBOM generator] GitHub Action +* [SLSA generic Provenance generator] GitHub Action +* [strace] Linux syscall tracer + +[Anchore SBOM generator]: https://github.com/anchore/sbom-action +[attestation-verifier]: https://github.com/in-toto/attestation-verifier +[demo workflow]: https://github.com/marcelamelara/private-data-objects/blob/intoto-kccncna2023-demo/.github/workflows/intoto-kccncna2023-demo.yml +[in-toto Maintainer Track talk]: https://kccncna2023.sched.com/event/1R2mx +[SLSA generic Provenance generator]: https://github.com/slsa-framework/slsa-github-generator +[SLSA Provenance]: https://github.com/in-toto/attestation/blob/main/spec/predicates/provenance.md +[SCAI Attribute Report]: https://github.com/in-toto/attestation/blob/main/spec/predicates/scai.md +[scai-gen GitHub Actions]: https://github.com/in-toto/scai-demos/tree/main/.github/actions +[strace]: https://strace.io/ diff --git a/scai-gen/cmd/assert.go b/scai-gen/cmd/assert.go index 4a4afb6..08f1a65 100644 --- a/scai-gen/cmd/assert.go +++ b/scai-gen/cmd/assert.go @@ -60,6 +60,11 @@ func init() { } func genAttrAssertion(_ *cobra.Command, args []string) error { + // want to make sure the AttributeAssertion is a JSON file + if !fileio.HasJSONExt(outFile) { + return fmt.Errorf("expected a .json extension for the generated SCAI AttributeAssertion file %s", outFile) + } + attribute := args[0] var target *ita.ResourceDescriptor @@ -101,5 +106,5 @@ func genAttrAssertion(_ *cobra.Command, args []string) error { return fmt.Errorf("invalid SCAI attribute assertion: %w", err) } - return fileio.WritePbToFile(aa, outFile) + return fileio.WritePbToFile(aa, outFile, false) } diff --git a/scai-gen/cmd/rd.go b/scai-gen/cmd/rd.go index 8252a1e..85aa909 100644 --- a/scai-gen/cmd/rd.go +++ b/scai-gen/cmd/rd.go @@ -75,14 +75,6 @@ func init() { "The URI of the resource", ) - rdFileCmd.Flags().BoolVarP( - &withContent, - "content", - "c", - false, - "Flag to include the content of the file", - ) - rdFileCmd.Flags().StringVarP( &downloadLocation, "download-location", @@ -165,6 +157,11 @@ func readAnnotations(filename string) (*structpb.Struct, error) { } func genRdFromFile(_ *cobra.Command, args []string) error { + // want to make sure the ResourceDescriptor is a JSON file + if !fileio.HasJSONExt(outFile) { + return fmt.Errorf("expected a .json extension for the generated ResourceDescriptor file %s", outFile) + } + filename := args[0] fileBytes, err := os.ReadFile(filename) if err != nil { @@ -203,10 +200,15 @@ func genRdFromFile(_ *cobra.Command, args []string) error { return fmt.Errorf("invalid resource descriptor: %w", err) } - return fileio.WritePbToFile(rd, outFile) + return fileio.WritePbToFile(rd, outFile, false) } func genRdForRemote(_ *cobra.Command, args []string) error { + // want to make sure the ResourceDescriptor is a JSON file + if !fileio.HasJSONExt(outFile) { + return fmt.Errorf("expected a .json extension for the generated ResourceDescriptor file %s", outFile) + } + remoteURI := args[0] digestSet := make(map[string]string) @@ -240,5 +242,5 @@ func genRdForRemote(_ *cobra.Command, args []string) error { return fmt.Errorf("invalid resource descriptor: %w", err) } - return fileio.WritePbToFile(rd, outFile) + return fileio.WritePbToFile(rd, outFile, false) } diff --git a/scai-gen/cmd/report.go b/scai-gen/cmd/report.go index 024f179..f05b27a 100644 --- a/scai-gen/cmd/report.go +++ b/scai-gen/cmd/report.go @@ -50,9 +50,22 @@ func init() { "", "The filename of the JSON-encoded producer resource descriptor", ) + + reportCmd.Flags().BoolVarP( + &prettyPrint, + "pretty-print", + "y", + false, + "Flag to JSON pretty-print the generated Report", + ) } func genAttrReport(_ *cobra.Command, args []string) error { + // want to make sure the Report is a JSON file + if !fileio.HasJSONExt(outFile) { + return fmt.Errorf("expected a .json extension for the generated in-toto Statement file %s", outFile) + } + attrAsserts := make([]*scai.AttributeAssertion, 0, len(args)) for _, attrAssertPath := range args { aa := &scai.AttributeAssertion{} @@ -117,5 +130,5 @@ func genAttrReport(_ *cobra.Command, args []string) error { return fmt.Errorf("invalid in-toto Statement: %w", err) } - return fileio.WritePbToFile(statement, outFile) + return fileio.WritePbToFile(statement, outFile, prettyPrint) } diff --git a/scai-gen/cmd/root.go b/scai-gen/cmd/root.go index 0b14aed..f606a75 100644 --- a/scai-gen/cmd/root.go +++ b/scai-gen/cmd/root.go @@ -12,7 +12,10 @@ var rootCmd = &cobra.Command{ Short: "A CLI tool for generating/checking SCAI metadata", } -var outFile string +var ( + outFile string + prettyPrint bool +) func init() { rootCmd.AddCommand(rdCmd) diff --git a/scai-gen/cmd/sigstore.go b/scai-gen/cmd/sigstore.go index 3a891a3..89a4f70 100644 --- a/scai-gen/cmd/sigstore.go +++ b/scai-gen/cmd/sigstore.go @@ -73,6 +73,11 @@ func getNewFulcioSigner(ctx context.Context) (*fulcio.Signer, error) { func signWithSigstore(cmd *cobra.Command, args []string) error { fmt.Println("EXPERIMENTAL FEATURE. DO NOT USE IN PRODUCTION.") + // want to make sure the DSSE is a JSON file + if !fileio.HasJSONExt(outFile) { + return fmt.Errorf("expected a .json extension for the generated DSSE file %s", outFile) + } + statementFile := args[0] statement := &ita.Statement{} err := fileio.ReadPbFromFile(statementFile, statement) diff --git a/scai-gen/fileio/common.go b/scai-gen/fileio/common.go new file mode 100644 index 0000000..9fa27de --- /dev/null +++ b/scai-gen/fileio/common.go @@ -0,0 +1,17 @@ +package fileio + +import ( + "os" + "path/filepath" + "strings" +) + +func HasJSONExt(filename string) bool { + return strings.HasSuffix(filename, ".json") +} + +func CreateOutDir(filename string) error { + outDir := filepath.Dir(filename) + + return os.MkdirAll(outDir, 0644) +} diff --git a/scai-gen/fileio/dsse.go b/scai-gen/fileio/dsse.go index c7d1a31..f1a59db 100644 --- a/scai-gen/fileio/dsse.go +++ b/scai-gen/fileio/dsse.go @@ -44,5 +44,10 @@ func ReadStatementFromDSSEFile(path string) (*ita.Statement, error) { } func WriteDSSEToFile(envBytes []byte, outFile string) error { + // ensure the out directory exists + if err := CreateOutDir(outFile); err != nil { + return fmt.Errorf("error creating output directory for file %s: %w", outFile, err) + } + return os.WriteFile(outFile, envBytes, 0644) //nolint:gosec } diff --git a/scai-gen/fileio/pb.go b/scai-gen/fileio/pb.go index 11b37b5..0cf650e 100644 --- a/scai-gen/fileio/pb.go +++ b/scai-gen/fileio/pb.go @@ -8,12 +8,24 @@ import ( "google.golang.org/protobuf/proto" ) -func WritePbToFile(pb proto.Message, outFile string) error { - pbBytes, err := protojson.Marshal(pb) +func WritePbToFile(pb proto.Message, outFile string, pretty bool) error { + opt := &protojson.MarshalOptions{} + if pretty { + opt.Multiline = true + opt.Indent = " " + opt.EmitUnpopulated = false + } + + pbBytes, err := opt.Marshal(pb) if err != nil { return err } + // ensure the out directory exists + if err = CreateOutDir(outFile); err != nil { + return fmt.Errorf("error creating output directory for file %s: %w", outFile, err) + } + return os.WriteFile(outFile, pbBytes, 0644) //nolint:gosec }