add cosign middleware (#16078)

The middleware is to land the cosign signature linkage with the subject artifact ID.

Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2021-12-07 15:43:10 +08:00 committed by GitHub
parent 7e67c1f495
commit 76b981faec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 276 additions and 0 deletions

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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