* Use receivers for manifest functions * Move SaveImage/SaveIndex from image-signing-verifier * Ignore test fixtures in coverage * Add AddImagesToIndex function
252 lines
8.3 KiB
Go
252 lines
8.3 KiB
Go
package attestation
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
|
"github.com/google/go-containerregistry/pkg/v1/match"
|
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
|
"github.com/google/go-containerregistry/pkg/v1/partial"
|
|
"github.com/google/go-containerregistry/pkg/v1/static"
|
|
"github.com/google/go-containerregistry/pkg/v1/types"
|
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
|
)
|
|
|
|
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index
|
|
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManifest, error) {
|
|
idx, err := index.IndexManifest()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
|
}
|
|
subjects := make(map[string]*v1.Descriptor)
|
|
for _, subject := range idx.Manifests {
|
|
subjects[subject.Digest.String()] = &subject
|
|
}
|
|
|
|
var attestationManifests []*AttestationManifest
|
|
for _, manifest := range idx.Manifests {
|
|
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
|
|
subject := subjects[manifest.Annotations[DockerReferenceDigest]]
|
|
if subject == nil {
|
|
return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err)
|
|
}
|
|
attestationImage, err := index.Image(manifest.Digest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
|
|
}
|
|
attestationLayers, err := GetAttestationsFromImage(attestationImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
|
}
|
|
attestationManifests = append(attestationManifests,
|
|
&AttestationManifest{
|
|
Descriptor: &manifest,
|
|
SubjectDescriptor: subject,
|
|
Attestation: &AttestationImage{
|
|
Layers: attestationLayers,
|
|
Image: attestationImage},
|
|
MediaType: manifest.MediaType,
|
|
Annotations: manifest.Annotations,
|
|
Digest: manifest.Digest})
|
|
}
|
|
}
|
|
return attestationManifests, nil
|
|
}
|
|
|
|
// GetAttestationsFromImage extracts all attestation layers from an image
|
|
func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
|
|
layers, err := image.Layers()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract layers from image: %w", err)
|
|
}
|
|
var attestationLayers []*AttestationLayer
|
|
for _, layer := range layers {
|
|
// parse layer blob as json
|
|
r, err := layer.Uncompressed()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
|
}
|
|
defer r.Close()
|
|
mt, err := layer.MediaType()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
|
}
|
|
layerDesc, err := partial.Descriptor(layer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
|
}
|
|
// copy original annotations
|
|
ann := maps.Clone(layerDesc.Annotations)
|
|
// only decode intoto statements
|
|
var stmt = new(intoto.Statement)
|
|
if mt == types.MediaType(intoto.PayloadType) {
|
|
err = json.NewDecoder(r).Decode(&stmt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode statement layer contents: %w", err)
|
|
}
|
|
}
|
|
attestationLayers = append(attestationLayers, &AttestationLayer{Layer: layer, MediaType: mt, Statement: stmt, Annotations: ann})
|
|
}
|
|
return attestationLayers, nil
|
|
}
|
|
|
|
func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
|
layer, err := createSignedImageLayer(ctx, statement, signer, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create signed layer: %w", err)
|
|
}
|
|
newImg, newDesc, err := addLayerToImage(manifest, layer, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add signed layers to image: %w", err)
|
|
}
|
|
manifest.Attestation.Image = newImg
|
|
manifest.Descriptor = newDesc
|
|
return nil
|
|
}
|
|
|
|
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) {
|
|
|
|
// sign the statement
|
|
env, err := SignInTotoStatement(ctx, statement, signer, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
|
}
|
|
|
|
mediaType, err := DSSEMediaType(statement.PredicateType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
|
|
}
|
|
data, err := json.Marshal(env)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
|
|
}
|
|
return &AttestationLayer{
|
|
Statement: statement,
|
|
MediaType: types.MediaType(intoto.PayloadType),
|
|
Annotations: map[string]string{
|
|
InTotoPredicateType: statement.PredicateType,
|
|
InTotoReferenceLifecycleStage: LifecycleStageExperimental,
|
|
},
|
|
Layer: static.NewLayer(data, types.MediaType(mediaType)),
|
|
}, nil
|
|
}
|
|
|
|
func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
|
payload, err := json.Marshal(statement)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
|
}
|
|
env, err := SignDSSE(ctx, payload, signer, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
func addLayerToImage(
|
|
manifest *AttestationManifest,
|
|
layer *AttestationLayer,
|
|
opts *SigningOptions) (v1.Image, *v1.Descriptor, error) {
|
|
|
|
err := manifest.AddOrReplaceLayer(layer, opts)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to add signed layers: %w", err)
|
|
}
|
|
newImg := manifest.Attestation.Image
|
|
if !opts.SkipSubject {
|
|
newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image)
|
|
}
|
|
newDesc, err := partial.Descriptor(newImg)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get descriptor: %w", err)
|
|
}
|
|
cf, err := manifest.Attestation.Image.ConfigFile()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get config file: %w", err)
|
|
}
|
|
newDesc.Platform = cf.Platform()
|
|
if newDesc.Platform == nil {
|
|
newDesc.Platform = &v1.Platform{
|
|
Architecture: "unknown",
|
|
OS: "unknown",
|
|
}
|
|
}
|
|
newDesc.MediaType = manifest.MediaType
|
|
newDesc.Annotations = manifest.Annotations
|
|
return newImg, newDesc, nil
|
|
}
|
|
|
|
// AddOrReplaceLayer adds signed layers to a new or existing attestation image
|
|
// NOTE: the pointers attestation.AttestationLayer.Statement are compared when replacing,
|
|
// so make sure you are signing a layer extracted from the original attestation-manifest image!
|
|
func (manifest *AttestationManifest) AddOrReplaceLayer(signedLayer *AttestationLayer, opts *SigningOptions) error {
|
|
var err error
|
|
// always create a new image from all the layers
|
|
newImg := empty.Image
|
|
newImg = mutate.Annotations(newImg, map[string]string{
|
|
DockerReferenceType: AttestationManifestType,
|
|
DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(),
|
|
}).(v1.Image)
|
|
|
|
newImg = mutate.MediaType(newImg, manifest.MediaType)
|
|
newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json")
|
|
add := mutate.Addendum{
|
|
Layer: signedLayer.Layer,
|
|
Annotations: signedLayer.Annotations,
|
|
}
|
|
newImg, err = mutate.Append(newImg, add)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add signed layer to image: %w", err)
|
|
}
|
|
layers := make([]*AttestationLayer, 0)
|
|
for _, layer := range manifest.Attestation.Layers {
|
|
if layer.Statement == signedLayer.Statement && opts.Replace {
|
|
continue
|
|
}
|
|
add := mutate.Addendum{
|
|
Layer: layer.Layer,
|
|
Annotations: layer.Annotations,
|
|
}
|
|
newImg, err = mutate.Append(newImg, add)
|
|
layers = append(layers, layer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add layer to image: %w", err)
|
|
}
|
|
}
|
|
manifest.Attestation.Layers = append(layers, signedLayer)
|
|
manifest.Attestation.Image = newImg
|
|
return nil
|
|
}
|
|
|
|
func AddImageToIndex(
|
|
idx v1.ImageIndex,
|
|
manifest *AttestationManifest,
|
|
) (v1.ImageIndex, error) {
|
|
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
|
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
|
Add: manifest.Attestation.Image,
|
|
Descriptor: *manifest.Descriptor,
|
|
})
|
|
return idx, nil
|
|
}
|
|
|
|
func AddImagesToIndex(
|
|
idx v1.ImageIndex,
|
|
manifests []*AttestationManifest,
|
|
) (v1.ImageIndex, error) {
|
|
for _, manifest := range manifests {
|
|
var err error
|
|
idx, err = AddImageToIndex(idx, manifest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to add image to index: %w", err)
|
|
}
|
|
}
|
|
return idx, nil
|
|
}
|