From 80f72a0059a05a6d4a284a9054797b5b1eb9193b Mon Sep 17 00:00:00 2001 From: mrjoelkamp Date: Tue, 30 Apr 2024 12:23:07 -0500 Subject: [PATCH] refactor: SignIndexAttestations --- pkg/attest/sign.go | 184 +++++++++++++-------------------- pkg/attest/sign_test.go | 15 ++- pkg/attest/vsa.go | 30 +++--- pkg/attestation/attestation.go | 82 +++++++++++++++ pkg/attestation/types.go | 24 +++++ 5 files changed, 204 insertions(+), 131 deletions(-) create mode 100644 pkg/attestation/attestation.go diff --git a/pkg/attest/sign.go b/pkg/attest/sign.go index ea8846a..a554e6e 100644 --- a/pkg/attest/sign.go +++ b/pkg/attest/sign.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/oci" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/match" @@ -19,159 +18,122 @@ import ( ) func SignIndexAttestations(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *SigningOptions) (v1.ImageIndex, error) { - indexManifest, err := idx.IndexManifest() + // extract attestation manifests from index + attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx) if err != nil { - return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err) + return nil, fmt.Errorf("failed to get attestation manifests: %w", err) } - var originalManifestDigests []v1.Hash - var muts []mutate.IndexAddendum - for _, manifest := range indexManifest.Manifests { - if manifest.Annotations[oci.DockerReferenceType] != oci.AttestationManifestType { - continue - } - - originalManifestDigests = append(originalManifestDigests, manifest.Digest) - - attestationImage, err := idx.Image(manifest.Digest) + // sign every attestation layer in each manifest + for _, manifest := range attestationManifests { + attestationLayers, err := attestation.GetAttestationsFromImage(manifest.Attestation.Image) if err != nil { - return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err) + return nil, fmt.Errorf("failed to get attestations from image: %w", err) } - layers, err := attestationImage.Layers() + signedLayers, err := signLayers(ctx, attestationLayers, signer) if err != nil { - return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err) + return nil, fmt.Errorf("failed to sign attestations: %w", err) } - - var signedLayers []mutate.Addendum - var originalLayers []v1.Layer - var statements []*intoto.Statement - - 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) - } - - if mt != types.MediaType(intoto.PayloadType) { - originalLayers = append(originalLayers, layer) - continue - } - var stmt = new(intoto.Statement) - err = json.NewDecoder(r).Decode(&stmt) - if err != nil { - return nil, fmt.Errorf("failed to decode statement layer contents: %w", err) - } - - statements = append(statements, stmt) - layerDesc, err := partial.Descriptor(layer) - if err != nil { - return nil, fmt.Errorf("failed to get descriptor for layer: %w", err) - } - // copy original annotations and add new ones - ann := make(map[string]string) - for k, v := range layerDesc.Annotations { - ann[k] = v - } - ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental - - payload, err := json.Marshal(stmt) - if err != nil { - return nil, fmt.Errorf("failed to marshal statement: %w", err) - } - env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) - if err != nil { - return nil, fmt.Errorf("failed to sign statement: %w", err) - } - mediaType, err := attestation.DSSEMediaType(stmt.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) - } - newLayer := static.NewLayer(data, types.MediaType(mediaType)) - - withAnnotations := mutate.Addendum{ - Layer: newLayer, - Annotations: ann, - } - signedLayers = append(signedLayers, withAnnotations) - } - - newImg, err := addSignedLayers(signedLayers, originalLayers, manifest.MediaType, attestationImage, opts) - if err != nil { - return nil, fmt.Errorf("failed to add signed layers: %w", err) - } - if opts.VSAOptions != nil { - newLayer, err := generateVSA(ctx, newImg, statements, signer, opts) + newLayer, err := generateVSA(ctx, manifest, signer, opts) if err != nil { return nil, fmt.Errorf("failed to generate VSA: %w", err) } - vsaReplace := &SigningOptions{ - Replace: false, - } - newImg, err = addSignedLayers([]mutate.Addendum{*newLayer}, layers, manifest.MediaType, newImg, vsaReplace) - if err != nil { - return nil, fmt.Errorf("failed to add VSA layer: %w", err) - } + signedLayers = append(signedLayers, *newLayer) + } + newImg, err := addSignedLayers(signedLayers, manifest, opts) + if err != nil { + return nil, fmt.Errorf("failed to add signed layers: %w", err) } newDesc, err := partial.Descriptor(newImg) if err != nil { return nil, fmt.Errorf("failed to get descriptor: %w", err) } - cf, err := attestationImage.ConfigFile() + cf, err := manifest.Attestation.Image.ConfigFile() if err != nil { return nil, fmt.Errorf("failed to get config file: %w", err) } newDesc.Platform = cf.Platform() newDesc.MediaType = manifest.MediaType newDesc.Annotations = manifest.Annotations - - muts = append(muts, mutate.IndexAddendum{ + idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest)) + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ Add: newImg, Descriptor: *newDesc, }) } - // create new index with signed images - newIndex := mutate.RemoveManifests(idx, match.Digests(originalManifestDigests...)) - newIndex = mutate.AppendManifests(newIndex, muts...) - - return newIndex, nil + return idx, nil } -func addSignedLayers(signedLayers []mutate.Addendum, originalLayers []v1.Layer, mediaType types.MediaType, attestationImage v1.Image, opts *SigningOptions) (v1.Image, error) { +// signLayers signs each intoto attestation layer with the given signer +func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier) ([]mutate.Addendum, error) { + var signedLayers []mutate.Addendum + for _, layer := range layers { + // only sign intoto layers + if layer.MediaType != types.MediaType(intoto.PayloadType) { + continue + } + // mark attestation as experimental + layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental + + // sign the statement + payload, err := json.Marshal(layer.Statement) + if err != nil { + return nil, fmt.Errorf("failed to marshal statement: %w", err) + } + env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer) + if err != nil { + return nil, fmt.Errorf("failed to sign statement: %w", err) + } + mediaType, err := attestation.DSSEMediaType(layer.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) + } + newLayer := static.NewLayer(data, types.MediaType(mediaType)) + withAnnotations := mutate.Addendum{ + Layer: newLayer, + Annotations: layer.Annotations, + } + signedLayers = append(signedLayers, withAnnotations) + } + return signedLayers, nil +} + +// addSignedLayers adds signed layers to a new or existing attestation image +func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *SigningOptions) (v1.Image, error) { var err error if opts.Replace { + // create a new attestation image with only signed layers newImg := empty.Image - newImg = mutate.MediaType(newImg, mediaType) + newImg = mutate.MediaType(newImg, manifest.MediaType) newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json") for _, layer := range signedLayers { newImg, err = mutate.Append(newImg, layer) if err != nil { - return nil, fmt.Errorf("failed to append layer: %w", err) + return nil, fmt.Errorf("failed to append signed layer: %w", err) } } - newImg, err = mutate.AppendLayers(newImg, originalLayers...) - if err != nil { - return nil, fmt.Errorf("failed to append original layers: %w", err) + // add any existing unsigned (non-intoto) layers to the new image + for _, layer := range manifest.Attestation.Layers { + if layer.MediaType != types.MediaType(intoto.PayloadType) { + newImg, err = mutate.AppendLayers(newImg, layer.Layer) + if err != nil { + return nil, fmt.Errorf("failed to append unsigned layer: %w", err) + } + } } return newImg, nil - } + // Add signed layers to the existing image for _, layer := range signedLayers { - attestationImage, err = mutate.Append(attestationImage, layer) + manifest.Attestation.Image, err = mutate.Append(manifest.Attestation.Image, layer) if err != nil { return nil, fmt.Errorf("failed to append layer: %w", err) } } - return attestationImage, nil + return manifest.Attestation.Image, nil } diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index c8b726d..bff0f57 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -131,11 +131,22 @@ func TestAddSignedLayerAnnotations(t *testing.T) { data = []byte("test") testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType)) mediaType := types.OCIManifestSchema1 - img := empty.Image opts := &SigningOptions{ Replace: tc.replace, } - newImg, err := addSignedLayers(signedLayers, []v1.Layer{testLayer}, mediaType, img, opts) + manifest := attestation.AttestationManifest{ + MediaType: mediaType, + Attestation: attestation.AttestationImage{ + Image: empty.Image, + Layers: []attestation.AttestationLayer{ + { + Layer: testLayer, + Statement: &intoto.Statement{}, + }, + }, + }, + } + newImg, err := addSignedLayers(signedLayers, manifest, opts) assert.NoError(t, err) mf, _ := newImg.RawManifest() type Annotations struct { diff --git a/pkg/attest/vsa.go b/pkg/attest/vsa.go index 8343002..015ea0a 100644 --- a/pkg/attest/vsa.go +++ b/pkg/attest/vsa.go @@ -9,7 +9,6 @@ import ( "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/oci" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/static" "github.com/google/go-containerregistry/pkg/v1/types" @@ -17,35 +16,30 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/dsse" ) -func generateVSA(ctx context.Context, image v1.Image, stmt []*intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*mutate.Addendum, error) { - if len(stmt) == 0 { +// generateVSA generates a VSA from the attestation manifest +// TODO: remove signing logic and move generateVSA to attestation/vsa.go +func generateVSA(ctx context.Context, manifest attestation.AttestationManifest, signer dsse.SignerVerifier, opts *SigningOptions) (*mutate.Addendum, error) { + if len(manifest.Attestation.Layers) == 0 { return nil, fmt.Errorf("no attestations found to generate VSA from") } - sub := stmt[0].Subject[0] - stype := stmt[0].Type + sub := manifest.Attestation.Layers[0].Statement.Subject[0] + stype := manifest.Attestation.Layers[0].Statement.Type uri, err := attestation.ToVSAResourceURI(sub) if err != nil { return nil, fmt.Errorf("failed to generate VSA resource URI: %w", err) } - inputs := make([]attestation.VSAInputAttestation, 0, len(stmt)) - layers, err := image.Layers() - if err != nil { - return nil, fmt.Errorf("failed to get layers: %w", err) - } - for _, layer := range layers { - mt, err := layer.MediaType() + inputs := make([]attestation.VSAInputAttestation, 0, len(manifest.Attestation.Layers)) + for _, att := range manifest.Attestation.Layers { + mt, err := att.Layer.MediaType() if err != nil { return nil, fmt.Errorf("failed to get layer media type: %w", err) } - mediaType := string(mt) - if !strings.HasPrefix(mediaType, "application/vnd.in-toto.") || - !strings.HasSuffix(mediaType, "+dsse") { + if !strings.HasSuffix(string(mt), "+dsse") { continue } - - dgst, err := layer.Digest() + dgst, err := att.Layer.Digest() if err != nil { return nil, fmt.Errorf("failed to get layer digest: %w", err) } @@ -58,7 +52,7 @@ func generateVSA(ctx context.Context, image v1.Image, stmt []*intoto.Statement, StatementHeader: intoto.StatementHeader{ PredicateType: attestation.VSAPredicateType, Type: stype, - Subject: stmt[0].Subject, + Subject: manifest.Attestation.Layers[0].Statement.Subject, }, Predicate: attestation.VSAPredicate{ Verifier: attestation.VSAVerifier{ diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go new file mode 100644 index 0000000..81e3f5e --- /dev/null +++ b/pkg/attestation/attestation.go @@ -0,0 +1,82 @@ +package attestation + +import ( + "encoding/json" + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" + intoto "github.com/in-toto/in-toto-golang/in_toto" +) + +// 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) + } + var attestationManifests []AttestationManifest + for _, manifest := range idx.Manifests { + if manifest.Annotations[DockerReferenceType] == AttestationManifestType { + 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{ + Manifest: manifest, + 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 := make(map[string]string) + for k, v := range layerDesc.Annotations { + ann[k] = v + } + // 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 +} diff --git a/pkg/attestation/types.go b/pkg/attestation/types.go index 54068b5..5ee6717 100644 --- a/pkg/attestation/types.go +++ b/pkg/attestation/types.go @@ -4,12 +4,16 @@ import ( "encoding/base64" "fmt" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" intoto "github.com/in-toto/in-toto-golang/in_toto" v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" ) const ( + DockerReferenceType = "vnd.docker.reference.type" + AttestationManifestType = "attestation-manifest" DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json" RekorTlExtKind = "Rekor" OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse" @@ -17,6 +21,26 @@ const ( var base64Encoding = base64.StdEncoding.Strict() +type AttestationLayer struct { + Statement *intoto.Statement + Layer v1.Layer + MediaType types.MediaType + Annotations map[string]string +} + +type AttestationImage struct { + Layers []AttestationLayer + Image v1.Image +} + +type AttestationManifest struct { + Manifest v1.Descriptor + Attestation AttestationImage + MediaType types.MediaType + Annotations map[string]string + Digest v1.Hash +} + // the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged type Envelope struct { PayloadType string `json:"payloadType"`