diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 0609bb83c..7ab90fffc 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -563,6 +563,10 @@ definitions: type: boolean x-omitempty: false description: The immutable status of the tag + signed: + type: boolean + x-omitempty: false + description: The attribute indicates whether the tag is signed or not ExtraAttrs: type: object additionalProperties: diff --git a/src/api/artifact/controller.go b/src/api/artifact/controller.go index 114ac5e8a..7cc62410a 100644 --- a/src/api/artifact/controller.go +++ b/src/api/artifact/controller.go @@ -27,6 +27,7 @@ import ( "github.com/goharbor/harbor/src/pkg/immutabletag/match" "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" "github.com/goharbor/harbor/src/pkg/label" + "github.com/goharbor/harbor/src/pkg/signature" "github.com/opencontainers/go-digest" "strings" @@ -94,6 +95,7 @@ func NewController() Controller { repoMgr: repository.Mgr, artMgr: artifact.Mgr, tagMgr: tag.Mgr, + sigMgr: signature.GetManager(), labelMgr: label.Mgr, abstractor: abstractor.NewAbstractor(), immutableMtr: rule.NewRuleMatcher(), @@ -106,6 +108,7 @@ type controller struct { repoMgr repository.Manager artMgr artifact.Manager tagMgr tag.Manager + sigMgr signature.Manager labelMgr label.Manager abstractor abstractor.Abstractor immutableMtr match.ImmutableTagMatcher @@ -382,9 +385,6 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac if option.WithScanOverview { c.populateScanOverview(ctx, artifact) } - if option.WithSignature { - c.populateSignature(ctx, artifact) - } // populate addition links c.populateAdditionLinks(ctx, artifact) return artifact @@ -413,12 +413,36 @@ func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOp if option == nil { return t } + repo, err := c.repoMgr.Get(ctx, tag.RepositoryID) + if err != nil { + log.Errorf("Failed to get repo for tag: %s, error: %v", tag.Name, err) + return t + } if option.WithImmutableStatus { c.populateImmutableStatus(ctx, t) } + if option.WithSignature { + if a, err := c.artMgr.Get(ctx, t.ArtifactID); err != nil { + log.Errorf("Failed to get artifact for tag: %s, error: %v, skip populating signature", t.Name, err) + } else { + c.populateTagSignature(ctx, repo.Name, t, a.Digest, option) + } + } return t } +func (c *controller) populateTagSignature(ctx context.Context, repo string, tag *Tag, digest string, option *TagOption) { + if option.SignatureChecker == nil { + chk, err := signature.GetManager().GetCheckerByRepo(ctx, repo) + if err != nil { + log.Error(err) + return + } + option.SignatureChecker = chk + } + tag.Signed = option.SignatureChecker.IsTagSigned(tag.Name, digest) +} + func (c *controller) populateLabels(ctx context.Context, art *Artifact) { labels, err := c.labelMgr.ListByArtifact(ctx, art.ID) if err != nil { diff --git a/src/api/artifact/controller_test.go b/src/api/artifact/controller_test.go index c8957f14d..996378e62 100644 --- a/src/api/artifact/controller_test.go +++ b/src/api/artifact/controller_test.go @@ -131,7 +131,6 @@ func (c *controllerTestSuite) TestAssembleArtifact() { }, WithLabel: true, WithScanOverview: true, - WithSignature: true, } tg := &tag.Tag{ ID: 1, @@ -262,7 +261,6 @@ func (c *controllerTestSuite) TestList() { option := &Option{ WithTag: true, WithScanOverview: true, - WithSignature: true, } c.artMgr.On("List").Return(1, []*artifact.Artifact{ { diff --git a/src/api/artifact/model.go b/src/api/artifact/model.go index 3a4706f39..6030cabf5 100644 --- a/src/api/artifact/model.go +++ b/src/api/artifact/model.go @@ -18,6 +18,7 @@ import ( "github.com/go-openapi/strfmt" cmodels "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/signature" "github.com/goharbor/harbor/src/pkg/tag/model/tag" "github.com/goharbor/harbor/src/server/v2.0/models" ) @@ -73,6 +74,7 @@ func (a *Artifact) ToSwagger() *models.Artifact { PushTime: strfmt.DateTime(tag.PushTime), RepositoryID: tag.RepositoryID, Immutable: tag.Immutable, + Signed: tag.Signed, }) } for addition, link := range a.AdditionLinks { @@ -104,7 +106,8 @@ func (a *Artifact) ToSwagger() *models.Artifact { type Tag struct { tag.Tag Immutable bool - // TODO add other attrs: signature, etc + Signed bool + // TODO add other attrs: label, etc } // AdditionLink is a link via that the addition can be fetched @@ -119,13 +122,13 @@ type Option struct { TagOption *TagOption // only works when WithTag is set to true WithLabel bool WithScanOverview bool - // TODO move it to TagOption? - WithSignature bool } // TagOption is used to specify the properties returned when listing/getting tags type TagOption struct { WithImmutableStatus bool + WithSignature bool + SignatureChecker *signature.Checker } // TODO move this to GC controller? diff --git a/src/common/models/repo.go b/src/common/models/repo.go index aa2bc24ec..bba2fa9eb 100644 --- a/src/common/models/repo.go +++ b/src/common/models/repo.go @@ -17,7 +17,7 @@ package models import ( "time" - "github.com/goharbor/harbor/src/common/utils/notary/model" + "github.com/goharbor/harbor/src/pkg/signature/notary/model" "github.com/theupdateframework/notary/tuf/data" ) diff --git a/src/core/api/repository.go b/src/core/api/repository.go index 82508e326..bdf687563 100755 --- a/src/core/api/repository.go +++ b/src/core/api/repository.go @@ -34,8 +34,6 @@ import ( "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/common/utils/notary" - notarymodel "github.com/goharbor/harbor/src/common/utils/notary/model" "github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/core/config" notifierEvt "github.com/goharbor/harbor/src/core/notifier/event" @@ -45,6 +43,8 @@ import ( "github.com/goharbor/harbor/src/pkg/immutabletag/match/rule" "github.com/goharbor/harbor/src/pkg/scan/api/scan" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/pkg/signature/notary" + notarymodel "github.com/goharbor/harbor/src/pkg/signature/notary/model" "github.com/goharbor/harbor/src/replication" "github.com/goharbor/harbor/src/replication/event" "github.com/goharbor/harbor/src/replication/model" diff --git a/src/core/middlewares/contenttrust/handler.go b/src/core/middlewares/contenttrust/handler.go index d7c9e3c64..9e261b7cd 100644 --- a/src/core/middlewares/contenttrust/handler.go +++ b/src/core/middlewares/contenttrust/handler.go @@ -17,9 +17,9 @@ package contenttrust import ( "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/common/utils/notary" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/middlewares/util" + "github.com/goharbor/harbor/src/pkg/signature/notary" "net/http" "net/http/httptest" ) diff --git a/src/core/middlewares/contenttrust/handler_test.go b/src/core/middlewares/contenttrust/handler_test.go index 08f9b4500..32bed47c0 100644 --- a/src/core/middlewares/contenttrust/handler_test.go +++ b/src/core/middlewares/contenttrust/handler_test.go @@ -20,9 +20,9 @@ import ( "testing" "github.com/goharbor/harbor/src/common" - notarytest "github.com/goharbor/harbor/src/common/utils/notary/test" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/middlewares/util" + notarytest "github.com/goharbor/harbor/src/pkg/signature/notary/test" "github.com/stretchr/testify/assert" ) diff --git a/src/core/middlewares/util/util_test.go b/src/core/middlewares/util/util_test.go index 8263cfe82..5fa6b99db 100644 --- a/src/core/middlewares/util/util_test.go +++ b/src/core/middlewares/util/util_test.go @@ -29,10 +29,10 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils" - notarytest "github.com/goharbor/harbor/src/common/utils/notary/test" testutils "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/pkg/scan/vuln" + notarytest "github.com/goharbor/harbor/src/pkg/signature/notary/test" digest "github.com/opencontainers/go-digest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/src/pkg/signature/manager.go b/src/pkg/signature/manager.go new file mode 100644 index 000000000..cfa2b551e --- /dev/null +++ b/src/pkg/signature/manager.go @@ -0,0 +1,94 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signature + +import ( + "github.com/goharbor/harbor/src/common/security" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/pkg/signature/notary" + "github.com/goharbor/harbor/src/pkg/signature/notary/model" + "golang.org/x/net/context" +) + +// Checker checks the signature status of artifact +type Checker struct { + signatures map[string]string +} + +// IsTagSigned checks if the tag of the artifact is signed, it also checks the signed artifact has the same digest as parm. +func (sc Checker) IsTagSigned(tag, digest string) bool { + d, ok := sc.signatures[tag] + if len(digest) == 0 { + return ok + } + return digest == d +} + +// IsArtifactSigned checks if the artifact with given digest is signed. +func (sc Checker) IsArtifactSigned(digest string) bool { + for _, v := range sc.signatures { + if v == digest { + return true + } + } + return false +} + +// Manager interface for handling signatures of artifacts +type Manager interface { + // GetCheckerByRepo returns a Checker for checking signature + GetCheckerByRepo(ctx context.Context, repo string) (*Checker, error) +} + +type mgr struct { +} + +// GetCheckerByRepo ... +func (m *mgr) GetCheckerByRepo(ctx context.Context, repo string) (*Checker, error) { + if !config.WithNotary() { // return a checker that always return false + return &Checker{}, nil + } + s := make(map[string]string) + targets, err := m.getTargetsByRepo(ctx, repo) + if err != nil { + return nil, err + } + for _, t := range targets { + if d, err := notary.DigestFromTarget(t); err != nil { + log.Warningf("Failed to get signed digest for tag %s, error: %v, skip", t.Tag, err) + } else { + s[t.Tag] = d + } + } + return &Checker{s}, nil +} + +func (m *mgr) getTargetsByRepo(ctx context.Context, repo string) ([]model.Target, error) { + name := "unknown" + if sc, ok := security.FromContext(ctx); !ok || sc == nil { + log.Warningf("Unable to get security context") + } else { + name = sc.GetUsername() + } + return notary.GetInternalTargets(config.InternalNotaryEndpoint(), name, repo) +} + +var instance = &mgr{} + +// GetManager ... +func GetManager() Manager { + return instance +} diff --git a/src/pkg/signature/manager_test.go b/src/pkg/signature/manager_test.go new file mode 100644 index 000000000..a0cb237e1 --- /dev/null +++ b/src/pkg/signature/manager_test.go @@ -0,0 +1,100 @@ +package signature + +import ( + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/pkg/signature/notary/test" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "os" + "testing" +) + +func TestMain(m *testing.M) { + // B/C the notary requires private key for signing token, b + // before running locally, please make sure the env var is set as follow: + // export TOKEN_PRIVATE_KEY_PATH="/harbor/tests/private_key.pem" + endpoint := "10.117.4.142" + // notary-demo/busybox:1.0 is signed, more details in the notary/test pkg + notaryServer := test.NewNotaryServer(endpoint) + defer notaryServer.Close() + conf := map[string]interface{}{ + common.WithNotary: "true", + common.NotaryURL: notaryServer.URL, + common.ExtEndpoint: "https://" + endpoint, + } + config.InitWithSettings(conf) + result := m.Run() + if result != 0 { + os.Exit(result) + } +} + +func TestGetCheckerByRepo(t *testing.T) { + type in struct { + repo string + tag string + digest string + } + type res struct { + tagSigned bool + artSigned bool + } + m := GetManager() + cases := []struct { + input in + expect res + }{ + { + input: in{ + repo: "notary-demo/busybox", + tag: "1.0", + digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7", + }, + expect: res{ + tagSigned: true, + artSigned: true, + }, + }, + { + input: in{ + repo: "notary-demo/busybox", + tag: "1.0", + digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a8", + }, + expect: res{ + tagSigned: false, + artSigned: false, + }, + }, + { + input: in{ + repo: "notary-demo/busybox", + tag: "2.0", + digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7", + }, + expect: res{ + tagSigned: false, + artSigned: true, + }, + }, + { + input: in{ + repo: "non-exist", + tag: "1.0", + digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7", + }, + expect: res{ + tagSigned: false, + artSigned: false, + }, + }, + } + for _, c := range cases { + checker, err := m.GetCheckerByRepo(context.Background(), c.input.repo) + assert.Nil(t, err) + assert.Equal(t, c.expect.tagSigned, checker.IsTagSigned(c.input.tag, c.input.digest), + "Unexpected tagSigned value for input: %#v", c.input) + assert.Equal(t, c.expect.artSigned, checker.IsArtifactSigned(c.input.digest), "Unexpected artSigned value for input: %#v", c.input) + } +} diff --git a/src/common/utils/notary/helper.go b/src/pkg/signature/notary/helper.go similarity index 94% rename from src/common/utils/notary/helper.go rename to src/pkg/signature/notary/helper.go index db80a9450..6c354d4b1 100644 --- a/src/common/utils/notary/helper.go +++ b/src/pkg/signature/notary/helper.go @@ -17,13 +17,12 @@ package notary import ( "encoding/hex" "fmt" + model2 "github.com/goharbor/harbor/src/pkg/signature/notary/model" "net/http" "os" "path" "strings" - "github.com/goharbor/harbor/src/common/utils/notary/model" - "github.com/docker/distribution/registry/auth/token" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/registry" @@ -54,7 +53,7 @@ func init() { } // GetInternalTargets wraps GetTargets to read config values for getting full-qualified repo from internal notary instance. -func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]model.Target, error) { +func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]model2.Target, error) { ext, err := config.ExtEndpoint() if err != nil { log.Errorf("Error while reading external endpoint: %v", err) @@ -68,8 +67,8 @@ func GetInternalTargets(notaryEndpoint string, username string, repo string) ([] // GetTargets is a help function called by API to fetch signature information of a given repository. // Per docker's convention the repository should contain the information of endpoint, i.e. it should look // like "192.168.0.1/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo) -func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.Target, error) { - res := []model.Target{} +func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model2.Target, error) { + res := []model2.Target{} t, err := tokenutil.MakeToken(username, tokenutil.Notary, []*token.ResourceActions{ { @@ -103,7 +102,7 @@ func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model. log.Warningf("Failed to clear cached root.json: %s, error: %v, when repo is removed from notary the signature status maybe incorrect", rootJSON, rmErr) } for _, t := range targets { - res = append(res, model.Target{ + res = append(res, model2.Target{ Tag: t.Name, Hashes: t.Hashes, }) @@ -112,7 +111,7 @@ func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model. } // DigestFromTarget get a target and return the value of digest, in accordance to Docker-Content-Digest -func DigestFromTarget(t model.Target) (string, error) { +func DigestFromTarget(t model2.Target) (string, error) { sha, ok := t.Hashes["sha256"] if !ok { return "", fmt.Errorf("no valid hash, expecting sha256") diff --git a/src/common/utils/notary/helper_test.go b/src/pkg/signature/notary/helper_test.go similarity index 92% rename from src/common/utils/notary/helper_test.go rename to src/pkg/signature/notary/helper_test.go index 56bd0b958..6e89c6491 100644 --- a/src/common/utils/notary/helper_test.go +++ b/src/pkg/signature/notary/helper_test.go @@ -16,10 +16,9 @@ package notary import ( "encoding/json" "fmt" + model2 "github.com/goharbor/harbor/src/pkg/signature/notary/model" + test2 "github.com/goharbor/harbor/src/pkg/signature/notary/test" - "github.com/goharbor/harbor/src/common/utils/notary/model" - - notarytest "github.com/goharbor/harbor/src/common/utils/notary/test" "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/core/config" "github.com/stretchr/testify/assert" @@ -36,7 +35,7 @@ var endpoint = "10.117.4.142" var notaryServer *httptest.Server func TestMain(m *testing.M) { - notaryServer = notarytest.NewNotaryServer(endpoint) + notaryServer = test2.NewNotaryServer(endpoint) defer notaryServer.Close() var defaultConfig = map[string]interface{}{ common.ExtEndpoint: "https://" + endpoint, @@ -80,13 +79,13 @@ func TestGetDigestFromTarget(t *testing.T) { } }` - var t1 model.Target + var t1 model2.Target err := json.Unmarshal([]byte(str), &t1) if err != nil { panic(err) } hash2 := make(map[string][]byte) - t2 := model.Target{ + t2 := model2.Target{ Tag: "2.0", Hashes: hash2, } diff --git a/src/common/utils/notary/model/model.go b/src/pkg/signature/notary/model/model.go similarity index 100% rename from src/common/utils/notary/model/model.go rename to src/pkg/signature/notary/model/model.go diff --git a/src/common/utils/notary/test/server.go b/src/pkg/signature/notary/test/server.go similarity index 100% rename from src/common/utils/notary/test/server.go rename to src/pkg/signature/notary/test/server.go diff --git a/src/common/utils/notary/test/valid/root.json b/src/pkg/signature/notary/test/valid/root.json similarity index 100% rename from src/common/utils/notary/test/valid/root.json rename to src/pkg/signature/notary/test/valid/root.json diff --git a/src/common/utils/notary/test/valid/snapshot.62f1f6fb42d7b1be70c7a0f4b61a444275bbdb857c2d0a2d373a51192af28d46.json b/src/pkg/signature/notary/test/valid/snapshot.62f1f6fb42d7b1be70c7a0f4b61a444275bbdb857c2d0a2d373a51192af28d46.json similarity index 100% rename from src/common/utils/notary/test/valid/snapshot.62f1f6fb42d7b1be70c7a0f4b61a444275bbdb857c2d0a2d373a51192af28d46.json rename to src/pkg/signature/notary/test/valid/snapshot.62f1f6fb42d7b1be70c7a0f4b61a444275bbdb857c2d0a2d373a51192af28d46.json diff --git a/src/common/utils/notary/test/valid/targets.7045319e2aa2b412c0028a5ebf0b0846e08f07fb0bd6dfd181cd4bbe72838fd7.json b/src/pkg/signature/notary/test/valid/targets.7045319e2aa2b412c0028a5ebf0b0846e08f07fb0bd6dfd181cd4bbe72838fd7.json similarity index 100% rename from src/common/utils/notary/test/valid/targets.7045319e2aa2b412c0028a5ebf0b0846e08f07fb0bd6dfd181cd4bbe72838fd7.json rename to src/pkg/signature/notary/test/valid/targets.7045319e2aa2b412c0028a5ebf0b0846e08f07fb0bd6dfd181cd4bbe72838fd7.json diff --git a/src/common/utils/notary/test/valid/timestamp.json b/src/pkg/signature/notary/test/valid/timestamp.json similarity index 100% rename from src/common/utils/notary/test/valid/timestamp.json rename to src/pkg/signature/notary/test/valid/timestamp.json diff --git a/src/server/middleware/contenttrust/contenttrust.go b/src/server/middleware/contenttrust/contenttrust.go index 71894a052..8bdda34bf 100644 --- a/src/server/middleware/contenttrust/contenttrust.go +++ b/src/server/middleware/contenttrust/contenttrust.go @@ -2,10 +2,10 @@ package contenttrust import ( "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/common/utils/notary" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/middlewares/util" internal_errors "github.com/goharbor/harbor/src/internal/error" + "github.com/goharbor/harbor/src/pkg/signature/notary" serror "github.com/goharbor/harbor/src/server/error" "github.com/goharbor/harbor/src/server/middleware" "net/http" diff --git a/src/server/v2.0/handler/artifact.go b/src/server/v2.0/handler/artifact.go index 553357c11..f20b58768 100644 --- a/src/server/v2.0/handler/artifact.go +++ b/src/server/v2.0/handler/artifact.go @@ -229,10 +229,12 @@ func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSigna option.WithTag = *(withTag) } if option.WithTag { + option.TagOption = &artifact.TagOption{} if withImmutableStatus != nil { - option.TagOption = &artifact.TagOption{ - WithImmutableStatus: *(withImmutableStatus), - } + option.TagOption.WithImmutableStatus = *(withImmutableStatus) + } + if withSignature != nil { + option.TagOption.WithSignature = *withSignature } } if withLabel != nil { @@ -241,8 +243,5 @@ func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSigna if withScanOverview != nil { option.WithScanOverview = *(withScanOverview) } - if withSignature != nil { - option.WithSignature = *(withSignature) - } return option }