Files
attest/pkg/attestation/attestation.go
James Carnegie 0038e3d23d Unify functions for use in sign & verify --vsa (#71)
* Use receivers for manifest functions
* Move SaveImage/SaveIndex from image-signing-verifier
* Ignore test fixtures in coverage
* Add AddImagesToIndex function
2024-07-05 09:29:14 +01:00

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
}