mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-27 02:58:05 +01:00
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:
parent
7e67c1f495
commit
76b981faec
132
src/server/middleware/cosign/cosign.go
Normal file
132
src/server/middleware/cosign/cosign.go
Normal 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
|
||||
}
|
142
src/server/middleware/cosign/cosign_test.go
Normal file
142
src/server/middleware/cosign/cosign_test.go
Normal 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{})
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user