From c2bf7fbc685e4f5cf31333763b2c230cc02c10b7 Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Fri, 2 Aug 2024 19:53:27 +0100 Subject: [PATCH 1/6] adding changes to oci attestor, messing around with docker buildx metadata Signed-off-by: chaosinthecrd --- attestation/oci/oci.go | 220 ++++++++++----------------------- attestation/product/product.go | 5 + internal/docker/metadata.go | 66 ++++++++++ 3 files changed, 135 insertions(+), 156 deletions(-) create mode 100644 internal/docker/metadata.go diff --git a/attestation/oci/oci.go b/attestation/oci/oci.go index 47f6ddf6..63abd944 100644 --- a/attestation/oci/oci.go +++ b/attestation/oci/oci.go @@ -16,18 +16,16 @@ package oci import ( "archive/tar" - "bytes" - "compress/gzip" "crypto" "encoding/json" "fmt" "io" - "net/http" "os" "strings" "github.com/in-toto/go-witness/attestation" "github.com/in-toto/go-witness/cryptoutil" + docker "github.com/in-toto/go-witness/internal/docker" "github.com/in-toto/go-witness/log" "github.com/invopop/jsonschema" ) @@ -37,7 +35,7 @@ const ( Type = "https://witness.dev/attestations/oci/v0.1" RunType = attestation.PostProductRunType - mimeTypes = "application/x-tar" + sha256MimeType = "text/sha256+text" ) // This is a hacky way to create a compile time error in case the attestor @@ -66,14 +64,15 @@ func init() { } type Attestor struct { - TarDigest cryptoutil.DigestSet `json:"tardigest"` - Manifest []Manifest `json:"manifest"` - ImageTags []string `json:"imagetags"` - LayerDiffIDs []cryptoutil.DigestSet `json:"diffids"` - ImageID cryptoutil.DigestSet `json:"imageid"` - ManifestRaw []byte `json:"manifestraw"` - ManifestDigest cryptoutil.DigestSet `json:"manifestdigest"` - tarFilePath string `json:"-"` + Materials []Material `json:"materials"` + ImageReferences []string `json:"imagereferences"` + ImageID cryptoutil.DigestSet `json:"imageid"` + ImageDigest cryptoutil.DigestSet `json:"imagedigest"` +} + +type Material struct { + URI string `json:"uri"` + Digest cryptoutil.DigestSet `json:"digest"` } type Manifest struct { @@ -82,7 +81,7 @@ type Manifest struct { Layers []string `json:"Layers"` } -func (m *Manifest) getImageID(ctx *attestation.AttestationContext, tarFilePath string) (cryptoutil.DigestSet, error) { +func (m *Manifest) getDockerImageID(ctx *attestation.AttestationContext, tarFilePath string) (cryptoutil.DigestSet, error) { tarFile, err := os.Open(tarFilePath) if err != nil { return nil, err @@ -143,190 +142,99 @@ func (a *Attestor) Schema() *jsonschema.Schema { } func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { - if err := a.getCandidate(ctx); err != nil { + met, err := a.getDockerCandidate(ctx) + if err != nil { log.Debugf("(attestation/oci) error getting candidate: %w", err) return err } - if err := a.parseMaifest(ctx); err != nil { - log.Debugf("(attestation/oci) error parsing manifest: %w", err) - return err - } - - imageID, err := a.Manifest[0].getImageID(ctx, a.tarFilePath) - if err != nil { - log.Debugf("(attestation/oci) error getting image id: %w", err) - return err + if met != nil { + a.ImageDigest = map[cryptoutil.DigestValue]string{} + a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = met.ContainerImageDigest + fmt.Println("setting image digest as", met.ContainerImageDigest) + fmt.Println("setting image references as", met.ImageName) + a.ImageReferences = []string{} + a.ImageReferences = append(a.ImageReferences, met.ImageName) } - layerDiffIDs, err := a.Manifest[0].getLayerDIFFIDs(ctx, a.tarFilePath) - if err != nil { - return err - } - - a.ImageID = imageID - a.LayerDiffIDs = layerDiffIDs - a.ImageTags = a.Manifest[0].RepoTags - return nil } -func (a *Attestor) getCandidate(ctx *attestation.AttestationContext) error { +func (a *Attestor) getDockerCandidate(ctx *attestation.AttestationContext) (*docker.BuildInfo, error) { products := ctx.Products() if len(products) == 0 { - return fmt.Errorf("no products to attest") + return nil, fmt.Errorf("no products to attest") } + //NOTE: it's not ideal to try and parse it without a mime type but the metadata file is completely different depending on how the buildx is executed for path, product := range products { - if !strings.Contains(mimeTypes, product.MimeType) { - continue - } - - newDigestSet, err := cryptoutil.CalculateDigestSetFromFile(path, ctx.Hashes()) - if newDigestSet == nil || err != nil { - return fmt.Errorf("error calculating digest set from file: %s", path) - } + fmt.Println("inspecting", path) + if strings.Contains(sha256MimeType, product.MimeType) { + log.Info("found image id") + f, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file %s", path) + } - if !newDigestSet.Equal(product.Digest) { - return fmt.Errorf("integrity error: product digest set does not match candidate digest set") + a.ImageID = map[cryptoutil.DigestValue]string{} + a.ImageID[cryptoutil.DigestValue{Hash: crypto.SHA256}] = string(f) + continue } - a.TarDigest = product.Digest - - a.tarFilePath = path - return nil - } - return fmt.Errorf("no tar file found") -} - -func (a *Attestor) parseMaifest(ctx *attestation.AttestationContext) error { - f, err := os.Open(a.tarFilePath) - if err != nil { - err = fmt.Errorf("error opening tar file: %w", err) - return err - } - - tarReader := tar.NewReader(f) - for { - h, err := tarReader.Next() - if err == io.EOF { - break - } + var met docker.BuildInfo + f, err := os.ReadFile(path) if err != nil { - return err + return nil, fmt.Errorf("failed to read file %s", path) } - if h.FileInfo().IsDir() { + err = json.Unmarshal(f, &met) + if err != nil { + log.Debugf("(attestation/oci) error parsing file %s as docker metadata file: %w", path, err) continue } - if h.Name == "manifest.json" { - a.ManifestRaw = make([]byte, h.Size) - _, err = tarReader.Read(a.ManifestRaw) - if err != nil || err == io.EOF { - break - } - break - } - } - - manifestDigest, err := cryptoutil.CalculateDigestSetFromBytes(a.ManifestRaw, ctx.Hashes()) - if err != nil { - return err - } - a.ManifestDigest = manifestDigest + log.Info("found image metadata file") - err = json.Unmarshal(a.ManifestRaw, &a.Manifest) - if err != nil { - return err + return &met, nil } - - return nil + return nil, nil } +// TODO Needs finished, some of these are wrongly configured func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { hashes := []cryptoutil.DigestValue{{Hash: crypto.SHA256}} subj := make(map[string]cryptoutil.DigestSet) - subj[fmt.Sprintf("manifestdigest:%s", a.ManifestDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.ManifestDigest - subj[fmt.Sprintf("tardigest:%s", a.TarDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.TarDigest + subj[fmt.Sprintf("imagedigest:%s", a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.ImageDigest subj[fmt.Sprintf("imageid:%s", a.ImageID[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.ImageID - // image tags - for _, tag := range a.ImageTags { - hash, err := cryptoutil.CalculateDigestSetFromBytes([]byte(tag), hashes) - if err != nil { - log.Debugf("(attestation/oci) error calculating image tag: %w", err) - continue + for _, ir := range a.ImageReferences { + if hash, err := cryptoutil.CalculateDigestSetFromBytes([]byte(ir), hashes); err == nil { + subj[fmt.Sprintf("imagereference:%s", ir)] = hash + } else { + log.Debugf("(attestation/oci) failed to record github imagereference subject: %w", err) } - subj[fmt.Sprintf("imagetag:%s", tag)] = hash } - // diff ids - for layer := range a.LayerDiffIDs { - subj[fmt.Sprintf("layerdiffid%02d:%s", layer, a.LayerDiffIDs[layer][cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.LayerDiffIDs[layer] - } - return subj -} - -func (m *Manifest) getLayerDIFFIDs(ctx *attestation.AttestationContext, tarFilePath string) ([]cryptoutil.DigestSet, error) { - var layerDiffIDs []cryptoutil.DigestSet - - tarFile, err := os.Open(tarFilePath) - if err != nil { - return nil, err + for _, m := range a.Materials { + subj[fmt.Sprintf("materialdigest:%s", m.Digest)] = m.Digest + if hash, err := cryptoutil.CalculateDigestSetFromBytes([]byte(m.URI), hashes); err == nil { + subj[fmt.Sprintf("materialuri:%s", m.URI)] = hash + } else { + log.Debugf("(attestation/github) failed to record github materialuri subject: %w", err) + } } - defer tarFile.Close() - tarReader := tar.NewReader(tarFile) - for { - h, err := tarReader.Next() - if err == io.EOF { - break - } + // image tags + for _, ref := range a.ImageReferences { + hash, err := cryptoutil.CalculateDigestSetFromBytes([]byte(ref), hashes) if err != nil { - return nil, err - } - if h.FileInfo().IsDir() { + log.Debugf("(attestation/oci) error calculating image reference: %w", err) continue } - for _, layerFile := range m.Layers { - if h.Name == layerFile { - b := make([]byte, h.Size) - - _, err := tarReader.Read(b) - if err != nil && err != io.EOF { - return nil, err - } - - contentType := http.DetectContentType(b) - if contentType == "application/x-gzip" { - breader, err := gzip.NewReader(bytes.NewReader(b)) - if err != nil { - return nil, err - } - defer breader.Close() - c, err := io.ReadAll(breader) - if err != nil { - return nil, err - } - layerDiffID, err := cryptoutil.CalculateDigestSetFromBytes(c, ctx.Hashes()) - if err != nil { - return nil, err - } - layerDiffIDs = append(layerDiffIDs, layerDiffID) - - } else { - layerDiffID, err := cryptoutil.CalculateDigestSetFromBytes(b, ctx.Hashes()) - if err != nil { - return nil, err - } - layerDiffIDs = append(layerDiffIDs, layerDiffID) - } - - } - } + subj[fmt.Sprintf("imagereference:%s", ref)] = hash } - return layerDiffIDs, nil + + return subj } diff --git a/attestation/product/product.go b/attestation/product/product.go index 8c9d6c34..94603c6c 100644 --- a/attestation/product/product.go +++ b/attestation/product/product.go @@ -264,6 +264,11 @@ func getFileContentType(fileName string) (string, error) { return bytes.HasPrefix(buf, []byte(`{"@context":"https://openvex.dev/ns`)) }, "application/vex+json", ".vex.json") + // Add sha256 digest detector + mimetype.Lookup("text/plain").Extend(func(buf []byte, limit uint32) bool { + return bytes.HasPrefix(buf, []byte(`sha256:`)) + }, "text/sha256+text", ".sha256") + contentType, err := mimetype.DetectFile(fileName) if err != nil { return "", err diff --git a/internal/docker/metadata.go b/internal/docker/metadata.go new file mode 100644 index 00000000..ea1e369c --- /dev/null +++ b/internal/docker/metadata.go @@ -0,0 +1,66 @@ +package oci + +type Digest struct { + Sha256 string `json:"sha256"` +} + +type Material struct { + URI string `json:"uri"` + Digest Digest `json:"digest"` +} + +type ConfigSource struct { + EntryPoint string `json:"entryPoint"` +} + +type Args struct { + Cmdline string `json:"cmdline"` + Source string `json:"source"` +} + +type Local struct { + Name string `json:"name"` +} + +type Parameters struct { + Frontend string `json:"frontend"` + Args Args `json:"args"` + Locals []Local `json:"locals"` +} + +type Environment struct { + Platform string `json:"platform"` +} + +type Invocation struct { + ConfigSource ConfigSource `json:"configSource"` + Parameters Parameters `json:"parameters"` + Environment Environment `json:"environment"` +} + +type Provenance struct { + BuildType string `json:"buildType"` + Materials []Material `json:"materials"` + Invocation Invocation `json:"invocation"` +} + +type Platform struct { + Architecture string `json:"architecture"` + OS string `json:"os"` +} + +type ContainerImageDescriptor struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int `json:"size"` + Platform Platform `json:"platform"` +} + +type BuildInfo struct { + Provenance Provenance `json:"buildx.build.provenance"` + BuildRef string `json:"buildx.build.ref"` + ContainerImageConfigDigest string `json:"containerimage.config.digest"` + ContainerImageDescriptor ContainerImageDescriptor `json:"containerimage.descriptor"` + ContainerImageDigest string `json:"containerimage.digest"` + ImageName string `json:"image.name"` +} From 210e3c9ebb8dbc2851bdc4e95bc64ef793693f1e Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Tue, 13 Aug 2024 10:25:23 +0100 Subject: [PATCH 2/6] added changes to oci attestor and git attestor for remotes in subject Signed-off-by: chaosinthecrd --- attestation/git/git.go | 22 +++++++++++++++++++++- attestation/oci/oci.go | 20 ++++++++++++-------- go.mod | 1 + go.sum | 2 ++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/attestation/git/git.go b/attestation/git/git.go index 7f41d5b7..1a8b8b36 100644 --- a/attestation/git/git.go +++ b/attestation/git/git.go @@ -17,6 +17,7 @@ package git import ( "crypto" "fmt" + giturl "github.com/whilp/git-urls" "strings" "time" @@ -25,6 +26,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" "github.com/in-toto/go-witness/attestation" "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/log" "github.com/invopop/jsonschema" ) @@ -153,7 +155,15 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } for _, remote := range remotes { - a.Remotes = append(a.Remotes, remote.Config().URLs...) + for _, u := range remote.Config().URLs { + rurl, err := giturl.Parse(u) + if err != nil { + log.Debugf("failed to parse remote url: %w", err) + } + + //NOTE: Not added the scheme to the remote so far, can add it if needed + a.Remotes = append(a.Remotes, fmt.Sprintf("%s/%s", rurl.Host, rurl.Path)) + } } refs, err := repo.References() @@ -291,6 +301,16 @@ func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects[subjectName] = ds } + // add remotes + for _, remote := range a.Remotes { + subjectName = fmt.Sprintf("remote:%v", remote) + ds, err = cryptoutil.CalculateDigestSetFromBytes([]byte(remote), hashes) + if err != nil { + return nil + } + subjects[subjectName] = ds + } + // add refname short subjectName = fmt.Sprintf("refnameshort:%v", a.RefNameShort) ds, err = cryptoutil.CalculateDigestSetFromBytes([]byte(a.RefNameShort), hashes) diff --git a/attestation/oci/oci.go b/attestation/oci/oci.go index 63abd944..134dbdf4 100644 --- a/attestation/oci/oci.go +++ b/attestation/oci/oci.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "github.com/in-toto/go-witness/attestation" @@ -149,10 +150,15 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } if met != nil { + if strings.HasPrefix(met.ContainerImageDigest, "sha256:") { + log.Debugf("setting image digest as", met.ContainerImageDigest) + a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = met.ContainerImageDigest + } else { + log.Warnf("found metadata file does not contain image digest of expected format: '%s'", met.ContainerImageDigest) + } + a.ImageDigest = map[cryptoutil.DigestValue]string{} - a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = met.ContainerImageDigest - fmt.Println("setting image digest as", met.ContainerImageDigest) - fmt.Println("setting image references as", met.ImageName) + log.Debugf("setting image references as", met.ImageName) a.ImageReferences = []string{} a.ImageReferences = append(a.ImageReferences, met.ImageName) } @@ -169,12 +175,10 @@ func (a *Attestor) getDockerCandidate(ctx *attestation.AttestationContext) (*doc //NOTE: it's not ideal to try and parse it without a mime type but the metadata file is completely different depending on how the buildx is executed for path, product := range products { - fmt.Println("inspecting", path) if strings.Contains(sha256MimeType, product.MimeType) { - log.Info("found image id") - f, err := os.ReadFile(path) + f, err := os.ReadFile(filepath.Join(ctx.WorkingDir(), path)) if err != nil { - return nil, fmt.Errorf("failed to read file %s", path) + return nil, fmt.Errorf("failed to read file %s: %w", path, err) } a.ImageID = map[cryptoutil.DigestValue]string{} @@ -186,7 +190,7 @@ func (a *Attestor) getDockerCandidate(ctx *attestation.AttestationContext) (*doc f, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("failed to read file %s", path) + return nil, fmt.Errorf("failed to read file %s: %w", path, err) } err = json.Unmarshal(f, &met) diff --git a/go.mod b/go.mod index 53bbdf89..b67b7e15 100644 --- a/go.mod +++ b/go.mod @@ -104,6 +104,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/whilp/git-urls v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/zclconf/go-cty v1.14.4 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/go.sum b/go.sum index 5efb624a..2b239272 100644 --- a/go.sum +++ b/go.sum @@ -341,6 +341,8 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= +github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= From fe6e6b9cf345dcb599ad0ba31e1d4bec7924cb64 Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Wed, 14 Aug 2024 13:42:28 +0100 Subject: [PATCH 3/6] fixed empty map issue Signed-off-by: chaosinthecrd --- attestation/oci/oci.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/attestation/oci/oci.go b/attestation/oci/oci.go index 134dbdf4..5f67ac65 100644 --- a/attestation/oci/oci.go +++ b/attestation/oci/oci.go @@ -151,14 +151,14 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { if met != nil { if strings.HasPrefix(met.ContainerImageDigest, "sha256:") { - log.Debugf("setting image digest as", met.ContainerImageDigest) + log.Debugf("setting image digest as '%s'", met.ContainerImageDigest) + a.ImageDigest = map[cryptoutil.DigestValue]string{} a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = met.ContainerImageDigest } else { log.Warnf("found metadata file does not contain image digest of expected format: '%s'", met.ContainerImageDigest) } - a.ImageDigest = map[cryptoutil.DigestValue]string{} - log.Debugf("setting image references as", met.ImageName) + log.Debugf("setting image references as '%s'", met.ImageName) a.ImageReferences = []string{} a.ImageReferences = append(a.ImageReferences, met.ImageName) } From 7b1f08ba0884e11cf25a6ef21ac933c7e41ae77e Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Wed, 4 Sep 2024 11:40:02 +0100 Subject: [PATCH 4/6] trimming sha256: prefix from digest Signed-off-by: chaosinthecrd --- attestation/oci/oci.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/attestation/oci/oci.go b/attestation/oci/oci.go index 5f67ac65..781e981d 100644 --- a/attestation/oci/oci.go +++ b/attestation/oci/oci.go @@ -151,11 +151,19 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { if met != nil { if strings.HasPrefix(met.ContainerImageDigest, "sha256:") { - log.Debugf("setting image digest as '%s'", met.ContainerImageDigest) + log.Debugf("(attestation/oci) found image digest '%s'", met.ContainerImageDigest) a.ImageDigest = map[cryptoutil.DigestValue]string{} - a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = met.ContainerImageDigest + log.Debugf("(attestation/oci) removing 'sha256:' prefix from digest '%s'", met.ContainerImageDigest) + trimmed, found := strings.CutPrefix(met.ContainerImageDigest, "sha256:") + if found == false { + err := fmt.Errorf("failed to remove prefix from digest") + log.Debugf("(attestation/oci) %s", err.Error()) + return err + } + log.Debugf("(attestation/oci) setting image digest as '%s'", trimmed) + a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = trimmed } else { - log.Warnf("found metadata file does not contain image digest of expected format: '%s'", met.ContainerImageDigest) + log.Warnf("(attestation/oci) found metadata file does not contain image digest of expected format: '%s'", met.ContainerImageDigest) } log.Debugf("setting image references as '%s'", met.ImageName) From 472aaf05735287d7306983e7387a52cb5db221c3 Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Mon, 24 Feb 2025 18:20:53 +0000 Subject: [PATCH 5/6] updating oci attestor so docker materials are stored Signed-off-by: chaosinthecrd --- attestation/oci/oci.go | 157 +++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 76 deletions(-) diff --git a/attestation/oci/oci.go b/attestation/oci/oci.go index 781e981d..e9e5aa65 100644 --- a/attestation/oci/oci.go +++ b/attestation/oci/oci.go @@ -15,11 +15,9 @@ package oci import ( - "archive/tar" "crypto" "encoding/json" "fmt" - "io" "os" "path/filepath" "strings" @@ -67,7 +65,6 @@ func init() { type Attestor struct { Materials []Material `json:"materials"` ImageReferences []string `json:"imagereferences"` - ImageID cryptoutil.DigestSet `json:"imageid"` ImageDigest cryptoutil.DigestSet `json:"imagedigest"` } @@ -82,46 +79,6 @@ type Manifest struct { Layers []string `json:"Layers"` } -func (m *Manifest) getDockerImageID(ctx *attestation.AttestationContext, tarFilePath string) (cryptoutil.DigestSet, error) { - tarFile, err := os.Open(tarFilePath) - if err != nil { - return nil, err - } - defer tarFile.Close() - - tarReader := tar.NewReader(tarFile) - for { - h, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if h.FileInfo().IsDir() { - continue - } - - if h.Name == m.Config { - - b := make([]byte, h.Size) - _, err := tarReader.Read(b) - if err != nil && err != io.EOF { - return nil, err - } - - imageID, err := cryptoutil.CalculateDigestSetFromBytes(b, ctx.Hashes()) - if err != nil { - log.Debugf("(attestation/oci) error calculating image id: %w", err) - return nil, err - } - - return imageID, nil - } - } - return nil, fmt.Errorf("could not find config in tar file") -} - func New() *Attestor { return &Attestor{} } @@ -145,57 +102,106 @@ func (a *Attestor) Schema() *jsonschema.Schema { func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { met, err := a.getDockerCandidate(ctx) if err != nil { - log.Debugf("(attestation/oci) error getting candidate: %w", err) + log.Debugf("(attestation/oci) error getting docker candidate: %w", err) return err } - if met != nil { - if strings.HasPrefix(met.ContainerImageDigest, "sha256:") { - log.Debugf("(attestation/oci) found image digest '%s'", met.ContainerImageDigest) - a.ImageDigest = map[cryptoutil.DigestValue]string{} - log.Debugf("(attestation/oci) removing 'sha256:' prefix from digest '%s'", met.ContainerImageDigest) - trimmed, found := strings.CutPrefix(met.ContainerImageDigest, "sha256:") - if found == false { - err := fmt.Errorf("failed to remove prefix from digest") - log.Debugf("(attestation/oci) %s", err.Error()) - return err - } - log.Debugf("(attestation/oci) setting image digest as '%s'", trimmed) - a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = trimmed - } else { - log.Warnf("(attestation/oci) found metadata file does not contain image digest of expected format: '%s'", met.ContainerImageDigest) + err := a.setDockerCandidate(met) + if err != nil { + log.Debugf("(attestation/oci) error setting docker candidate: %w", err) + return err + } + } else { + // NOTE: our final attempt here is to try and find the sha256 image digest saved to a file. + // most tools provide the ability to do this (e.g., docker, podman), and if they don't other manual mechanisms could be + // established by a user + dig, err := a.getImageDigestFileCandidate(ctx) + if err != nil { + log.Debugf("(attestation/oci) error getting image digest from file: %w", err) + return err } - log.Debugf("setting image references as '%s'", met.ImageName) - a.ImageReferences = []string{} - a.ImageReferences = append(a.ImageReferences, met.ImageName) + trimmed, found := strings.CutPrefix(dig, "sha256:") + if found == false { + err := fmt.Errorf("failed to remove prefix from digest") + log.Debugf("(attestation/oci) %s", err.Error()) + return err + } + a.ImageDigest = map[cryptoutil.DigestValue]string{} + a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = trimmed } return nil } -func (a *Attestor) getDockerCandidate(ctx *attestation.AttestationContext) (*docker.BuildInfo, error) { - products := ctx.Products() +func (a *Attestor) setDockerCandidate(met *docker.BuildInfo) error { + if strings.HasPrefix(met.ContainerImageDigest, "sha256:") { + log.Debugf("(attestation/oci) found image digest '%s'", met.ContainerImageDigest) + a.ImageDigest = map[cryptoutil.DigestValue]string{} + log.Debugf("(attestation/oci) removing 'sha256:' prefix from digest '%s'", met.ContainerImageDigest) + trimmed, found := strings.CutPrefix(met.ContainerImageDigest, "sha256:") + if found == false { + err := fmt.Errorf("failed to remove prefix from digest") + log.Debugf("(attestation/oci) %s", err.Error()) + return err + } + log.Debugf("(attestation/oci) setting image digest as '%s'", trimmed) + a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}] = trimmed + } else { + log.Warnf("(attestation/oci) found metadata file does not contain image digest of expected format: '%s'", met.ContainerImageDigest) + } - if len(products) == 0 { - return nil, fmt.Errorf("no products to attest") + if len(met.Provenance.Materials) != 0 { + a.Materials = []Material{} + for _, material := range met.Provenance.Materials { + a.Materials = append(a.Materials, Material{URI: material.URI, Digest: cryptoutil.DigestSet{ + cryptoutil.DigestValue{crypto.SHA256, false}: material.Digest.Sha256, + }}) + } } - //NOTE: it's not ideal to try and parse it without a mime type but the metadata file is completely different depending on how the buildx is executed + // NOTE: we can get the builder architecture information from another attestor + // if plat := met.Provenance.Invocation.Environment.Platform; plat != "" { + // s := strings.Split(plat, "/") + // if len(s) != 2 { + // log.Warnf("(attestation/oci) docker buildx metadata `invocation.environment.platform` field '%s' not in expected `os/arch` fomat. skipping", plat) + // } + // + // a.Environment = met.Provenance.Invocation.Environment.Platform + // } + + log.Debugf("setting image references as '%s'", met.ImageName) + a.ImageReferences = []string{} + a.ImageReferences = append(a.ImageReferences, met.ImageName) + return nil +} + +func (a *Attestor) getImageDigestFileCandidate(ctx *attestation.AttestationContext) (string, error) { + products := ctx.Products() + for path, product := range products { if strings.Contains(sha256MimeType, product.MimeType) { f, err := os.ReadFile(filepath.Join(ctx.WorkingDir(), path)) if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", path, err) + return "", fmt.Errorf("failed to read file %s: %w", path, err) } - - a.ImageID = map[cryptoutil.DigestValue]string{} - a.ImageID[cryptoutil.DigestValue{Hash: crypto.SHA256}] = string(f) - continue + return string(f), nil } + } - var met docker.BuildInfo + return "", nil +} +func (a *Attestor) getDockerCandidate(ctx *attestation.AttestationContext) (*docker.BuildInfo, error) { + products := ctx.Products() + + if len(products) == 0 { + return nil, fmt.Errorf("no products to attest") + } + + //NOTE: it's not ideal to try and parse it without a mime type but the metadata file is completely different depending on how the buildx is executed + for path, _ := range products { + var met docker.BuildInfo f, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read file %s: %w", path, err) @@ -211,15 +217,14 @@ func (a *Attestor) getDockerCandidate(ctx *attestation.AttestationContext) (*doc return &met, nil } + return nil, nil } -// TODO Needs finished, some of these are wrongly configured func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { hashes := []cryptoutil.DigestValue{{Hash: crypto.SHA256}} subj := make(map[string]cryptoutil.DigestSet) subj[fmt.Sprintf("imagedigest:%s", a.ImageDigest[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.ImageDigest - subj[fmt.Sprintf("imageid:%s", a.ImageID[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = a.ImageID for _, ir := range a.ImageReferences { if hash, err := cryptoutil.CalculateDigestSetFromBytes([]byte(ir), hashes); err == nil { @@ -230,7 +235,7 @@ func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { } for _, m := range a.Materials { - subj[fmt.Sprintf("materialdigest:%s", m.Digest)] = m.Digest + subj[fmt.Sprintf("materialdigest:%s", m.Digest[cryptoutil.DigestValue{Hash: crypto.SHA256}])] = m.Digest if hash, err := cryptoutil.CalculateDigestSetFromBytes([]byte(m.URI), hashes); err == nil { subj[fmt.Sprintf("materialuri:%s", m.URI)] = hash } else { From 96944f16486ad74f8b7b901a05e7e4957b75882d Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Tue, 25 Feb 2025 13:28:31 +0000 Subject: [PATCH 6/6] chore: removing changes to git attestor (should not be in this pr) Signed-off-by: chaosinthecrd --- attestation/git/git.go | 65 ++++++++++++++++++++++++++---------------- go.mod | 1 - go.sum | 2 -- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/attestation/git/git.go b/attestation/git/git.go index 1a8b8b36..e61a0af6 100644 --- a/attestation/git/git.go +++ b/attestation/git/git.go @@ -17,7 +17,6 @@ package git import ( "crypto" "fmt" - giturl "github.com/whilp/git-urls" "strings" "time" @@ -26,7 +25,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" "github.com/in-toto/go-witness/attestation" "github.com/in-toto/go-witness/cryptoutil" - "github.com/in-toto/go-witness/log" "github.com/invopop/jsonschema" ) @@ -81,6 +79,9 @@ type Tag struct { } type Attestor struct { + GitTool string `json:"gittool"` + GitBinPath string `json:"gitbinpath,omitempty"` + GitBinHash cryptoutil.DigestSet `json:"gitbinhash,omitempty"` CommitHash string `json:"commithash"` Author string `json:"author"` AuthorEmail string `json:"authoremail"` @@ -155,15 +156,7 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { } for _, remote := range remotes { - for _, u := range remote.Config().URLs { - rurl, err := giturl.Parse(u) - if err != nil { - log.Debugf("failed to parse remote url: %w", err) - } - - //NOTE: Not added the scheme to the remote so far, can add it if needed - a.Remotes = append(a.Remotes, fmt.Sprintf("%s/%s", rurl.Host, rurl.Path)) - } + a.Remotes = append(a.Remotes, remote.Config().URLs...) } refs, err := repo.References() @@ -231,14 +224,46 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { a.TreeHash = commit.TreeHash.String() + if GitExists() { + a.GitTool = "go-git+git-bin" + + a.GitBinPath, err = GitGetBinPath() + if err != nil { + return err + } + + a.GitBinHash, err = GitGetBinHash(ctx) + if err != nil { + return err + } + + a.Status, err = GitGetStatus(ctx.WorkingDir()) + if err != nil { + return err + } + } else { + a.GitTool = "go-git" + + a.Status, err = GoGitGetStatus(repo) + if err != nil { + return err + } + } + + return nil +} + +func GoGitGetStatus(repo *git.Repository) (map[string]Status, error) { + var gitStatuses map[string]Status = make(map[string]Status) + worktree, err := repo.Worktree() if err != nil { - return err + return map[string]Status{}, err } status, err := worktree.Status() if err != nil { - return err + return map[string]Status{}, err } for file, status := range status { @@ -251,10 +276,10 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { Staging: statusCodeString(status.Staging), } - a.Status[file] = attestStatus + gitStatuses[file] = attestStatus } - return nil + return gitStatuses, nil } func (a *Attestor) Data() *Attestor { @@ -301,16 +326,6 @@ func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects[subjectName] = ds } - // add remotes - for _, remote := range a.Remotes { - subjectName = fmt.Sprintf("remote:%v", remote) - ds, err = cryptoutil.CalculateDigestSetFromBytes([]byte(remote), hashes) - if err != nil { - return nil - } - subjects[subjectName] = ds - } - // add refname short subjectName = fmt.Sprintf("refnameshort:%v", a.RefNameShort) ds, err = cryptoutil.CalculateDigestSetFromBytes([]byte(a.RefNameShort), hashes) diff --git a/go.mod b/go.mod index b67b7e15..53bbdf89 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,6 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect - github.com/whilp/git-urls v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/zclconf/go-cty v1.14.4 // indirect go.opencensus.io v0.24.0 // indirect diff --git a/go.sum b/go.sum index 2b239272..5efb624a 100644 --- a/go.sum +++ b/go.sum @@ -341,8 +341,6 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= -github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=