diff --git a/README.md b/README.md index d8d9b02..fe262f7 100644 --- a/README.md +++ b/README.md @@ -3,128 +3,13 @@ library to create, verify, and evaluate policy for attestations on container ima # usage ## verifying attestations -1. create a TUF client - * using OCI registry for TUF - ```go - tufOutputPath = "/.docker/tuf" - metadataURI = "docker/tuf-metadata:latest" - targetsURI = "docker/tuf-targets" - tufClient, err := tuf.NewTufClient(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI) - ``` - * using HTTPS for TUF - ```go - tufOutputPath = "/.docker/tuf" - metadataURI = "https://docker.github.io/tuf/metadata" - targetsURI = "https://docker.github.io/tuf/targets" - tufClient, err := tuf.NewTufClient(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI) - ``` - -1. configure an attestation resolver - * using OCI registry - ```go - var resolver oci.AttestationResolver - resolver = &oci.RegistryResolver{ - Image: image, // path to image index in OCI registry containing image attestations (e.g. docker/nginx:latest) - Platform: platform, // platform of subject image (image that attestations are being verified against) - } - ``` - * using local OCI layout - ```go - var resolver oci.AttestationResolver - resolver = &oci.OCILayoutResolver{ - Path: path, // file path to OCI layout containing image attestations (e.g. /myimage) - Platform: platform, // platform of subject image (image that attestations are being verified against) - } - ``` - -1. configure policy options - ```go - opts := &policy.PolicyOptions{ - TufClient: tufClient, - LocalTargetsDir: "/.docker/policy", // location to store policy files downloaded from TUF - LocalPolicyDir: "", // overrides TUF policy for local policy files - } - ``` - -1. verify attestations - ```go - policy, err := attest.Verify(ctx, opts, resolver) - if err != nil { - return false // failed policy or attestation signature verification - } - if policy { - return true // passed policy - } - return true // no policy for image - ``` +See example [example_verify.go](./pkg/attest/example_verify.go) ## signing attestations -1. generate an image with intoto Statements (optional) - ```sh - docker buildx build --sbom true --provenance true --output type=oci,tar=false,name=:,dest= - ``` +See example [example_sign.go](./pkg/attest/example_sign.go) -1. confgiure a `dsse.SignerVerifier` - ```go - var signer dsse.SignerVerifier - signer, err = signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region) - ``` +## mirroring TUF repositories to OCI +See example [example_mirror.go](./pkg/mirror/example_mirror.go) -1. configure signing options - ```go - opts := &attest.SigningOptions{ - Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place - } - ``` - * add [Verification Summary Attestation (VSA)](https://slsa.dev/spec/v1.0/verification_summary) for all intoto attestations (optional) - ```go - opts.VSAOptions = &attestation.VSAOptions{ - BuildLevel: "SLSA_BUILD_LEVEL_" + slsaBuildLevel, - PolicyURI: slsaPolicyUri, - VerifierID: slsaVerifierId, - } - ``` -1. load attestations - * oci registry - ```go - ref := "docker/attest:latest" - att, err := oci.AttestationIndexFromRemote(ref) - ``` - * local filepath - ```go - path := "/test-image" - att, err := oci.AttestationIndexFromPath(path) - ``` - -1. sign attestations - ```go - signedImageIndex, err := attest.Sign(ctx, att, signer, opts) - ``` - `attest.Sign()` iterates over attestation manifests in the image index and signs all intoto statements (optionally generates a VSA), returning a mutated ImageIndex with all intoto statements signed as attestations. - -1. save output (optional) - * push to oci registry - ```go - err = mirror.PushToRegistry(signedImageIndex, ref) - ``` - * save to local filesystem - ```go - idx := v1.ImageIndex(empty.Index) - idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ - Add: signedImageIndex, - Descriptor: v1.Descriptor{ - Annotations: map[string]string{ - oci.OciReferenceTarget: att.Name, - }, - }, - }) - err = mirror.SaveAsOCILayout(idx, path) - ``` - -## mirroring TUF repositories -TODO: write content for this outline -### mirroring TUF metadata to OCI -#### delegated metadata -### mirroring TUF targets to OCI -#### delegated targets ### using `go-tuf` OCI registry client +See example [example_registry](./pkg/tuf/example_registry.go) diff --git a/pkg/attest/example_sign.go b/pkg/attest/example_sign.go new file mode 100644 index 0000000..6330f7e --- /dev/null +++ b/pkg/attest/example_sign.go @@ -0,0 +1,78 @@ +package attest + +import ( + "context" + + "github.com/docker/attest/pkg/attestation" + "github.com/docker/attest/pkg/mirror" + "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/signerverifier" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" +) + +func ExampleSign_remote() { + // configure signerverifier + // local signer (unsafe for production) + signer, err := signerverifier.GenKeyPair() + if err != nil { + panic(err) + } + // example using AWS KMS signer + // aws_arn := "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012" + // aws_region := "us-west-2" + // signer, err := signerverifier.GetAWSSigner(cmd.Context(), aws_arn, aws_region) + + // configure signing options + opts := &SigningOptions{ + Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place + } + + // configure VSA options (optional) + slsaBuildLevel := "3" + slsaPolicyUri := "https://docker.com/attest/policy" + slsaVerifierId := "https://docker.com" + opts.VSAOptions = &attestation.VSAOptions{ + BuildLevel: "SLSA_BUILD_LEVEL_" + slsaBuildLevel, + PolicyURI: slsaPolicyUri, + VerifierID: slsaVerifierId, + } + + // load image index with unsigned attestation-manifests + ref := "docker/image-signer-verifier:latest" + att, err := oci.AttestationIndexFromRemote(ref) + if err != nil { + panic(err) + } + // example for local image index + // path := "/myimage" + // att, err := oci.AttestationIndexFromLocal(path) + + // sign attestations + signedImageIndex, err := Sign(context.Background(), att.Index, signer, opts) + if err != nil { + panic(err) + } + + // push image index with signed attestation-manifests + err = mirror.PushToRegistry(signedImageIndex, ref) + if err != nil { + panic(err) + } + // output image index to filesystem (optional) + path := "/myimage" + idx := v1.ImageIndex(empty.Index) + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: signedImageIndex, + Descriptor: v1.Descriptor{ + Annotations: map[string]string{ + oci.OciReferenceTarget: att.Name, + }, + }, + }) + err = mirror.SaveAsOCILayout(idx, path) + if err != nil { + panic(err) + } +} diff --git a/pkg/attest/example_verify.go b/pkg/attest/example_verify.go new file mode 100644 index 0000000..570b124 --- /dev/null +++ b/pkg/attest/example_verify.go @@ -0,0 +1,69 @@ +package attest + +import ( + "context" + "os" + "path/filepath" + + "github.com/docker/attest/internal/embed" + "github.com/docker/attest/pkg/oci" + "github.com/docker/attest/pkg/policy" + "github.com/docker/attest/pkg/tuf" +) + +func createTufClient(outputPath string) (*tuf.TufClient, error) { + // using oci tuf metadata and targets + metadataURI := "regsitry-1.docker.io/docker/tuf-metadata:latest" + targetsURI := "regsitry-1.docker.io/docker/tuf-targets" + // example using http tuf metadata and targets + // metadataURI := "https://docker.github.io/tuf-staging/metadata" + // targetsURI := "https://docker.github.io/tuf-staging/targets" + + return tuf.NewTufClient(embed.DefaultRoot, outputPath, metadataURI, targetsURI) +} + +func ExampleVerify_remote() { + // create a tuf client + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + tufOutputPath := filepath.Join(home, ".docker", "tuf") + tufClient, err := createTufClient(tufOutputPath) + if err != nil { + panic(err) + } + + // create a resolver for remote attestations + image := "regsitry-1.docker.io/notary:server" + platform := "linux/amd64" + resolver := &oci.RegistryResolver{ + Image: image, // path to image index in OCI registry containing image attestations + Platform: platform, // platform of subject image (image that attestations are being verified against) + } + // example using a local resolver + // path := "/myimage" + // platform := "linux/amd64" + // resolver := &oci.OCILayoutResolver{ + // Path: path, // file path to OCI layout containing image attestations + // Platform: platform, // platform of subject image (image that attestations are being verified against) + // } + + // configure policy options + opts := &policy.PolicyOptions{ + TufClient: tufClient, + LocalTargetsDir: filepath.Join(home, ".docker", "policy"), // location to store policy files downloaded from TUF + LocalPolicyDir: "", // overrides TUF policy for local policy files if set + } + + // verify attestations + policy, err := Verify(context.Background(), opts, resolver) + if err != nil { + panic(err) // failed policy or attestation signature verification + } + if policy { + print("policy passed: %v\n", policy) + return // passed policy + } + // no policy for image +} diff --git a/pkg/mirror/example_mirror.go b/pkg/mirror/example_mirror.go new file mode 100644 index 0000000..d89312c --- /dev/null +++ b/pkg/mirror/example_mirror.go @@ -0,0 +1,150 @@ +package mirror + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/docker/attest/internal/embed" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +type TufMirrorOutput struct { + metadata *v1.Image + delegatedMetadata []*MirrorImage + targets []*MirrorImage + delegatedTargets []*MirrorIndex +} + +func ExampleMirror() { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + tufOutputPath := filepath.Join(home, ".docker", "tuf") + + // configure TUF mirror + metadataURI := "https://docker.github.io/tuf-staging/metadata" + targetsURI := "https://docker.github.io/tuf-staging/targets" + m, err := NewTufMirror(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI) + if err != nil { + panic(err) + } + + // create metadata manifest + metadataManifest, err := m.GetMetadataManifest(metadataURI) + if err != nil { + panic(err) + } + // create delegated targets metadata manifests + delegatedMetadata, err := m.GetDelegatedMetadataMirrors() + if err != nil { + panic(err) + } + + // create targets manifest + targets, err := m.GetTufTargetMirrors() + if err != nil { + panic(err) + } + // create delegated targets manifests + delegatedTargets, err := m.GetDelegatedTargetMirrors() + if err != nil { + panic(err) + } + + mirrorOutput := &TufMirrorOutput{ + metadata: metadataManifest, + delegatedMetadata: delegatedMetadata, + targets: targets, + delegatedTargets: delegatedTargets, + } + + // push metadata and targets to registry (optional) + err = mirrorToRegistry(mirrorOutput) + if err != nil { + panic(err) + } + + // save metadata and targets to local directory (optional) + mirrorOutputPath := filepath.Join(home, ".docker", "tuf", "mirror") + err = mirrorToLocal(mirrorOutput, mirrorOutputPath) + if err != nil { + panic(err) + } +} + +func mirrorToRegistry(o *TufMirrorOutput) error { + // push metadata to registry + metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest" + err := PushToRegistry(o.metadata, metadataRepo) + if err != nil { + return err + } + // push delegated metadata to registry + for _, metadata := range o.delegatedMetadata { + repo, _, ok := strings.Cut(metadataRepo, ":") + if !ok { + return fmt.Errorf("failed to get repo without tag: %s", metadataRepo) + } + imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag) + err = PushToRegistry(metadata.Image, imageName) + if err != nil { + return err + } + } + + // push top-level targets to registry + targetsRepo := "registry-1.docker.io/docker/tuf-targets" + for _, target := range o.targets { + imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag) + err = PushToRegistry(target.Image, imageName) + if err != nil { + return err + } + } + // push delegated targets to registry + for _, target := range o.delegatedTargets { + imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag) + err = PushToRegistry(target.Index, imageName) + if err != nil { + return err + } + } + return nil +} + +func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { + // output metadata to local directory + err := SaveAsOCILayout(o.metadata, outputPath) + if err != nil { + return err + } + // output delegated metadata to local directory + for _, metadata := range o.delegatedMetadata { + path := filepath.Join(outputPath, metadata.Tag) + err = SaveAsOCILayout(metadata.Image, path) + if err != nil { + return err + } + } + + // output top-level targets to local directory + for _, target := range o.targets { + path := filepath.Join(outputPath, target.Tag) + err = SaveAsOCILayout(target.Image, path) + if err != nil { + return err + } + } + // output delegated targets to local directory + for _, target := range o.delegatedTargets { + path := filepath.Join(outputPath, target.Tag) + err = SaveAsOCILayout(target.Index, path) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/tuf/example_registry.go b/pkg/tuf/example_registry.go new file mode 100644 index 0000000..77bca56 --- /dev/null +++ b/pkg/tuf/example_registry.go @@ -0,0 +1,43 @@ +package tuf + +import ( + "os" + "path/filepath" + + "github.com/docker/attest/internal/embed" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +func ExampleTufRegistryClient() { + // create a tuf client + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + tufOutputPath := filepath.Join(home, ".docker", "tuf") + + // using oci tuf metadata and targets + metadataURI := "regsitry-1.docker.io/docker/tuf-metadata:latest" + targetsURI := "regsitry-1.docker.io/docker/tuf-targets" + registryClient, err := NewTufClient(embed.DefaultRoot, tufOutputPath, metadataURI, targetsURI) + if err != nil { + panic(err) + } + + // get trusted tuf metadata + trustedMetadata := registryClient.GetMetadata() + if err != nil { + panic(err) + } + + // top-level target files + targets := trustedMetadata.Targets[metadata.TARGETS].Signed.Targets + + for _, t := range targets { + // download target files + _, _, err := registryClient.DownloadTarget(t.Path, filepath.Join(tufOutputPath, "download")) + if err != nil { + panic(err) + } + } +}