Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LinkAttestor for in-toto Attestation Framework Link Predicate #288

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 40 additions & 4 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
materialsPaths []string
productsPaths []string
noCommand bool
attestations []string
)

var runCmd = &cobra.Command{
Expand Down Expand Up @@ -163,6 +164,16 @@ recorded independently of this parameter.`,
"UDS path for SPIFFE workload API",
)

runCmd.Flags().StringArrayVarP(
&attestations,
"attestations",
"a",
[]string{},
`Create metadata using Attestation Framework.
Attestation accepts a list of supported predicate types.
Note: Currently only 'link' is supported.`,
)

}

func run(cmd *cobra.Command, args []string) error {
Expand All @@ -174,12 +185,37 @@ func run(cmd *cobra.Command, args []string) error {
return fmt.Errorf("no command arguments passed, please specify or use --no-command option")
}

metadata, err := intoto.InTotoRun(stepName, runDir, materialsPaths, productsPaths, args, key, []string{"sha256"}, exclude, lStripPaths, lineNormalization, followSymlinkDirs, useDSSE)
if err != nil {
return fmt.Errorf("failed to create link metadata: %w", err)
var metadata intoto.Metadata
var err error
if len(attestations) > 0 {
// For now any value passed to attestations will run the link attestor
attestor := intoto.LinkAttestor{
MaterialPaths: materialsPaths,
ProductPaths: productsPaths,
HashAlgorithms: []string{"sha256"},
GitignorePatterns: exclude,
LStripPaths: lStripPaths,
CmdArgs: args,
RunDir: runDir,
StepName: stepName,
LineNormalization: lineNormalization,
FollowSymlinkDirs: followSymlinkDirs,
Key: key,
}

metadata, err = attestor.Attest()
if err != nil {
return fmt.Errorf("failed to create attestation: %w", err)
}
} else {
metadata, err = intoto.InTotoRun(stepName, runDir, materialsPaths, productsPaths,
args, key, []string{"sha256"}, exclude, lStripPaths, lineNormalization, followSymlinkDirs, useDSSE)
if err != nil {
return fmt.Errorf("failed to create link metadata: %w", err)
}
}

linkName := fmt.Sprintf(intoto.LinkNameFormat, metadata.GetPayload().(intoto.Link).Name, key.KeyID)
linkName := fmt.Sprintf(intoto.LinkNameFormat, stepName, key.KeyID)

linkPath := filepath.Join(outDir, linkName)
err = metadata.Dump(linkPath)
Expand Down
3 changes: 3 additions & 0 deletions doc/in-toto_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ in-toto run [flags]
### Options

```
-a, --attestations stringArray Create metadata using Attestation Framework.
Attestation accepts a list of supported predicate types.
Note: Currently only 'link' is supported.
-c, --cert string Path to a PEM formatted certificate that corresponds with
the provided key.
-e, --exclude stringArray Path patterns to match paths that should not be recorded as 0
Expand Down
147 changes: 146 additions & 1 deletion in_toto/attestations.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package in_toto

import (
link "github.com/in-toto/attestation/go/predicates/link/v0"
ita1 "github.com/in-toto/attestation/go/v1"
v1 "github.com/in-toto/attestation/go/v1"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
slsa01 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.1"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
)

const (
Expand All @@ -23,8 +27,15 @@ const (
PredicateSPDX = "https://spdx.dev/Document"
// PredicateCycloneDX represents a CycloneDX SBOM
PredicateCycloneDX = "https://cyclonedx.org/bom"
// PredicateLinkV1 represents an in-toto 0.9 link.

/*
Deprecated - use PredicateLink instead
PredicateLinkV1 represents an in-toto 0.9 link.
*/
PredicateLinkV1 = "https://in-toto.io/Link/v1"

// Represents an in-toto link predicate
PredicateLink = "https://in-toto.io/attestation/link/v0.3"
)

// Subject describes the set of software artifacts the statement applies to.
Expand Down Expand Up @@ -131,3 +142,137 @@ type CycloneDXStatement struct {
StatementHeader
Predicate interface{} `json:"predicate"`
}

/*
An Attestor is used to create in-toto Attestation
framework compliant metadata.

- GenerateStatement exists to enable usage of alternative
signing solutions.
- Attest supports using the DSSE for signing
*/
type Attestor interface {
GenerateStatement() (*v1.Statement, error)
Attest() (*Envelope, error)
}

/*
LinkAttestor follows the in-toto Attestation framework
for a Link Predicate.
*/
type LinkAttestor struct {
MaterialPaths []string
ProductPaths []string
HashAlgorithms []string
GitignorePatterns []string
LStripPaths []string
CmdArgs []string
RunDir string
StepName string
LineNormalization bool
FollowSymlinkDirs bool
Key Key
}

func (a *LinkAttestor) Attest() (*Envelope, error) {
statement, err := a.GenerateStatement()
if err != nil {
return nil, err
}

return signStatement(statement, a.Key)
}

func (a *LinkAttestor) GenerateStatement() (*v1.Statement, error) {
materials, err := RecordArtifacts(a.MaterialPaths, a.HashAlgorithms, a.GitignorePatterns, a.LStripPaths, a.LineNormalization, a.FollowSymlinkDirs)
if err != nil {
return nil, err
}

// make sure that we only run RunCommand if cmdArgs is not nil or empty
byProducts := map[string]interface{}{}
if len(a.CmdArgs) != 0 {
byProducts, err = RunCommand(a.CmdArgs, a.RunDir)
if err != nil {
return nil, err
}
}

products, err := RecordArtifacts(a.ProductPaths, a.HashAlgorithms, a.GitignorePatterns, a.LStripPaths, a.LineNormalization, a.FollowSymlinkDirs)
if err != nil {
return nil, err
}

var materialResources = make([]*v1.ResourceDescriptor, len(materials))
matCount := 0
for key, value := range materials {
materialResources[matCount] = &v1.ResourceDescriptor{
Name: key,
Digest: value,
}

matCount++
}

var productResources = make([]*v1.ResourceDescriptor, len(products))
prdCount := 0
for key, value := range products {
productResources[0] = &v1.ResourceDescriptor{
Name: key,
Digest: value,
}

prdCount++
}

byProductsStruct, err := structpb.NewStruct(byProducts)
if err != nil {
return nil, err
}

link := link.Link{
Name: a.StepName,
Command: a.CmdArgs,
Materials: materialResources,
Byproducts: byProductsStruct,
}

linkJson, err := protojson.Marshal(&link)
if err != nil {
return nil, err
}

linkStruct := &structpb.Struct{}
err = protojson.Unmarshal(linkJson, linkStruct)
if err != nil {
return nil, err
}

statement := v1.Statement{
Type: StatementInTotoV1,
Subject: productResources,
PredicateType: PredicateLink,
Predicate: linkStruct,
}

Forrin marked this conversation as resolved.
Show resolved Hide resolved
err = statement.Validate()
if err != nil {
return nil, err
}

return &statement, nil
}

func signStatement(statement *v1.Statement, key Key) (*Envelope, error) {
env := &Envelope{}
if err := env.SetPayloadProtobuf(statement); err != nil {
return nil, err
}

err := env.Sign(key)
if err != nil {
return nil, err
}

return env, nil
}
132 changes: 132 additions & 0 deletions in_toto/attestations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package in_toto

import (
"encoding/json"
"fmt"
"testing"
"time"

v1 "github.com/in-toto/attestation/go/v1"
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
slsa01 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.1"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/encoding/protojson"
)

func TestDecodeProvenanceStatementSLSA02(t *testing.T) {
Expand Down Expand Up @@ -483,3 +486,132 @@ func TestLinkStatement(t *testing.T) {

assert.Equal(t, want, got, "Unexpexted object after decoding")
}

func TestLinkAttestorAttest(t *testing.T) {
linkName := "test"

var validKey Key
if err := validKey.LoadKey("carol", "ed25519", []string{"sha256", "sha512"}); err != nil {
t.Error(err)
}

tablesCorrect := []struct {
materialPaths []string
productPaths []string
cmdArgs []string
key Key
hashAlgorithms []string
result string
}{
{
[]string{}, []string{"emptyfile"}, []string{"touch", "emptyfile"}, validKey, []string{"sha256"},
`{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "emptyfile",
"digest": {
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
}
],
"predicateType": "https://in-toto.io/attestation/link/v0.3",
"predicate": {
"byproducts": {
"return-value": 0,
"stderr": "",
"stdout": ""
},
"command": [
"touch",
"emptyfile"
],
"name": "test"
}
}`,
},
}

for _, table := range tablesCorrect {
attestor := LinkAttestor{
MaterialPaths: table.materialPaths,
ProductPaths: table.productPaths,
StepName: linkName,
RunDir: "",
CmdArgs: table.cmdArgs,
Key: table.key,
HashAlgorithms: table.hashAlgorithms,
GitignorePatterns: []string{},
LStripPaths: []string{},
LineNormalization: false,
FollowSymlinkDirs: false,
}

envelope, err := attestor.Attest()
if err != nil {
t.Errorf(err.Error())
}

actualDecodedPayload, err := envelope.envelope.DecodeB64Payload()
if err != nil {
t.Errorf(err.Error())
}

var actualStatement v1.Statement
err = protojson.Unmarshal(actualDecodedPayload, &actualStatement)
if err != nil {
t.Errorf(err.Error())
}

var expectedStatement v1.Statement
err = protojson.Unmarshal([]byte(table.result), &expectedStatement)
if err != nil {
t.Errorf(err.Error())
}

assert.Equal(t, &expectedStatement, &actualStatement,
fmt.Sprintf("LinkAttestor Attest returned '(%s, %s)', expected '(%s, nil)'", &actualStatement, err, &expectedStatement))
}

// Run LinkAttestor with errors
tablesInvalid := []struct {
materialPaths []string
productPaths []string
cmdArgs []string
key Key
hashAlgorithms []string
}{
{[]string{"material-does-not-exist"}, []string{""}, []string{"sh", "-c", "printf test"}, Key{}, []string{"sha256"}},
{[]string{"demo.layout"}, []string{"product-does-not-exist"}, []string{"sh", "-c", "printf test"}, Key{}, []string{"sha256"}},
{[]string{""}, []string{"/invalid-path/"}, []string{"sh", "-c", "printf test"}, Key{}, []string{"sha256"}},
{[]string{}, []string{}, []string{"command-does-not-exist"}, Key{}, []string{"sha256"}},
{[]string{"demo.layout"}, []string{"foo.tar.gz"}, []string{"sh", "-c", "printf out; printf err >&2"}, Key{
KeyID: "this-is-invalid",
KeyIDHashAlgorithms: nil,
KeyType: "",
KeyVal: KeyVal{},
Scheme: "",
}, []string{"sha256"}},
}

for _, table := range tablesInvalid {
attestor := LinkAttestor{
MaterialPaths: table.materialPaths,
ProductPaths: table.productPaths,
StepName: linkName,
RunDir: "",
CmdArgs: table.cmdArgs,
Key: table.key,
HashAlgorithms: table.hashAlgorithms,
GitignorePatterns: nil,
LStripPaths: nil,
LineNormalization: testOSisWindows(),
FollowSymlinkDirs: false,
}

envelope, err := attestor.Attest()
if err == nil {
t.Errorf("LinkAttestor Attest returned '(%s, %s)', expected error", envelope.envelope, err)
}
}
}