Skip to content

Commit

Permalink
Add support for signing SBOMs
Browse files Browse the repository at this point in the history
The idea is a TaskRun produces an unsigned SBOM and pushes it to the
OCI registry. The reference to the SBOM is then added to the result
IMAGE_SBOM_URL. When Chains sees this result, it downloads the SBOM from
the OCI registry, signs it, then adds to the image attestations with an
SBOM-specific predicate. IMAGE_SBOM_FORMAT is used to specify the
predicate to list in the attestation.

This is also possible when using the IMAGES result, and the
corresponding SBOMS result. Both must have the exact number of items.
When using the SBOMS result, SBOMS_FORMAT is also required to specify
the SBOM format for all the items in SBOMS.

Signed-off-by: Luiz Carvalho <lucarval@redhat.com>
  • Loading branch information
lcarva committed Apr 19, 2023
1 parent 3cff9a4 commit a52b8b3
Show file tree
Hide file tree
Showing 18 changed files with 1,129 additions and 23 deletions.
153 changes: 148 additions & 5 deletions pkg/artifacts/signable.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ type image struct {
// URI is the resource uri for the target needed iff the target is a material.
// Digest is the target's SHA digest.
type StructuredSignable struct {
URI string
Digest string
URI string
Digest string
SBOMURI string
SBOMFormat string
}

func (oa *OCIArtifact) ExtractObjects(ctx context.Context, obj objects.TektonObject) []interface{} {
Expand Down Expand Up @@ -201,7 +203,7 @@ func (oa *OCIArtifact) ExtractObjects(ctx context.Context, obj objects.TektonObj
func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject) []interface{} {
logger := logging.FromContext(ctx)
objs := []interface{}{}
ss := extractTargetFromResults(ctx, obj, "IMAGE_URL", "IMAGE_DIGEST")
ss := extractTargetFromResults(ctx, obj, "IMAGE_URL", "IMAGE_DIGEST", "", "")
for _, s := range ss {
if s == nil || s.Digest == "" || s.URI == "" {
continue
Expand Down Expand Up @@ -242,7 +244,7 @@ func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject)
func ExtractSignableTargetFromResults(ctx context.Context, obj objects.TektonObject) []*StructuredSignable {
logger := logging.FromContext(ctx)
objs := []*StructuredSignable{}
ss := extractTargetFromResults(ctx, obj, "ARTIFACT_URI", "ARTIFACT_DIGEST")
ss := extractTargetFromResults(ctx, obj, "ARTIFACT_URI", "ARTIFACT_DIGEST", "", "")
// Only add it if we got both the signable URI and digest.
for _, s := range ss {
if s == nil || s.Digest == "" || s.URI == "" {
Expand All @@ -264,7 +266,7 @@ func (s *StructuredSignable) FullRef() string {
return fmt.Sprintf("%s@%s", s.URI, s.Digest)
}

func extractTargetFromResults(ctx context.Context, obj objects.TektonObject, identifierSuffix string, digestSuffix string) map[string]*StructuredSignable {
func extractTargetFromResults(ctx context.Context, obj objects.TektonObject, identifierSuffix, digestSuffix, sbomSuffix, sbomFormatSuffix string) map[string]*StructuredSignable {
logger := logging.FromContext(ctx)
ss := map[string]*StructuredSignable{}

Expand Down Expand Up @@ -295,6 +297,31 @@ func extractTargetFromResults(ctx context.Context, obj objects.TektonObject, ide
ss[marker] = &StructuredSignable{Digest: strings.TrimSpace(res.Value.StringVal)}
}
}
if sbomSuffix != "" && strings.HasSuffix(res.Name, sbomSuffix) {
if res.Value.StringVal == "" {
logger.Debugf("error getting string value for %s", res.Name)
continue
}
marker := strings.TrimSuffix(res.Name, sbomSuffix)
sbomURI := strings.TrimSpace(res.Value.StringVal)
if v, ok := ss[marker]; ok {
v.SBOMURI = sbomURI
} else {
ss[marker] = &StructuredSignable{SBOMURI: sbomURI}
}
}
if sbomFormatSuffix != "" && strings.HasSuffix(res.Name, sbomFormatSuffix) {
if res.Value.StringVal == "" {
logger.Debugf("error getting string value for %s", res.Name)
}
marker := strings.TrimSuffix(res.Name, sbomFormatSuffix)
sbomFormat := strings.TrimSpace(res.Value.StringVal)
if v, ok := ss[marker]; ok {
v.SBOMFormat = sbomFormat
} else {
ss[marker] = &StructuredSignable{SBOMFormat: sbomFormat}
}
}

}
return ss
Expand Down Expand Up @@ -435,3 +462,119 @@ func (oa *OCIArtifact) FullKey(obj interface{}) string {
func (oa *OCIArtifact) Enabled(cfg config.Config) bool {
return cfg.Artifacts.OCI.Enabled()
}

type SBOMArtifact struct{}

var _ Signable = &SBOMArtifact{}

func (sa *SBOMArtifact) ExtractObjects(ctx context.Context, tektonObject objects.TektonObject) []interface{} {
var objs []interface{}
for _, obj := range extractSBOMFromResults(ctx, tektonObject) {
objs = append(objs, objects.NewSBOMObject(obj.SBOMURI, obj.SBOMFormat, obj.URI, obj.Digest, tektonObject))
}
return objs
}

func (sa *SBOMArtifact) StorageBackend(cfg config.Config) sets.Set[string] {
return cfg.Artifacts.SBOM.StorageBackend
}
func (sa *SBOMArtifact) Signer(cfg config.Config) string {
return cfg.Artifacts.SBOM.Signer
}
func (sa *SBOMArtifact) PayloadFormat(cfg config.Config) config.PayloadType {
return config.PayloadType(cfg.Artifacts.SBOM.Format)
}

func (sa *SBOMArtifact) FullKey(obj interface{}) string {
v := obj.(*objects.SBOMObject)
return v.GetSBOMURL()
}

func (sa *SBOMArtifact) ShortKey(obj interface{}) string {
v := obj.(*objects.SBOMObject)
return v.GetSBOMURL()
}
func (sa *SBOMArtifact) Type() string {
return "sbom"
}
func (sa *SBOMArtifact) Enabled(cfg config.Config) bool {
return cfg.Artifacts.SBOM.Enabled()
}

func extractSBOMFromResults(ctx context.Context, tektonObject objects.TektonObject) []*StructuredSignable {
logger := logging.FromContext(ctx)
var objs []*StructuredSignable

ss := extractTargetFromResults(ctx, tektonObject, "IMAGE_URL", "IMAGE_DIGEST", "IMAGE_SBOM_URL", "IMAGE_SBOM_FORMAT")
for _, s := range ss {
if s == nil || s.Digest == "" || s.URI == "" || s.SBOMURI == "" || s.SBOMFormat == "" {
continue
}
if _, err := name.NewDigest(s.SBOMURI); err != nil {
logger.Errorf("error getting digest for SBOM image %s: %v", s.SBOMURI, err)
continue
}
objs = append(objs, s)
}

var images []name.Digest
var sboms []string
var sbomsFormat string
// look for a comma separated list of images and their SBOMs
for _, key := range tektonObject.GetResults() {
switch key.Name {
case "IMAGES":
for _, img := range strings.FieldsFunc(key.Value.StringVal, split) {
trimmed := strings.TrimSpace(img)
if trimmed == "" {
continue
}

dgst, err := name.NewDigest(trimmed)
if err != nil {
logger.Errorf("error getting digest for img %s: %v", trimmed, err)
continue
}
images = append(images, dgst)
}
case "SBOMS":
for _, img := range strings.FieldsFunc(key.Value.StringVal, split) {
trimmed := strings.TrimSpace(img)
if trimmed == "" {
continue
}
if _, err := name.NewDigest(trimmed); err != nil {
logger.Errorf("error getting digest for SBOM image %s: %v", trimmed, err)
continue
}
sboms = append(sboms, trimmed)
}
case "SBOMS_FORMAT":
f := strings.TrimSpace(key.Value.StringVal)
if f != "" {
sbomsFormat = f
}
}
}

if len(images) != len(sboms) {
logger.Warnf("IMAGES and SBOMS do not contain the same amount of entries")
return objs
}

if len(sboms) > 0 && sbomsFormat == "" {
logger.Warnf("SBOMS_FORMAT not specified")
return objs
}

for i, sbom := range sboms {
img := images[i]
objs = append(objs, &StructuredSignable{
URI: img.Name(),
Digest: img.Identifier(),
SBOMURI: sbom,
SBOMFormat: sbomsFormat,
})
}
return objs
}
Loading

0 comments on commit a52b8b3

Please sign in to comment.