From 76b981faec6f33560c0b5cb914c4d2a1217a9a92 Mon Sep 17 00:00:00 2001 From: Wang Yan Date: Tue, 7 Dec 2021 15:43:10 +0800 Subject: [PATCH] add cosign middleware (#16078) The middleware is to land the cosign signature linkage with the subject artifact ID. Signed-off-by: Wang Yan --- src/server/middleware/cosign/cosign.go | 132 ++++++++++++++++++ src/server/middleware/cosign/cosign_test.go | 142 ++++++++++++++++++++ src/server/registry/route.go | 2 + 3 files changed, 276 insertions(+) create mode 100644 src/server/middleware/cosign/cosign.go create mode 100644 src/server/middleware/cosign/cosign_test.go diff --git a/src/server/middleware/cosign/cosign.go b/src/server/middleware/cosign/cosign.go new file mode 100644 index 000000000..8aeb986b7 --- /dev/null +++ b/src/server/middleware/cosign/cosign.go @@ -0,0 +1,132 @@ +package cosign + +import ( + "fmt" + "github.com/docker/distribution/reference" + "github.com/goharbor/harbor/src/controller/artifact" + "github.com/goharbor/harbor/src/lib" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/accessory" + "github.com/goharbor/harbor/src/pkg/accessory/model" + "github.com/goharbor/harbor/src/pkg/distribution" + "github.com/goharbor/harbor/src/server/middleware" + digest "github.com/opencontainers/go-digest" + "io/ioutil" + "net/http" + "regexp" +) + +var ( + // repositorySubexp is the name for sub regex that maps to subject artifact digest in the url + subArtDigestSubexp = "digest" + // repositorySubexp is the name for sub regex that maps to repository name in the url + repositorySubexp = "repository" + cosignRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/manifests/%s-(?P<%s>%s).sig$`, repositorySubexp, reference.NameRegexp.String(), digest.SHA256, subArtDigestSubexp, reference.IdentifierRegexp)) + // the media type of consign signature layer + mediaTypeCosignLayer = "application/vnd.dev.cosign.simplesigning.v1+json" +) + +// CosignSignatureMiddleware middleware to record the linkeage of artifact and its accessory +/* PUT /v2/library/hello-world/manifests/sha256-1b26826f602946860c279fce658f31050cff2c596583af237d971f4629b57792.sig +{ + "schemaVersion":2, + "config":{ + "mediaType":"application/vnd.oci.image.config.v1+json", + "size":233, + "digest":"sha256:d4e6059ece7bea95266fd7766353130d4bf3dc21048b8a9783c98b8412618c38" + }, + "layers":[ + { + "mediaType":"application/vnd.dev.cosign.simplesigning.v1+json", + "size":250, + "digest":"sha256:91a821a0e2412f1b99b07bfe176451bcc343568b761388718abbf38076048564", + "annotations":{ + "dev.cosignproject.cosign/signature":"MEUCIQD/imXjZJlcV82eXu9y9FJGgbDwVPw7AaGFzqva8G+CgwIgYc4CRvEjwoAwkzGoX+aZxQWCASpv5G+EAWDKOJRLbTQ=" + } + } + ] +} +*/ +func CosignSignatureMiddleware() func(http.Handler) http.Handler { + return middleware.AfterResponse(func(w http.ResponseWriter, r *http.Request, statusCode int) error { + if statusCode != http.StatusCreated { + return nil + } + + ctx := r.Context() + logger := log.G(ctx).WithFields(log.Fields{"middleware": "cosign"}) + + none := lib.ArtifactInfo{} + info := lib.GetArtifactInfo(ctx) + if info == none { + return errors.New("artifactinfo middleware required before this middleware").WithCode(errors.NotFoundCode) + } + if info.Tag == "" { + return nil + } + + // Needs tag to match the cosign tag pattern. + _, subjectArtDigest, ok := matchCosignSignaturePattern(r.URL.Path) + if !ok { + return nil + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + contentType := r.Header.Get("Content-Type") + manifest, desc, err := distribution.UnmarshalManifest(contentType, body) + if err != nil { + logger.Errorf("unmarshal manifest failed, error: %v", err) + return err + } + + var hasSignature bool + for _, descriptor := range manifest.References() { + if descriptor.MediaType == mediaTypeCosignLayer { + hasSignature = true + break + } + } + + if hasSignature { + subjectArt, err := artifact.Ctl.GetByReference(ctx, info.Repository, fmt.Sprintf("%s:%s", digest.SHA256, subjectArtDigest), nil) + if err != nil { + logger.Errorf("failed to get subject artifact: %s, error: %v", subjectArtDigest, err) + return err + } + art, err := artifact.Ctl.GetByReference(ctx, info.Repository, desc.Digest.String(), nil) + if err != nil { + logger.Errorf("failed to get cosign signature artifact: %s, error: %v", desc.Digest.String(), err) + return err + } + + _, err = accessory.Mgr.Create(ctx, model.AccessoryData{ + ArtifactID: art.ID, + SubArtifactID: subjectArt.ID, + Size: desc.Size, + Digest: desc.Digest.String(), + Type: model.TypeCosignSignature, + }) + if err != nil { + logger.Errorf("failed to get cosign signature artifact: %s, error: %v", desc.Digest.String(), err) + return err + } + } + + return nil + }) +} + +// matchCosignSignaturePattern checks whether the provided path matches the blob upload URL pattern, +// if does, returns the repository as well +func matchCosignSignaturePattern(path string) (repository, digest string, match bool) { + strs := cosignRe.FindStringSubmatch(path) + if len(strs) < 3 { + return "", "", false + } + return strs[1], strs[2], true +} diff --git a/src/server/middleware/cosign/cosign_test.go b/src/server/middleware/cosign/cosign_test.go new file mode 100644 index 000000000..2916d4423 --- /dev/null +++ b/src/server/middleware/cosign/cosign_test.go @@ -0,0 +1,142 @@ +package cosign + +import ( + "fmt" + "github.com/goharbor/harbor/src/controller/repository" + "github.com/goharbor/harbor/src/lib" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/accessory" + "github.com/goharbor/harbor/src/pkg/accessory/model" + "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/distribution" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +type MiddlewareTestSuite struct { + htesting.Suite +} + +func (suite *MiddlewareTestSuite) SetupTest() { + suite.Suite.SetupSuite() +} + +func (suite *MiddlewareTestSuite) TearDownTest() { +} + +func (suite *MiddlewareTestSuite) prepare(name, ref string) (distribution.Manifest, distribution.Descriptor, *http.Request) { + body := fmt.Sprintf(` + { + "schemaVersion":2, + "config":{ + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "size":233, + "digest":"sha256:d4e6059ece7bea95266fd7766353130d4bf3dc21048b8a9783c98b8412618c38" + }, + "layers":[ + { + "mediaType":"application/vnd.dev.cosign.simplesigning.v1+json", + "size":250, + "digest":"sha256:91a821a0e2412f1b99b07bfe176451bcc343568b761388718abbf38076048564", + "annotations":{ + "dev.cosignproject.cosign/signature":"MEUCIQD/imXjZJlcV82eXu9y9FJGgbDwVPw7AaGFzqva8G+CgwIgYc4CRvEjwoAwkzGoX+aZxQWCASpv5G+EAWDKOJRLbTQ=" + } + } + ] + }`) + + manifest, descriptor, err := distribution.UnmarshalManifest("application/vnd.oci.image.manifest.v1+json", []byte(body)) + suite.Nil(err) + + req := suite.NewRequest(http.MethodPut, fmt.Sprintf("/v2/%s/manifests/%s", name, ref), strings.NewReader(body)) + req.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json") + info := lib.ArtifactInfo{ + Repository: name, + Reference: ref, + Tag: ref, + Digest: descriptor.Digest.String(), + } + + return manifest, descriptor, req.WithContext(lib.WithArtifactInfo(req.Context(), info)) +} + +func (suite *MiddlewareTestSuite) addArt(pid, repositoryID int64, repositoryName, dgt string) int64 { + af := &artifact.Artifact{ + Type: "Docker-Image", + ProjectID: pid, + RepositoryID: repositoryID, + RepositoryName: repositoryName, + Digest: dgt, + Size: 1024, + PushTime: time.Now(), + PullTime: time.Now(), + } + afid, err := artifact.Mgr.Create(suite.Context(), af) + suite.Nil(err, fmt.Sprintf("Add artifact failed for %d", repositoryID)) + return afid +} + +func (suite *MiddlewareTestSuite) TestCosignSignature() { + suite.WithProject(func(projectID int64, projectName string) { + name := fmt.Sprintf("%s/hello-world", projectName) + subArtDigest := suite.DigestString() + ref := fmt.Sprintf("%s.sig", strings.ReplaceAll(subArtDigest, "sha256:", "sha256-")) + _, descriptor, req := suite.prepare(name, ref) + + _, repoId, err := repository.Ctl.Ensure(suite.Context(), name) + suite.Nil(err) + subjectArtID := suite.addArt(projectID, repoId, name, subArtDigest) + artID := suite.addArt(projectID, repoId, name, descriptor.Digest.String()) + suite.Nil(err) + + res := httptest.NewRecorder() + next := suite.NextHandler(http.StatusCreated, map[string]string{"Docker-Content-Digest": descriptor.Digest.String()}) + CosignSignatureMiddleware()(next).ServeHTTP(res, req) + suite.Equal(http.StatusCreated, res.Code) + + accs, err := accessory.Mgr.List(suite.Context(), &q.Query{ + Keywords: map[string]interface{}{ + "SubjectArtifactID": subjectArtID, + }, + }) + suite.Equal(1, len(accs)) + suite.Equal(subjectArtID, accs[0].GetData().SubArtifactID) + suite.Equal(artID, accs[0].GetData().ArtifactID) + suite.True(accs[0].IsHard()) + suite.Equal(model.TypeCosignSignature, accs[0].GetData().Type) + }) +} + +func (suite *MiddlewareTestSuite) TestMatchManifestURLPattern() { + _, _, ok := matchCosignSignaturePattern("/v2/library/hello-world/manifests/.Invalid") + suite.False(ok) + + _, _, ok = matchCosignSignaturePattern("/v2/") + suite.False(ok) + + _, _, ok = matchCosignSignaturePattern("/v2/library/hello-world/manifests//") + suite.False(ok) + + _, _, ok = matchCosignSignaturePattern("/v2/library/hello-world/manifests/###") + suite.False(ok) + + repository, _, ok := matchCosignSignaturePattern("/v2/library/hello-world/manifests/latest") + suite.False(ok) + + _, _, ok = matchCosignSignaturePattern("/v2/library/hello-world/manifests/sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9") + suite.False(ok) + + repository, reference, ok := matchCosignSignaturePattern("/v2/library/hello-world/manifests/sha256-e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9.sig") + suite.True(ok) + suite.Equal("library/hello-world", repository) + suite.Equal("e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9", reference) +} + +func TestMiddlewareTestSuite(t *testing.T) { + suite.Run(t, &MiddlewareTestSuite{}) +} diff --git a/src/server/registry/route.go b/src/server/registry/route.go index a21123fb1..5dae94da3 100644 --- a/src/server/registry/route.go +++ b/src/server/registry/route.go @@ -15,6 +15,7 @@ package registry import ( + "github.com/goharbor/harbor/src/server/middleware/cosign" "net/http" "github.com/goharbor/harbor/src/server/middleware/blob" @@ -75,6 +76,7 @@ func RegisterRoutes() { Middleware(repoproxy.DisableBlobAndManifestUploadMiddleware()). Middleware(immutable.Middleware()). Middleware(quota.PutManifestMiddleware()). + Middleware(cosign.CosignSignatureMiddleware()). Middleware(blob.PutManifestMiddleware()). HandlerFunc(putManifest) // blob head