feat: [CODE-2554]: add commit and tag signature parse and verify (#3915)
* empty commit * Merge remote-tracking branch 'origin/main' into mg/publickey/verify * addressing PR comments * addressing PR comments * addressing PR comments * commit signature parsing
This commit is contained in:
parent
0bacf9d63d
commit
7d0ffbfbc0
|
|
@ -151,6 +151,14 @@ func (c *Controller) GetContent(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if info.LatestCommit != nil {
|
||||
err = c.signatureVerifyService.VerifyCommits(ctx, repo.ID, []*types.Commit{info.LatestCommit})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify signature of the last commit SHA=%s: %w",
|
||||
info.LatestCommit.SHA.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return &GetContentOutput{
|
||||
ContentInfo: info,
|
||||
Content: content,
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/app/api/controller"
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/git"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
|
|
@ -28,7 +31,7 @@ type PathsDetailsInput struct {
|
|||
}
|
||||
|
||||
type PathsDetailsOutput struct {
|
||||
Details []git.PathDetails `json:"details"`
|
||||
Details []types.PathDetails `json:"details"`
|
||||
}
|
||||
|
||||
// PathsDetails finds the additional info about the provided paths of the repo.
|
||||
|
|
@ -68,7 +71,25 @@ func (c *Controller) PathsDetails(ctx context.Context,
|
|||
return PathsDetailsOutput{}, err
|
||||
}
|
||||
|
||||
return PathsDetailsOutput{
|
||||
Details: result.Details,
|
||||
}, nil
|
||||
commits := make([]*types.Commit, 0, len(result.Details))
|
||||
output := PathsDetailsOutput{
|
||||
Details: make([]types.PathDetails, len(result.Details)),
|
||||
}
|
||||
for i, d := range result.Details {
|
||||
lastCommit := controller.MapCommit(d.LastCommit)
|
||||
output.Details[i] = types.PathDetails{
|
||||
Path: d.Path,
|
||||
LastCommit: lastCommit,
|
||||
}
|
||||
if lastCommit != nil {
|
||||
commits = append(commits, lastCommit)
|
||||
}
|
||||
}
|
||||
|
||||
err = c.signatureVerifyService.VerifyCommits(ctx, repo.ID, commits)
|
||||
if err != nil {
|
||||
return PathsDetailsOutput{}, fmt.Errorf("failed to verify signature of commits: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/harness/gitness/app/services/locker"
|
||||
"github.com/harness/gitness/app/services/protection"
|
||||
"github.com/harness/gitness/app/services/publicaccess"
|
||||
"github.com/harness/gitness/app/services/publickey"
|
||||
"github.com/harness/gitness/app/services/refcache"
|
||||
"github.com/harness/gitness/app/services/rules"
|
||||
"github.com/harness/gitness/app/services/settings"
|
||||
|
|
@ -78,42 +79,43 @@ func (r RepositoryOutput) MarshalJSON() ([]byte, error) {
|
|||
type Controller struct {
|
||||
defaultBranch string
|
||||
|
||||
tx dbtx.Transactor
|
||||
urlProvider url.Provider
|
||||
authorizer authz.Authorizer
|
||||
repoStore store.RepoStore
|
||||
spaceStore store.SpaceStore
|
||||
pipelineStore store.PipelineStore
|
||||
executionStore store.ExecutionStore
|
||||
principalStore store.PrincipalStore
|
||||
ruleStore store.RuleStore
|
||||
checkStore store.CheckStore
|
||||
pullReqStore store.PullReqStore
|
||||
settings *settings.Service
|
||||
principalInfoCache store.PrincipalInfoCache
|
||||
userGroupStore store.UserGroupStore
|
||||
userGroupService usergroup.Service
|
||||
protectionManager *protection.Manager
|
||||
git git.Interface
|
||||
spaceFinder refcache.SpaceFinder
|
||||
repoFinder refcache.RepoFinder
|
||||
importer *importer.Repository
|
||||
codeOwners *codeowners.Service
|
||||
eventReporter *repoevents.Reporter
|
||||
indexer keywordsearch.Indexer
|
||||
resourceLimiter limiter.ResourceLimiter
|
||||
locker *locker.Locker
|
||||
auditService audit.Service
|
||||
mtxManager lock.MutexManager
|
||||
identifierCheck check.RepoIdentifier
|
||||
repoCheck Check
|
||||
publicAccess publicaccess.Service
|
||||
labelSvc *label.Service
|
||||
instrumentation instrument.Service
|
||||
rulesSvc *rules.Service
|
||||
sseStreamer sse.Streamer
|
||||
lfsCtrl *lfs.Controller
|
||||
favoriteStore store.FavoriteStore
|
||||
tx dbtx.Transactor
|
||||
urlProvider url.Provider
|
||||
authorizer authz.Authorizer
|
||||
repoStore store.RepoStore
|
||||
spaceStore store.SpaceStore
|
||||
pipelineStore store.PipelineStore
|
||||
executionStore store.ExecutionStore
|
||||
principalStore store.PrincipalStore
|
||||
ruleStore store.RuleStore
|
||||
checkStore store.CheckStore
|
||||
pullReqStore store.PullReqStore
|
||||
settings *settings.Service
|
||||
principalInfoCache store.PrincipalInfoCache
|
||||
userGroupStore store.UserGroupStore
|
||||
userGroupService usergroup.Service
|
||||
protectionManager *protection.Manager
|
||||
git git.Interface
|
||||
spaceFinder refcache.SpaceFinder
|
||||
repoFinder refcache.RepoFinder
|
||||
importer *importer.Repository
|
||||
codeOwners *codeowners.Service
|
||||
eventReporter *repoevents.Reporter
|
||||
indexer keywordsearch.Indexer
|
||||
resourceLimiter limiter.ResourceLimiter
|
||||
locker *locker.Locker
|
||||
auditService audit.Service
|
||||
mtxManager lock.MutexManager
|
||||
identifierCheck check.RepoIdentifier
|
||||
repoCheck Check
|
||||
publicAccess publicaccess.Service
|
||||
labelSvc *label.Service
|
||||
instrumentation instrument.Service
|
||||
rulesSvc *rules.Service
|
||||
sseStreamer sse.Streamer
|
||||
lfsCtrl *lfs.Controller
|
||||
favoriteStore store.FavoriteStore
|
||||
signatureVerifyService publickey.SignatureVerifyService
|
||||
}
|
||||
|
||||
func NewController(
|
||||
|
|
@ -154,45 +156,47 @@ func NewController(
|
|||
sseStreamer sse.Streamer,
|
||||
lfsCtrl *lfs.Controller,
|
||||
favoriteStore store.FavoriteStore,
|
||||
signatureVerifyService publickey.SignatureVerifyService,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
defaultBranch: config.Git.DefaultBranch,
|
||||
tx: tx,
|
||||
urlProvider: urlProvider,
|
||||
authorizer: authorizer,
|
||||
repoStore: repoStore,
|
||||
spaceStore: spaceStore,
|
||||
pipelineStore: pipelineStore,
|
||||
executionStore: executionStore,
|
||||
principalStore: principalStore,
|
||||
ruleStore: ruleStore,
|
||||
checkStore: checkStore,
|
||||
pullReqStore: pullReqStore,
|
||||
settings: settings,
|
||||
principalInfoCache: principalInfoCache,
|
||||
protectionManager: protectionManager,
|
||||
git: git,
|
||||
spaceFinder: spaceFinder,
|
||||
repoFinder: repoFinder,
|
||||
importer: importer,
|
||||
codeOwners: codeOwners,
|
||||
eventReporter: eventReporter,
|
||||
indexer: indexer,
|
||||
resourceLimiter: limiter,
|
||||
locker: locker,
|
||||
auditService: auditService,
|
||||
mtxManager: mtxManager,
|
||||
identifierCheck: identifierCheck,
|
||||
repoCheck: repoCheck,
|
||||
publicAccess: publicAccess,
|
||||
labelSvc: labelSvc,
|
||||
instrumentation: instrumentation,
|
||||
userGroupStore: userGroupStore,
|
||||
userGroupService: userGroupService,
|
||||
rulesSvc: rulesSvc,
|
||||
sseStreamer: sseStreamer,
|
||||
lfsCtrl: lfsCtrl,
|
||||
favoriteStore: favoriteStore,
|
||||
defaultBranch: config.Git.DefaultBranch,
|
||||
tx: tx,
|
||||
urlProvider: urlProvider,
|
||||
authorizer: authorizer,
|
||||
repoStore: repoStore,
|
||||
spaceStore: spaceStore,
|
||||
pipelineStore: pipelineStore,
|
||||
executionStore: executionStore,
|
||||
principalStore: principalStore,
|
||||
ruleStore: ruleStore,
|
||||
checkStore: checkStore,
|
||||
pullReqStore: pullReqStore,
|
||||
settings: settings,
|
||||
principalInfoCache: principalInfoCache,
|
||||
protectionManager: protectionManager,
|
||||
git: git,
|
||||
spaceFinder: spaceFinder,
|
||||
repoFinder: repoFinder,
|
||||
importer: importer,
|
||||
codeOwners: codeOwners,
|
||||
eventReporter: eventReporter,
|
||||
indexer: indexer,
|
||||
resourceLimiter: limiter,
|
||||
locker: locker,
|
||||
auditService: auditService,
|
||||
mtxManager: mtxManager,
|
||||
identifierCheck: identifierCheck,
|
||||
repoCheck: repoCheck,
|
||||
publicAccess: publicAccess,
|
||||
labelSvc: labelSvc,
|
||||
instrumentation: instrumentation,
|
||||
userGroupStore: userGroupStore,
|
||||
userGroupService: userGroupService,
|
||||
rulesSvc: rulesSvc,
|
||||
sseStreamer: sseStreamer,
|
||||
lfsCtrl: lfsCtrl,
|
||||
favoriteStore: favoriteStore,
|
||||
signatureVerifyService: signatureVerifyService,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func (c *Controller) CreateCommitTag(ctx context.Context,
|
|||
session *auth.Session,
|
||||
repoRef string,
|
||||
in *CreateCommitTagInput,
|
||||
) (*CommitTag, []types.RuleViolations, error) {
|
||||
) (*types.CommitTag, []types.RuleViolations, error) {
|
||||
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -100,7 +100,7 @@ func (c *Controller) CreateCommitTag(ctx context.Context,
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
commitTag := mapCommitTag(rpcOut.CommitTag)
|
||||
commitTag := controller.MapCommitTag(rpcOut.CommitTag)
|
||||
|
||||
err = c.instrumentation.Track(ctx, instrument.Event{
|
||||
Type: instrument.EventTypeCreateTag,
|
||||
|
|
|
|||
|
|
@ -16,39 +16,28 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/app/api/controller"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/git"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
type CommitTag struct {
|
||||
Name string `json:"name"`
|
||||
SHA sha.SHA `json:"sha"`
|
||||
IsAnnotated bool `json:"is_annotated"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Tagger *types.Signature `json:"tagger,omitempty"`
|
||||
SignedData *types.SignedData `json:"-"`
|
||||
Commit *types.Commit `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
// ListCommitTags lists the commit tags of a repo.
|
||||
func (c *Controller) ListCommitTags(ctx context.Context,
|
||||
session *auth.Session,
|
||||
repoRef string,
|
||||
includeCommit bool,
|
||||
filter *types.TagFilter,
|
||||
) ([]CommitTag, error) {
|
||||
) ([]*types.CommitTag, error) {
|
||||
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rpcOut, err := c.git.ListCommitTags(ctx, &git.ListCommitTagsParams{
|
||||
result, err := c.git.ListCommitTags(ctx, &git.ListCommitTagsParams{
|
||||
ReadParams: git.CreateReadParams(repo),
|
||||
IncludeCommit: includeCommit,
|
||||
Query: filter.Query,
|
||||
|
|
@ -61,11 +50,33 @@ func (c *Controller) ListCommitTags(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tags := make([]CommitTag, len(rpcOut.Tags))
|
||||
for i := range rpcOut.Tags {
|
||||
tags[i] = mapCommitTag(rpcOut.Tags[i])
|
||||
tags := make([]*types.CommitTag, len(result.Tags))
|
||||
for i := range result.Tags {
|
||||
t := controller.MapCommitTag(result.Tags[i])
|
||||
tags[i] = &t
|
||||
}
|
||||
|
||||
verifySession := c.signatureVerifyService.NewVerifySession(repo.ID)
|
||||
|
||||
err = verifySession.VerifyCommitTags(ctx, tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify tags: %w", err)
|
||||
}
|
||||
|
||||
commits := make([]*types.Commit, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
if tag.Commit != nil {
|
||||
commits = append(commits, tag.Commit)
|
||||
}
|
||||
}
|
||||
|
||||
err = verifySession.VerifyCommits(ctx, commits)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify signature of tags' commits: %w", err)
|
||||
}
|
||||
|
||||
verifySession.StoreSignatures(ctx)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
|
|
@ -82,22 +93,3 @@ func mapToRPCTagSortOption(o enum.TagSortOption) git.TagSortOption {
|
|||
return git.TagSortOptionDefault
|
||||
}
|
||||
}
|
||||
|
||||
func mapCommitTag(t git.CommitTag) CommitTag {
|
||||
var tagger *types.Signature
|
||||
if t.Tagger != nil {
|
||||
tagger = &types.Signature{}
|
||||
*tagger = controller.MapSignature(*t.Tagger)
|
||||
}
|
||||
|
||||
return CommitTag{
|
||||
Name: t.Name,
|
||||
SHA: t.SHA,
|
||||
IsAnnotated: t.IsAnnotated,
|
||||
Title: t.Title,
|
||||
Message: t.Message,
|
||||
Tagger: tagger,
|
||||
SignedData: (*types.SignedData)(t.SignedData),
|
||||
Commit: controller.MapCommit(t.Commit),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func (c *Controller) ListCommits(ctx context.Context,
|
|||
return types.ListCommitResponse{}, fmt.Errorf("failed create author regex: %w", err)
|
||||
}
|
||||
|
||||
rpcOut, err := c.git.ListCommits(ctx, &git.ListCommitsParams{
|
||||
result, err := c.git.ListCommits(ctx, &git.ListCommitsParams{
|
||||
ReadParams: git.CreateReadParams(repo),
|
||||
GitREF: gitRef,
|
||||
After: filter.After,
|
||||
|
|
@ -74,23 +74,29 @@ func (c *Controller) ListCommits(ctx context.Context,
|
|||
return types.ListCommitResponse{}, err
|
||||
}
|
||||
|
||||
commits := make([]types.Commit, len(rpcOut.Commits))
|
||||
for i := range rpcOut.Commits {
|
||||
commits[i] = *controller.MapCommit(&rpcOut.Commits[i])
|
||||
commits := make([]*types.Commit, len(result.Commits))
|
||||
for i := range result.Commits {
|
||||
commits[i] = controller.MapCommit(&result.Commits[i])
|
||||
}
|
||||
|
||||
renameDetailList := make([]types.RenameDetails, len(rpcOut.RenameDetails))
|
||||
for i := range rpcOut.RenameDetails {
|
||||
renameDetails := controller.MapRenameDetails(rpcOut.RenameDetails[i])
|
||||
err = c.signatureVerifyService.VerifyCommits(ctx, repo.ID, commits)
|
||||
if err != nil {
|
||||
return types.ListCommitResponse{}, fmt.Errorf("failed to verify signature of commits: %w", err)
|
||||
}
|
||||
|
||||
renameDetailList := make([]types.RenameDetails, len(result.RenameDetails))
|
||||
for i := range result.RenameDetails {
|
||||
renameDetails := controller.MapRenameDetails(result.RenameDetails[i])
|
||||
if renameDetails == nil {
|
||||
return types.ListCommitResponse{}, fmt.Errorf("rename details was nil")
|
||||
}
|
||||
renameDetailList[i] = *renameDetails
|
||||
}
|
||||
|
||||
return types.ListCommitResponse{
|
||||
Commits: commits,
|
||||
RenameDetails: renameDetailList,
|
||||
TotalCommits: rpcOut.TotalCommits,
|
||||
TotalCommits: result.TotalCommits,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/harness/gitness/app/services/locker"
|
||||
"github.com/harness/gitness/app/services/protection"
|
||||
"github.com/harness/gitness/app/services/publicaccess"
|
||||
"github.com/harness/gitness/app/services/publickey"
|
||||
"github.com/harness/gitness/app/services/refcache"
|
||||
"github.com/harness/gitness/app/services/rules"
|
||||
"github.com/harness/gitness/app/services/settings"
|
||||
|
|
@ -87,6 +88,7 @@ func ProvideController(
|
|||
sseStreamer sse.Streamer,
|
||||
lfsCtrl *lfs.Controller,
|
||||
favoriteStore store.FavoriteStore,
|
||||
signatureVerifyService publickey.SignatureVerifyService,
|
||||
) *Controller {
|
||||
return NewController(config, tx, urlProvider,
|
||||
authorizer,
|
||||
|
|
@ -95,7 +97,7 @@ func ProvideController(
|
|||
principalInfoCache, protectionManager, rpcClient, spaceFinder, repoFinder, importer,
|
||||
codeOwners, repoReporter, indexer, limiter, locker, auditService, mtxManager, identifierCheck,
|
||||
repoChecks, publicAccess, labelSvc, instrumentation, userGroupStore, userGroupService,
|
||||
rulesSvc, sseStreamer, lfsCtrl, favoriteStore,
|
||||
rulesSvc, sseStreamer, lfsCtrl, favoriteStore, signatureVerifyService,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,17 +30,18 @@ import (
|
|||
)
|
||||
|
||||
type Controller struct {
|
||||
tx dbtx.Transactor
|
||||
principalUIDCheck check.PrincipalUID
|
||||
authorizer authz.Authorizer
|
||||
principalStore store.PrincipalStore
|
||||
tokenStore store.TokenStore
|
||||
membershipStore store.MembershipStore
|
||||
publicKeyStore store.PublicKeyStore
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore
|
||||
eventReporter *userevents.Reporter
|
||||
repoFinder refcache.RepoFinder
|
||||
favoriteStore store.FavoriteStore
|
||||
tx dbtx.Transactor
|
||||
principalUIDCheck check.PrincipalUID
|
||||
authorizer authz.Authorizer
|
||||
principalStore store.PrincipalStore
|
||||
tokenStore store.TokenStore
|
||||
membershipStore store.MembershipStore
|
||||
publicKeyStore store.PublicKeyStore
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore
|
||||
gitSignatureResultStore store.GitSignatureResultStore
|
||||
eventReporter *userevents.Reporter
|
||||
repoFinder refcache.RepoFinder
|
||||
favoriteStore store.FavoriteStore
|
||||
}
|
||||
|
||||
func NewController(
|
||||
|
|
@ -52,22 +53,24 @@ func NewController(
|
|||
membershipStore store.MembershipStore,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore,
|
||||
gitSignatureResultStore store.GitSignatureResultStore,
|
||||
eventReporter *userevents.Reporter,
|
||||
repoFinder refcache.RepoFinder,
|
||||
favoriteStore store.FavoriteStore,
|
||||
) *Controller {
|
||||
return &Controller{
|
||||
tx: tx,
|
||||
principalUIDCheck: principalUIDCheck,
|
||||
authorizer: authorizer,
|
||||
principalStore: principalStore,
|
||||
tokenStore: tokenStore,
|
||||
membershipStore: membershipStore,
|
||||
publicKeyStore: publicKeyStore,
|
||||
publicKeySubKeyStore: publicKeySubKeyStore,
|
||||
eventReporter: eventReporter,
|
||||
repoFinder: repoFinder,
|
||||
favoriteStore: favoriteStore,
|
||||
tx: tx,
|
||||
principalUIDCheck: principalUIDCheck,
|
||||
authorizer: authorizer,
|
||||
principalStore: principalStore,
|
||||
tokenStore: tokenStore,
|
||||
membershipStore: membershipStore,
|
||||
publicKeyStore: publicKeyStore,
|
||||
publicKeySubKeyStore: publicKeySubKeyStore,
|
||||
gitSignatureResultStore: gitSignatureResultStore,
|
||||
eventReporter: eventReporter,
|
||||
repoFinder: repoFinder,
|
||||
favoriteStore: favoriteStore,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package user
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -77,7 +76,7 @@ func (c *Controller) CreatePublicKey(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
key, err := publickey.ParseString(in.Content)
|
||||
key, err := publickey.ParseString(in.Content, &session.Principal)
|
||||
if err != nil {
|
||||
return nil, errors.InvalidArgument("unrecognized key content")
|
||||
}
|
||||
|
|
@ -86,18 +85,6 @@ func (c *Controller) CreatePublicKey(
|
|||
return nil, errors.InvalidArgument("key is not a valid %s key", in.Scheme)
|
||||
}
|
||||
|
||||
// SSH keys don't have an embedded identity, by PGP keys do.
|
||||
// If a key has identities, one of those must match the current user's.
|
||||
// The email address must match, the name can be different.
|
||||
if identities := key.Identities(); len(identities) > 0 {
|
||||
found := slices.ContainsFunc(identities, func(identity types.Identity) bool {
|
||||
return strings.EqualFold(identity.Email, session.Principal.Email)
|
||||
})
|
||||
if !found {
|
||||
return nil, errors.InvalidArgument("key identities don't contain the current user's email address")
|
||||
}
|
||||
}
|
||||
|
||||
switch key.Scheme() {
|
||||
case enum.PublicKeySchemeSSH:
|
||||
if in.Usage == "" {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apiauth "github.com/harness/gitness/app/api/auth"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
)
|
||||
|
||||
type UpdatePublicKeyInput struct {
|
||||
RevocationReason *enum.RevocationReason `json:"revocation_reason"`
|
||||
ValidFrom *int64 `json:"valid_from"`
|
||||
ValidTo *int64 `json:"valid_to"`
|
||||
}
|
||||
|
||||
func (in *UpdatePublicKeyInput) Sanitize() error {
|
||||
if in.RevocationReason != nil {
|
||||
if _, ok := in.RevocationReason.Sanitize(); !ok {
|
||||
return errors.InvalidArgument("invalid public key revocation reason")
|
||||
}
|
||||
|
||||
if in.ValidFrom != nil || in.ValidTo != nil {
|
||||
return errors.InvalidArgument("must either revoke the key or update its validity period")
|
||||
}
|
||||
}
|
||||
|
||||
if in.ValidFrom != nil && in.ValidTo != nil {
|
||||
if *in.ValidFrom > *in.ValidTo {
|
||||
return errors.InvalidArgument("invalid validity period")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) UpdatePublicKey(
|
||||
ctx context.Context,
|
||||
session *auth.Session,
|
||||
userUID string,
|
||||
identifier string,
|
||||
in *UpdatePublicKeyInput,
|
||||
) (*types.PublicKey, error) {
|
||||
user, err := c.principalStore.FindUserByUID(ctx, userUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch user by uid: %w", err)
|
||||
}
|
||||
|
||||
if err = apiauth.CheckUser(ctx, c.authorizer, session, user, enum.PermissionUserEdit); err != nil {
|
||||
return nil, fmt.Errorf("access check failed: %w", err)
|
||||
}
|
||||
|
||||
if err := in.Sanitize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := c.publicKeyStore.FindByIdentifier(ctx, user.ID, identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find public key by identifier: %w", err)
|
||||
}
|
||||
|
||||
if key.RevocationReason != nil {
|
||||
if in.ValidFrom != nil || in.ValidTo != nil {
|
||||
return nil, errors.InvalidArgument("can't update the validity period of revoked keys")
|
||||
}
|
||||
|
||||
if *key.RevocationReason == enum.RevocationReasonCompromised {
|
||||
return nil, errors.InvalidArgument("can't update the revocation reason of compromised keys")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
changedRevocationReason bool
|
||||
changedValidityPeriod bool
|
||||
)
|
||||
|
||||
if in.RevocationReason != nil &&
|
||||
(key.RevocationReason == nil || *key.RevocationReason != *in.RevocationReason) {
|
||||
now := time.Now().UnixMilli()
|
||||
key.RevocationReason = in.RevocationReason
|
||||
if key.ValidTo == nil || *key.ValidTo > now {
|
||||
key.ValidTo = &now
|
||||
}
|
||||
changedRevocationReason = true
|
||||
}
|
||||
|
||||
isTimestampChanged := func(ts1, ts2 *int64) bool {
|
||||
return ts1 != nil && ts2 != nil && *ts1 != *ts2 ||
|
||||
ts1 == nil && ts2 != nil ||
|
||||
ts1 != nil && ts2 == nil
|
||||
}
|
||||
|
||||
if in.ValidFrom != nil {
|
||||
if *in.ValidFrom == 0 { // zero means clear
|
||||
in.ValidFrom = nil
|
||||
}
|
||||
|
||||
if isTimestampChanged(key.ValidFrom, in.ValidFrom) {
|
||||
key.ValidFrom = in.ValidFrom
|
||||
changedValidityPeriod = true
|
||||
}
|
||||
}
|
||||
|
||||
if in.ValidTo != nil {
|
||||
if *in.ValidTo == 0 { // zero means clear
|
||||
in.ValidTo = nil
|
||||
}
|
||||
|
||||
if isTimestampChanged(key.ValidTo, in.ValidTo) {
|
||||
key.ValidTo = in.ValidTo
|
||||
changedValidityPeriod = true
|
||||
}
|
||||
}
|
||||
|
||||
if !changedRevocationReason && !changedValidityPeriod {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
|
||||
err = c.publicKeyStore.Update(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update public key: %w", err)
|
||||
}
|
||||
|
||||
if changedRevocationReason && *key.RevocationReason == enum.RevocationReasonCompromised {
|
||||
switch key.Scheme {
|
||||
case enum.PublicKeySchemePGP:
|
||||
subKeys, err := c.publicKeySubKeyStore.List(ctx, key.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list subkeys: %w", err)
|
||||
}
|
||||
|
||||
err = c.gitSignatureResultStore.UpdateAll(
|
||||
ctx,
|
||||
enum.GitSignatureRevoked,
|
||||
key.PrincipalID,
|
||||
subKeys,
|
||||
nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke all PGP signatures for keys %v: %w", subKeys, err)
|
||||
}
|
||||
|
||||
case enum.PublicKeySchemeSSH:
|
||||
fingerprints := []string{key.Fingerprint}
|
||||
err := c.gitSignatureResultStore.UpdateAll(
|
||||
ctx,
|
||||
enum.GitSignatureRevoked,
|
||||
key.PrincipalID,
|
||||
nil,
|
||||
fingerprints)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke all SSH signatures for key %v: %w", fingerprints, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ func ProvideController(
|
|||
membershipStore store.MembershipStore,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore,
|
||||
gitSignatureResultStore store.GitSignatureResultStore,
|
||||
eventReporter *userevents.Reporter,
|
||||
repoFinder refcache.RepoFinder,
|
||||
favoriteStore store.FavoriteStore,
|
||||
|
|
@ -51,6 +52,7 @@ func ProvideController(
|
|||
membershipStore,
|
||||
publicKeyStore,
|
||||
publicKeySubKeyStore,
|
||||
gitSignatureResultStore,
|
||||
eventReporter,
|
||||
repoFinder,
|
||||
favoriteStore)
|
||||
|
|
|
|||
|
|
@ -118,6 +118,25 @@ func MapCommit(c *git.Commit) *types.Commit {
|
|||
}
|
||||
}
|
||||
|
||||
func MapCommitTag(t git.CommitTag) types.CommitTag {
|
||||
var tagger *types.Signature
|
||||
if t.Tagger != nil {
|
||||
tagger = &types.Signature{}
|
||||
*tagger = MapSignature(*t.Tagger)
|
||||
}
|
||||
|
||||
return types.CommitTag{
|
||||
Name: t.Name,
|
||||
SHA: t.SHA,
|
||||
IsAnnotated: t.IsAnnotated,
|
||||
Title: t.Title,
|
||||
Message: t.Message,
|
||||
Tagger: tagger,
|
||||
SignedData: (*types.SignedData)(t.SignedData),
|
||||
Commit: MapCommit(t.Commit),
|
||||
}
|
||||
}
|
||||
|
||||
func mapStats(c *git.Commit) *types.CommitStats {
|
||||
if len(c.FileStats) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ import (
|
|||
"github.com/harness/gitness/app/api/request"
|
||||
)
|
||||
|
||||
/*
|
||||
* Writes json-encoded commit tag information to the http response body.
|
||||
*/
|
||||
func HandleListCommitTags(repoCtrl *repo.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 user
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/app/api/controller/user"
|
||||
"github.com/harness/gitness/app/api/render"
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
)
|
||||
|
||||
func HandleUpdatePublicKey(userCtrl *user.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
userUID := session.Principal.UID
|
||||
|
||||
id, err := request.GetPublicKeyIdentifierFromPath(r)
|
||||
if err != nil {
|
||||
render.BadRequest(ctx, w)
|
||||
return
|
||||
}
|
||||
|
||||
in := new(user.UpdatePublicKeyInput)
|
||||
err = json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
key, err := userCtrl.UpdatePublicKey(ctx, session, userUID, id, in)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, http.StatusOK, key)
|
||||
}
|
||||
}
|
||||
|
|
@ -1123,7 +1123,7 @@ func repoOperations(reflector *openapi3.Reflector) {
|
|||
queryParameterQueryTags, queryParameterOrder, queryParameterSortTags,
|
||||
QueryParameterPage, QueryParameterLimit)
|
||||
_ = reflector.SetRequest(&opListTags, new(listTagsRequest), http.MethodGet)
|
||||
_ = reflector.SetJSONResponse(&opListTags, []repo.CommitTag{}, http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opListTags, []*types.CommitTag{}, http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opListTags, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opListTags, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opListTags, new(usererror.Error), http.StatusForbidden)
|
||||
|
|
@ -1134,7 +1134,7 @@ func repoOperations(reflector *openapi3.Reflector) {
|
|||
opCreateTag.WithTags("repository")
|
||||
opCreateTag.WithMapOfAnything(map[string]interface{}{"operationId": "createTag"})
|
||||
_ = reflector.SetRequest(&opCreateTag, new(createTagRequest), http.MethodPost)
|
||||
_ = reflector.SetJSONResponse(&opCreateTag, new(repo.CommitTag), http.StatusCreated)
|
||||
_ = reflector.SetJSONResponse(&opCreateTag, new(types.CommitTag), http.StatusCreated)
|
||||
_ = reflector.SetJSONResponse(&opCreateTag, new(usererror.Error), http.StatusBadRequest)
|
||||
_ = reflector.SetJSONResponse(&opCreateTag, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opCreateTag, new(usererror.Error), http.StatusUnauthorized)
|
||||
|
|
|
|||
|
|
@ -182,6 +182,18 @@ func buildUser(reflector *openapi3.Reflector) {
|
|||
_ = reflector.SetJSONResponse(&opKeyDelete, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.Spec.AddOperation(http.MethodDelete, "/user/keys/{public_key_identifier}", opKeyDelete)
|
||||
|
||||
opKeyUpdate := openapi3.Operation{}
|
||||
opKeyUpdate.WithTags("user")
|
||||
opKeyUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updatePublicKey"})
|
||||
_ = reflector.SetRequest(&opKeyUpdate, struct {
|
||||
ID string `path:"public_key_identifier"`
|
||||
}{}, http.MethodPatch)
|
||||
_ = reflector.SetJSONResponse(&opKeyUpdate, &types.PublicKey{}, http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opKeyUpdate, new(usererror.Error), http.StatusBadRequest)
|
||||
_ = reflector.SetJSONResponse(&opKeyUpdate, new(usererror.Error), http.StatusNotFound)
|
||||
_ = reflector.SetJSONResponse(&opKeyUpdate, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.Spec.AddOperation(http.MethodPatch, "/user/keys/{public_key_identifier}", opKeyUpdate)
|
||||
|
||||
opKeyList := openapi3.Operation{}
|
||||
opKeyList.WithTags("user")
|
||||
opKeyList.WithMapOfAnything(map[string]interface{}{"operationId": "listPublicKey"})
|
||||
|
|
|
|||
|
|
@ -806,6 +806,8 @@ func setupUser(r chi.Router, userCtrl *user.Controller) {
|
|||
r.Post("/", handleruser.HandleCreatePublicKey(userCtrl))
|
||||
r.Delete(fmt.Sprintf("/{%s}", request.PathParamPublicKeyIdentifier),
|
||||
handleruser.HandleDeletePublicKey(userCtrl))
|
||||
r.Patch(fmt.Sprintf("/{%s}", request.PathParamPublicKeyIdentifier),
|
||||
handleruser.HandleUpdatePublicKey(userCtrl))
|
||||
})
|
||||
|
||||
// Favorites
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package publickey
|
||||
package keypgp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type PGPKeyMetadata struct {
|
||||
type KeyMetadata struct {
|
||||
// ID of the key.
|
||||
ID string `json:"id"`
|
||||
|
||||
|
|
@ -54,43 +54,43 @@ type PGPKeyMetadata struct {
|
|||
BitLength uint16 `json:"bit_length"`
|
||||
}
|
||||
|
||||
type PGPEntityMetadata struct {
|
||||
type EntityMetadata struct {
|
||||
PrimaryIdentity *types.Identity `json:"primary_identity,omitempty"`
|
||||
Identities []types.Identity `json:"identities,omitempty"`
|
||||
PrimaryKey PGPKeyMetadata `json:"primary_key"`
|
||||
SubKeys []PGPKeyMetadata `json:"sub_keys,omitempty"`
|
||||
PrimaryKey KeyMetadata `json:"primary_key"`
|
||||
SubKeys []KeyMetadata `json:"sub_keys,omitempty"`
|
||||
}
|
||||
|
||||
func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
||||
func Parse(r io.Reader, principal *types.Principal) (KeyInfo, error) {
|
||||
keyRing, err := openpgp.ReadArmoredKeyRing(r)
|
||||
if err != nil {
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("failed to read PGP key ring: %s", err.Error())
|
||||
return KeyInfo{}, errors.InvalidArgument("failed to read PGP key ring: %s", err.Error())
|
||||
}
|
||||
|
||||
if len(keyRing) == 0 {
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("PGP key ring contains no keys")
|
||||
return KeyInfo{}, errors.InvalidArgument("PGP key ring contains no keys")
|
||||
}
|
||||
|
||||
if len(keyRing) > 1 {
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("can't accept a PGP key ring with multiple primary keys")
|
||||
return KeyInfo{}, errors.InvalidArgument("can't accept a PGP key ring with multiple primary keys")
|
||||
}
|
||||
|
||||
keyEntity := keyRing[0]
|
||||
|
||||
if keyEntity == nil || keyEntity.PrimaryKey == nil {
|
||||
// Should not happen.
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("PGP key ring entity is nil")
|
||||
return KeyInfo{}, errors.InvalidArgument("PGP key ring entity is nil")
|
||||
}
|
||||
|
||||
if keyEntity.PrivateKey != nil {
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("refusing to accept private key: please upload a public key")
|
||||
return KeyInfo{}, errors.InvalidArgument("refusing to accept private key: please upload a public key")
|
||||
}
|
||||
|
||||
primarySignature, primaryIdentity := keyEntity.PrimarySelfSignature()
|
||||
|
||||
if primarySignature == nil {
|
||||
// Should not happen.
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("PGP key entity is missing primary signature")
|
||||
return KeyInfo{}, errors.InvalidArgument("PGP key entity is missing primary signature")
|
||||
}
|
||||
|
||||
// Extract the validity period from the key's primary signature.
|
||||
|
|
@ -100,6 +100,13 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
var identity *types.Identity
|
||||
var comment string
|
||||
|
||||
foundPrincipal := false
|
||||
if principal == nil {
|
||||
// If principal is nil, it means that no particular principal is needed.
|
||||
// By `foundPrincipal = true` we declare that we have "found" it.
|
||||
foundPrincipal = true
|
||||
}
|
||||
|
||||
// Process the primary identity (name and email address for the key) if it exists.
|
||||
// The identity can also have revocations. We ignore the revocation reason, but honor
|
||||
// the validity period. The final validity period for the key is intersection between
|
||||
|
|
@ -114,6 +121,8 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
Email: primaryIdentity.UserId.Email,
|
||||
}
|
||||
|
||||
foundPrincipal = foundPrincipal || strings.EqualFold(identity.Email, principal.Email)
|
||||
|
||||
comment = primaryIdentity.UserId.Comment
|
||||
}
|
||||
|
||||
|
|
@ -123,12 +132,20 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
Name: ident.UserId.Name,
|
||||
Email: ident.UserId.Email,
|
||||
})
|
||||
|
||||
foundPrincipal = foundPrincipal || strings.EqualFold(ident.UserId.Email, principal.Email)
|
||||
}
|
||||
|
||||
var subKeys []PGPKeyMetadata
|
||||
// PGP keys can have multiple identities and one of those must match the current user's.
|
||||
// The email address must match, the name can be different.
|
||||
if !foundPrincipal {
|
||||
return KeyInfo{}, errors.InvalidArgument("key identities don't contain the user's email address")
|
||||
}
|
||||
|
||||
var subKeys []KeyMetadata
|
||||
for _, subKey := range keyEntity.Subkeys {
|
||||
if subKey.PublicKey == nil || subKey.Sig == nil {
|
||||
return PGPKeyInfo{}, errors.InvalidArgument("found a subkey without public key")
|
||||
return KeyInfo{}, errors.InvalidArgument("found a subkey without public key")
|
||||
}
|
||||
|
||||
// We'll only consider keys than can be used for signing
|
||||
|
|
@ -142,7 +159,7 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
|
||||
subKeyValidFrom, subKeyValidTo := validitySubkey.Milliseconds()
|
||||
bits, _ := subKey.PublicKey.BitLength()
|
||||
subKeys = append(subKeys, PGPKeyMetadata{
|
||||
subKeys = append(subKeys, KeyMetadata{
|
||||
ID: subKey.PublicKey.KeyIdString(),
|
||||
Fingerprint: fmt.Sprintf("%X", subKey.PublicKey.Fingerprint),
|
||||
RevocationReason: getRevocationReason(subKey.Revocations),
|
||||
|
|
@ -155,10 +172,10 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
|
||||
keyValidFrom, keyValidTo := validityKey.Milliseconds()
|
||||
bits, _ := keyEntity.PrimaryKey.BitLength()
|
||||
metadata := PGPEntityMetadata{
|
||||
metadata := EntityMetadata{
|
||||
PrimaryIdentity: identity,
|
||||
Identities: identities,
|
||||
PrimaryKey: PGPKeyMetadata{
|
||||
PrimaryKey: KeyMetadata{
|
||||
ID: keyEntity.PrimaryKey.KeyIdString(),
|
||||
Fingerprint: fmt.Sprintf("%X", keyEntity.PrimaryKey.Fingerprint),
|
||||
RevocationReason: getRevocationReason(keyEntity.Revocations),
|
||||
|
|
@ -170,7 +187,7 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
SubKeys: subKeys,
|
||||
}
|
||||
|
||||
keyInfo := PGPKeyInfo{
|
||||
keyInfo := KeyInfo{
|
||||
entity: keyEntity,
|
||||
metadata: metadata,
|
||||
validFrom: keyValidFrom,
|
||||
|
|
@ -181,20 +198,20 @@ func parsePGP(r io.Reader) (PGPKeyInfo, error) {
|
|||
return keyInfo, nil
|
||||
}
|
||||
|
||||
type PGPKeyInfo struct {
|
||||
type KeyInfo struct {
|
||||
// entity holds the original PGP key
|
||||
entity *openpgp.Entity
|
||||
|
||||
// metadata holds additional key info
|
||||
metadata PGPEntityMetadata
|
||||
metadata EntityMetadata
|
||||
|
||||
validFrom int64
|
||||
validTo *int64
|
||||
comment string
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Matches(s string) bool {
|
||||
otherKey, err := parsePGP(strings.NewReader(s))
|
||||
func (key KeyInfo) Matches(s string) bool {
|
||||
otherKey, err := Parse(strings.NewReader(s), nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -212,45 +229,46 @@ func (key PGPKeyInfo) Matches(s string) bool {
|
|||
return slices.Equal(buf1.Bytes(), buf2.Bytes())
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Fingerprint() string {
|
||||
func (key KeyInfo) Fingerprint() string {
|
||||
return key.metadata.PrimaryKey.Fingerprint
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Type() string {
|
||||
func (key KeyInfo) Type() string {
|
||||
return pgpAlgo(key.entity.PrimaryKey.PubKeyAlgo)
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Scheme() enum.PublicKeyScheme {
|
||||
func (key KeyInfo) Scheme() enum.PublicKeyScheme {
|
||||
return enum.PublicKeySchemePGP
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Comment() string {
|
||||
func (key KeyInfo) Comment() string {
|
||||
return key.comment
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) ValidFrom() *int64 {
|
||||
func (key KeyInfo) ValidFrom() *int64 {
|
||||
return &key.validFrom
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) ValidTo() *int64 {
|
||||
func (key KeyInfo) ValidTo() *int64 {
|
||||
return key.validTo
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Identities() []types.Identity {
|
||||
func (key KeyInfo) Identities() []types.Identity {
|
||||
return key.metadata.Identities
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) RevocationReason() *enum.RevocationReason {
|
||||
func (key KeyInfo) RevocationReason() *enum.RevocationReason {
|
||||
return key.metadata.PrimaryKey.RevocationReason
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) Metadata() json.RawMessage {
|
||||
func (key KeyInfo) Metadata() json.RawMessage {
|
||||
data, _ := json.Marshal(key.metadata)
|
||||
return data
|
||||
}
|
||||
|
||||
func (key PGPKeyInfo) SubKeyIDs() []string {
|
||||
func (key KeyInfo) SubKeyIDs() []string {
|
||||
subKeyIDs := make([]string, 0)
|
||||
subKeyIDs = append(subKeyIDs, key.entity.PrimaryKey.KeyIdString())
|
||||
for i := range key.entity.Subkeys {
|
||||
if key.entity.Subkeys[i].PublicKey.CanSign() {
|
||||
subKeyIDs = append(subKeyIDs, key.entity.Subkeys[i].PublicKey.KeyIdString())
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 keypgp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
SignatureType = "PGP SIGNATURE"
|
||||
)
|
||||
|
||||
type Verify struct {
|
||||
signature []byte
|
||||
keyID string
|
||||
keyFingerprint string
|
||||
}
|
||||
|
||||
func (v *Verify) Parse(
|
||||
ctx context.Context,
|
||||
signature []byte,
|
||||
objectSHA sha.SHA,
|
||||
) enum.GitSignatureResult {
|
||||
block, err := armor.Decode(bytes.NewReader(signature))
|
||||
if err != nil || block == nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to decode signature")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
if block.Type != openpgp.SignatureType {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("signature_type", block.Type).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("unexpected PGP signature block type")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
reader := packet.NewReader(block.Body)
|
||||
sig, err := reader.Next()
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to read PGP signature")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
p, ok := sig.(*packet.Signature)
|
||||
if !ok {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("signature_type", fmt.Sprintf("%T", sig)).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("signature type mismatch")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
if p.IssuerKeyId == nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("no public key ID in PGP signature")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
v.signature = signature
|
||||
v.keyID = fmt.Sprintf("%016X", *p.IssuerKeyId)
|
||||
v.keyFingerprint = fmt.Sprintf("%X", p.IssuerFingerprint)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *Verify) Key(
|
||||
ctx context.Context,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
principalID int64,
|
||||
) (*types.PublicKey, error) {
|
||||
schemes := []enum.PublicKeyScheme{enum.PublicKeySchemePGP}
|
||||
usages := []enum.PublicKeyUsage{enum.PublicKeyUsageSign}
|
||||
keys, err := publicKeyStore.ListBySubKeyID(ctx, v.KeyID(), &principalID, usages, schemes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list PGP public keys by subkey ID: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
//nolint:nilnil
|
||||
return nil, nil // No key is available and there is no error.
|
||||
}
|
||||
|
||||
return &keys[0], nil
|
||||
}
|
||||
|
||||
func (v *Verify) Verify(
|
||||
ctx context.Context,
|
||||
armoredPublicKey []byte,
|
||||
signedContent []byte,
|
||||
objectSHA sha.SHA,
|
||||
committer types.Signature,
|
||||
) enum.GitSignatureResult {
|
||||
keyRingReader := bytes.NewReader(armoredPublicKey)
|
||||
keyRing, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to read key ring")
|
||||
return enum.GitSignatureUnverified
|
||||
}
|
||||
|
||||
// CheckArmoredDetachedSignature returns an error if:
|
||||
// - The signature (or one of the binding signatures mentioned below)
|
||||
// has a unknown critical notation data subpacket
|
||||
// - The primary key of the signing entity is revoked
|
||||
// - The primary identity is revoked
|
||||
// - The signature is expired
|
||||
// - The primary key of the signing entity is expired according to the
|
||||
// primary identity binding signature
|
||||
//
|
||||
// ... or, if the signature was signed by a subkey and:
|
||||
// - The signing subkey is revoked
|
||||
// - The signing subkey is expired according to the subkey binding signature
|
||||
// - The signing subkey binding signature is expired
|
||||
// - The signing subkey cross-signature is expired
|
||||
//
|
||||
// NOTE: The order of these checks is important, as the caller may choose to
|
||||
// ignore ErrSignatureExpired or ErrKeyExpired errors, but should never
|
||||
// ignore any other errors.
|
||||
// NOTE 2: The comment above is copied from the openpgp library.
|
||||
signer, err := openpgp.CheckArmoredDetachedSignature(
|
||||
keyRing,
|
||||
bytes.NewReader(signedContent),
|
||||
bytes.NewReader(v.signature),
|
||||
&packet.Config{
|
||||
Time: func() time.Time {
|
||||
return committer.When
|
||||
},
|
||||
},
|
||||
)
|
||||
// If error happened, try to convert it to one of the enum values.
|
||||
//nolint:nestif
|
||||
if err != nil {
|
||||
var errUnsupported pgperrors.UnsupportedError
|
||||
if errors.As(err, &errUnsupported); errUnsupported != "" {
|
||||
return enum.GitSignatureUnsupported
|
||||
}
|
||||
|
||||
if errors.Is(err, pgperrors.ErrKeyRevoked) {
|
||||
return enum.GitSignatureRevoked
|
||||
}
|
||||
|
||||
if errors.Is(err, pgperrors.ErrUnknownIssuer) {
|
||||
// This shouldn't happen because we fetched the key by ID,
|
||||
// so we are using the correct key with the correct identity.
|
||||
return enum.GitSignatureBad
|
||||
}
|
||||
|
||||
if errors.Is(err, pgperrors.ErrKeyExpired) {
|
||||
return enum.GitSignatureKeyExpired
|
||||
}
|
||||
|
||||
if errors.Is(err, pgperrors.ErrSignatureExpired) {
|
||||
return enum.GitSignatureBad
|
||||
}
|
||||
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("error_type", fmt.Sprintf("%T", err)).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("unrecognized error")
|
||||
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
var signatureIdentity *openpgp.Identity
|
||||
for _, identity := range signer.Identities {
|
||||
if strings.EqualFold(committer.Identity.Email, identity.UserId.Email) {
|
||||
signatureIdentity = identity
|
||||
}
|
||||
}
|
||||
if signatureIdentity == nil {
|
||||
return enum.GitSignatureBad
|
||||
}
|
||||
|
||||
if signatureIdentity.Revoked(committer.When) {
|
||||
return enum.GitSignatureRevoked
|
||||
}
|
||||
|
||||
return enum.GitSignatureGood
|
||||
}
|
||||
|
||||
func (v *Verify) KeyScheme() enum.PublicKeyScheme {
|
||||
return enum.PublicKeySchemePGP
|
||||
}
|
||||
|
||||
func (v *Verify) KeyID() string {
|
||||
return v.keyID
|
||||
}
|
||||
|
||||
func (v *Verify) KeyFingerprint() string {
|
||||
return v.keyFingerprint
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package publickey
|
||||
package keyssh
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
|
@ -29,31 +29,31 @@ import (
|
|||
)
|
||||
|
||||
func FromSSH(key gossh.PublicKey) KeyInfo {
|
||||
return SSHKeyInfo{
|
||||
return KeyInfo{
|
||||
PublicKey: key,
|
||||
KeyComment: "",
|
||||
}
|
||||
}
|
||||
|
||||
func parseSSH(keyData []byte) (SSHKeyInfo, error) {
|
||||
func Parse(keyData []byte) (KeyInfo, error) {
|
||||
publicKey, comment, _, _, err := gossh.ParseAuthorizedKey(keyData)
|
||||
if err != nil {
|
||||
return SSHKeyInfo{}, errors.InvalidArgument("invalid SSH key data: %s" + err.Error())
|
||||
return KeyInfo{}, errors.InvalidArgument("invalid SSH key data: %s" + err.Error())
|
||||
}
|
||||
|
||||
keyType := publicKey.Type()
|
||||
|
||||
// explicitly disallowed
|
||||
if slices.Contains(DisallowedTypes, keyType) {
|
||||
return SSHKeyInfo{}, errors.InvalidArgument("keys of type %s are not allowed", keyType)
|
||||
return KeyInfo{}, errors.InvalidArgument("keys of type %s are not allowed", keyType)
|
||||
}
|
||||
|
||||
// only allowed
|
||||
if !slices.Contains(AllowedTypes, keyType) {
|
||||
return SSHKeyInfo{}, errors.InvalidArgument("allowed key types are %v", AllowedTypes)
|
||||
return KeyInfo{}, errors.InvalidArgument("allowed key types are %v", AllowedTypes)
|
||||
}
|
||||
|
||||
return SSHKeyInfo{
|
||||
return KeyInfo{
|
||||
PublicKey: publicKey,
|
||||
KeyComment: comment,
|
||||
}, nil
|
||||
|
|
@ -73,12 +73,12 @@ var DisallowedTypes = []string{
|
|||
gossh.KeyAlgoDSA,
|
||||
}
|
||||
|
||||
type SSHKeyInfo struct {
|
||||
type KeyInfo struct {
|
||||
PublicKey gossh.PublicKey
|
||||
KeyComment string
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Matches(s string) bool {
|
||||
func (key KeyInfo) Matches(s string) bool {
|
||||
otherKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(s))
|
||||
if err != nil {
|
||||
return false
|
||||
|
|
@ -87,48 +87,48 @@ func (key SSHKeyInfo) Matches(s string) bool {
|
|||
return key.matchesKey(otherKey)
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) matchesKey(otherKey gossh.PublicKey) bool {
|
||||
func (key KeyInfo) matchesKey(otherKey gossh.PublicKey) bool {
|
||||
return ssh.KeysEqual(key.PublicKey, otherKey)
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Fingerprint() string {
|
||||
func (key KeyInfo) Fingerprint() string {
|
||||
sum := sha256.New()
|
||||
sum.Write(key.PublicKey.Marshal())
|
||||
return "SHA256:" + base64.RawStdEncoding.EncodeToString(sum.Sum(nil))
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Type() string {
|
||||
func (key KeyInfo) Type() string {
|
||||
return key.PublicKey.Type()
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Scheme() enum.PublicKeyScheme {
|
||||
func (key KeyInfo) Scheme() enum.PublicKeyScheme {
|
||||
return enum.PublicKeySchemeSSH
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Comment() string {
|
||||
func (key KeyInfo) Comment() string {
|
||||
return key.KeyComment
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) ValidFrom() *int64 {
|
||||
func (key KeyInfo) ValidFrom() *int64 {
|
||||
return nil // SSH keys do not have validity period
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) ValidTo() *int64 {
|
||||
func (key KeyInfo) ValidTo() *int64 {
|
||||
return nil // SSH keys do not have validity period
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Identities() []types.Identity {
|
||||
func (key KeyInfo) Identities() []types.Identity {
|
||||
return nil // SSH keys do not have identities
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) RevocationReason() *enum.RevocationReason {
|
||||
return nil
|
||||
func (key KeyInfo) RevocationReason() *enum.RevocationReason {
|
||||
return nil // SSH keys do not have revocations
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) Metadata() json.RawMessage {
|
||||
func (key KeyInfo) Metadata() json.RawMessage {
|
||||
return json.RawMessage("{}")
|
||||
}
|
||||
|
||||
func (key SSHKeyInfo) SubKeyIDs() []string {
|
||||
return nil
|
||||
func (key KeyInfo) SubKeyIDs() []string {
|
||||
return nil // SSH keys do not have subkeys
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 keyssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const SignatureType = "SSH SIGNATURE"
|
||||
|
||||
// signatureBlob describes a lightweight SSH signature format.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L34
|
||||
type signatureBlob struct {
|
||||
MagicPreamble [6]byte
|
||||
Version uint32
|
||||
PublicKey []byte
|
||||
Namespace string
|
||||
Reserved string
|
||||
HashAlgorithm string
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// messageWrapper represents SSH signed data.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L81
|
||||
type messageWrapper struct {
|
||||
Namespace string
|
||||
Reserved string
|
||||
HashAlgorithm string
|
||||
Hash []byte
|
||||
}
|
||||
|
||||
// hashFunc returns hash function used for SSH signature verification.
|
||||
// Data to be signed is first hashed with the specified hash_algorithm.
|
||||
// This is done to limit the amount of data presented to the signature
|
||||
// operation, which may be of concern if the signing key is held in limited
|
||||
// or slow hardware or on a remote ssh-agent. The supported hash algorithms
|
||||
// are "sha256" and "sha512".
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L63
|
||||
func hashFunc(hashAlgorithm string) hash.Hash {
|
||||
switch hashAlgorithm {
|
||||
case "sha256":
|
||||
return sha256.New()
|
||||
case "sha512":
|
||||
return sha512.New()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
sshMagicPreamble = "SSHSIG"
|
||||
sshNamespace = "git"
|
||||
)
|
||||
|
||||
type Verify struct {
|
||||
hashAlgorithm string
|
||||
signatureBytes []byte
|
||||
publicKey []byte
|
||||
keyFingerprint string
|
||||
}
|
||||
|
||||
// Parse parses the provided ASCII-armored signature and returns fingerprint of the key used to sign it.
|
||||
// Also, it updates the internal object fields required for key validation.
|
||||
func (v *Verify) Parse(
|
||||
ctx context.Context,
|
||||
signature []byte,
|
||||
objectSHA sha.SHA,
|
||||
) enum.GitSignatureResult {
|
||||
block, _ := pem.Decode(signature)
|
||||
if block == nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to decode signature")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
if block.Type != SignatureType {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("signature_type", block.Type).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("unexpected SSH signature block type")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
var blob signatureBlob
|
||||
if err := ssh.Unmarshal(block.Bytes, &blob); err != nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to unmarshal SSH signature")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
// The preamble is the six-byte sequence "SSHSIG". It is included to
|
||||
// ensure that manual signatures can never be confused with any message
|
||||
// signed during SSH user or host authentication.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L89
|
||||
if !bytes.Equal(blob.MagicPreamble[:], []byte(sshMagicPreamble)) {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("invalid SSH signature magic preamble")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
// Verifiers MUST reject signatures with versions greater than those they support.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L50
|
||||
if blob.Version > 1 {
|
||||
return enum.GitSignatureUnsupported
|
||||
}
|
||||
|
||||
// The purpose of the namespace value is to specify a unambiguous
|
||||
// interpretation domain for the signature, e.g. file signing.
|
||||
// This prevents cross-protocol attacks caused by signatures
|
||||
// intended for one intended domain being accepted in another.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L53
|
||||
if blob.Namespace != sshNamespace {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("namespace", blob.Namespace).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("SSH signature namespace mismatch")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
publicKey, err := ssh.ParsePublicKey(blob.PublicKey)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("invalid SSH signature public key")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
v.hashAlgorithm = blob.HashAlgorithm
|
||||
v.signatureBytes = blob.Signature
|
||||
v.publicKey = blob.PublicKey
|
||||
v.keyFingerprint = ssh.FingerprintSHA256(publicKey)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *Verify) Key(
|
||||
ctx context.Context,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
principalID int64,
|
||||
) (*types.PublicKey, error) {
|
||||
schemes := []enum.PublicKeyScheme{enum.PublicKeySchemeSSH}
|
||||
usages := []enum.PublicKeyUsage{enum.PublicKeyUsageSign}
|
||||
keys, err := publicKeyStore.ListByFingerprint(ctx, v.KeyFingerprint(), &principalID, usages, schemes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list SSH public keys by fingerprint: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
//nolint:nilnil
|
||||
return nil, nil // No key is available and there is no error.
|
||||
}
|
||||
|
||||
return &keys[0], nil
|
||||
}
|
||||
|
||||
func (v *Verify) Verify(
|
||||
ctx context.Context,
|
||||
publicKeyRaw []byte,
|
||||
signedContent []byte,
|
||||
objectSHA sha.SHA,
|
||||
_ types.Signature,
|
||||
) enum.GitSignatureResult {
|
||||
publicKey, _, _, _, err := ssh.ParseAuthorizedKey(publicKeyRaw)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to parse SSH key")
|
||||
return enum.GitSignatureUnverified
|
||||
}
|
||||
|
||||
hashAlgorithm := v.hashAlgorithm
|
||||
signatureBytes := v.signatureBytes
|
||||
|
||||
h := hashFunc(hashAlgorithm)
|
||||
if h == nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str("hash_algorithm", v.hashAlgorithm).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("unrecognized SSH signature algorithm")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
h.Write(signedContent)
|
||||
|
||||
hashSum := h.Sum(nil)
|
||||
|
||||
sig := ssh.Signature{}
|
||||
if err := ssh.Unmarshal(signatureBytes, &sig); err != nil {
|
||||
log.Ctx(ctx).Warn().
|
||||
Err(err).
|
||||
Str("object_sha", objectSHA.String()).
|
||||
Msg("failed to unmarshal SSH signature")
|
||||
return enum.GitSignatureInvalid
|
||||
}
|
||||
|
||||
signedMessage := ssh.Marshal(messageWrapper{
|
||||
Namespace: sshNamespace,
|
||||
HashAlgorithm: hashAlgorithm,
|
||||
Hash: hashSum,
|
||||
})
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_, _ = buf.WriteString(sshMagicPreamble)
|
||||
_, _ = buf.Write(signedMessage)
|
||||
err = publicKey.Verify(buf.Bytes(), &sig)
|
||||
if err != nil {
|
||||
return enum.GitSignatureBad
|
||||
}
|
||||
|
||||
return enum.GitSignatureGood
|
||||
}
|
||||
|
||||
func (v *Verify) KeyScheme() enum.PublicKeyScheme {
|
||||
return enum.PublicKeySchemeSSH
|
||||
}
|
||||
|
||||
func (v *Verify) KeyID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *Verify) KeyFingerprint() string {
|
||||
return v.keyFingerprint
|
||||
}
|
||||
|
||||
func (v *Verify) SignaturePublicKey() []byte {
|
||||
return v.publicKey
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/app/services/publickey/keypgp"
|
||||
"github.com/harness/gitness/app/services/publickey/keyssh"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
|
@ -42,7 +44,7 @@ type KeyInfo interface {
|
|||
SubKeyIDs() []string
|
||||
}
|
||||
|
||||
func ParseString(keyData string) (KeyInfo, error) {
|
||||
func ParseString(keyData string, principal *types.Principal) (KeyInfo, error) {
|
||||
if len(keyData) == 0 {
|
||||
return nil, errors.InvalidArgument("empty key")
|
||||
}
|
||||
|
|
@ -51,7 +53,7 @@ func ParseString(keyData string) (KeyInfo, error) {
|
|||
const pgpFooter = "-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
if strings.HasPrefix(keyData, pgpHeader) && strings.HasSuffix(keyData, pgpFooter) {
|
||||
key, err := parsePGP(strings.NewReader(keyData))
|
||||
key, err := keypgp.Parse(strings.NewReader(keyData), principal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse PGP key: %w", err)
|
||||
}
|
||||
|
|
@ -59,7 +61,7 @@ func ParseString(keyData string) (KeyInfo, error) {
|
|||
return key, nil
|
||||
}
|
||||
|
||||
key, err := parseSSH([]byte(keyData))
|
||||
key, err := keyssh.Parse([]byte(keyData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse SSH key: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/services/publickey/keyssh"
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/types"
|
||||
|
|
@ -56,7 +57,7 @@ func (s sshAuthService) ValidateKey(
|
|||
_ string,
|
||||
publicKey ssh.PublicKey,
|
||||
) (*types.PrincipalInfo, error) {
|
||||
key := FromSSH(publicKey)
|
||||
key := keyssh.FromSSH(publicKey)
|
||||
fingerprint := key.Fingerprint()
|
||||
|
||||
existingKeys, err := s.publicKeyStore.ListByFingerprint(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,407 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 publickey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/services/publickey/keypgp"
|
||||
"github.com/harness/gitness/app/services/publickey/keyssh"
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
gitness_store "github.com/harness/gitness/store"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type SignatureVerifyService struct {
|
||||
principalStore store.PrincipalStore
|
||||
publicKeyStore store.PublicKeyStore
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore
|
||||
gitSignatureResultStore store.GitSignatureResultStore
|
||||
}
|
||||
|
||||
func NewSignatureVerifyService(
|
||||
principalStore store.PrincipalStore,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore,
|
||||
gitSignatureResultStore store.GitSignatureResultStore,
|
||||
) SignatureVerifyService {
|
||||
return SignatureVerifyService{
|
||||
principalStore: principalStore,
|
||||
publicKeyStore: publicKeyStore,
|
||||
publicKeySubKeyStore: publicKeySubKeyStore,
|
||||
gitSignatureResultStore: gitSignatureResultStore,
|
||||
}
|
||||
}
|
||||
|
||||
// NewVerifySession creates a new session for git object signature verification.
|
||||
// The session holds a small cache for users and signing keys.
|
||||
func (s SignatureVerifyService) NewVerifySession(repoID int64) *VerifySession {
|
||||
return &VerifySession{
|
||||
SignatureVerifyService: s,
|
||||
repoID: repoID,
|
||||
principalIDCache: make(map[string]int64),
|
||||
keyCache: make(map[personalKey]*types.PublicKey),
|
||||
}
|
||||
}
|
||||
|
||||
func (s SignatureVerifyService) VerifyCommitTags(ctx context.Context, repoID int64, tags []*types.CommitTag) error {
|
||||
session := s.NewVerifySession(repoID)
|
||||
if err := verifyObjects(ctx, session, tags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.StoreSignatures(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SignatureVerifyService) VerifyCommits(ctx context.Context, repoID int64, commits []*types.Commit) error {
|
||||
session := s.NewVerifySession(repoID)
|
||||
if err := verifyObjects(ctx, session, commits); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.StoreSignatures(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *VerifySession) VerifyCommitTags(ctx context.Context, tags []*types.CommitTag) error {
|
||||
return verifyObjects(ctx, s, tags)
|
||||
}
|
||||
|
||||
func (s *VerifySession) VerifyCommits(ctx context.Context, commits []*types.Commit) error {
|
||||
return verifyObjects(ctx, s, commits)
|
||||
}
|
||||
|
||||
// VerifySession holds short time caches for a single iteration of verifyObject function.
|
||||
type VerifySession struct {
|
||||
SignatureVerifyService
|
||||
repoID int64
|
||||
|
||||
// principalIDCache is cache of principal IDs. The key is email address.
|
||||
principalIDCache map[string]int64
|
||||
|
||||
// keyCache is cache of personal keys. The map key holds principalID, key ID and fingerprint.
|
||||
keyCache map[personalKey]*types.PublicKey
|
||||
|
||||
// sigResults are git signature verification results that should be stored to the database.
|
||||
sigResults []*types.GitSignatureResult
|
||||
}
|
||||
|
||||
// personalKey is cache key for the cache of public keys.
|
||||
type personalKey struct {
|
||||
principalID int64
|
||||
keyID string
|
||||
keyFingerprint string
|
||||
}
|
||||
|
||||
func (s *VerifySession) principalByEmail(ctx context.Context, email string) (int64, error) {
|
||||
if principalID, ok := s.principalIDCache[email]; ok {
|
||||
return principalID, nil
|
||||
}
|
||||
|
||||
principal, err := s.principalStore.FindByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, gitness_store.ErrResourceNotFound) {
|
||||
s.principalIDCache[email] = 0
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s.principalIDCache[email] = principal.ID
|
||||
|
||||
return principal.ID, nil
|
||||
}
|
||||
|
||||
func (s *VerifySession) fetchKey(
|
||||
ctx context.Context,
|
||||
v verifier,
|
||||
k personalKey,
|
||||
principalID int64,
|
||||
) (*types.PublicKey, error) {
|
||||
key, ok := s.keyCache[k]
|
||||
if ok {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
key, err := v.Key(ctx, s.publicKeyStore, principalID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get public key from verifier: %w", err)
|
||||
}
|
||||
|
||||
s.keyCache[k] = key // We also store nils here to avoid searching for a non-existing key multiple times.
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (s *VerifySession) StoreSignatures(ctx context.Context) {
|
||||
err := s.gitSignatureResultStore.TryCreateAll(ctx, s.sigResults)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().Err(err).
|
||||
Msg("failed to create git signature results")
|
||||
}
|
||||
}
|
||||
|
||||
func verifyObjects[T signedObject](ctx context.Context, session *VerifySession, objects []T) error {
|
||||
// Fill objects' signature data from the DB,
|
||||
// and get a map of objects without signature data in the DB.
|
||||
objectMap, err := fillSignatureFromDB(ctx, &session.SignatureVerifyService, session.repoID, objects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to backfill object signature info from the DB: %w", err)
|
||||
}
|
||||
|
||||
for _, object := range objectMap {
|
||||
sigResult, err := verifyGitObjectSignature(ctx, session, object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify object signature: %w", err)
|
||||
}
|
||||
|
||||
object.SetSignature(sigResult)
|
||||
|
||||
// These we don't store to the database: Invalid, Unsupported and Unverified.
|
||||
// An invalid signature can mean not just that the signature contains garbage data, but also that
|
||||
// we failed to verify it because of a bug. So, we deliberately don't store them to the DB.
|
||||
if result := sigResult.Result; result == enum.GitSignatureInvalid ||
|
||||
result == enum.GitSignatureUnsupported ||
|
||||
result == enum.GitSignatureUnverified {
|
||||
continue
|
||||
}
|
||||
|
||||
session.sigResults = append(session.sigResults, sigResult)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fillSignatureFromDB reads git object signatures from the DB,
|
||||
// updates the elements of the provided slice,
|
||||
// and return a map of objects that do not yet have a signature in the DB.
|
||||
func fillSignatureFromDB[T signedObject](
|
||||
ctx context.Context,
|
||||
s *SignatureVerifyService,
|
||||
repoID int64,
|
||||
objects []T,
|
||||
) (map[sha.SHA]T, error) {
|
||||
objectMap := make(map[sha.SHA]T)
|
||||
for i := range objects {
|
||||
if objects[i].GetSignedData() != nil {
|
||||
objectMap[objects[i].GetSHA()] = objects[i]
|
||||
}
|
||||
}
|
||||
|
||||
if len(objectMap) == 0 {
|
||||
return objectMap, nil
|
||||
}
|
||||
|
||||
// Get slice of SHAs from the map.
|
||||
objectSHAs := slices.AppendSeq[[]sha.SHA](make([]sha.SHA, 0, len(objectMap)), maps.Keys(objectMap))
|
||||
|
||||
// Read signature data from the tags from the DB.
|
||||
objectSignatureMap, err := s.gitSignatureResultStore.Map(ctx, repoID, objectSHAs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read commit signatures: %w", err)
|
||||
}
|
||||
|
||||
// Update the objects found in the database and remove them from the map.
|
||||
for objectSHA, objectSignature := range objectSignatureMap {
|
||||
object := objectMap[objectSHA]
|
||||
object.SetSignature(&objectSignature)
|
||||
|
||||
delete(objectMap, objectSHA)
|
||||
}
|
||||
|
||||
return objectMap, nil
|
||||
}
|
||||
|
||||
func verifyGitObjectSignature[T signedObject](
|
||||
ctx context.Context,
|
||||
s *VerifySession,
|
||||
object T,
|
||||
) (*types.GitSignatureResult, error) {
|
||||
signedData := object.GetSignedData()
|
||||
if signedData == nil {
|
||||
return &sigVerUnverified, nil
|
||||
}
|
||||
|
||||
var v verifier
|
||||
|
||||
switch signedData.Type {
|
||||
case keyssh.SignatureType:
|
||||
v = &keyssh.Verify{}
|
||||
case keypgp.SignatureType:
|
||||
v = &keypgp.Verify{}
|
||||
default:
|
||||
return &sigVerUnsupported, nil // We mark unsupported signature types as unsupported.
|
||||
}
|
||||
|
||||
// Get the object's signer email address - the committer for commits, the tagger for annotated tags.
|
||||
|
||||
signer := object.GetSigner()
|
||||
if signer == nil {
|
||||
return &sigVerUnverified, nil
|
||||
}
|
||||
|
||||
objectTime := signer.When.UnixMilli()
|
||||
email := signer.Identity.Email
|
||||
|
||||
// Find the principal by the signer's email address.
|
||||
// If principal is not found the signature is unverified.
|
||||
|
||||
principalID, err := s.principalByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find principal by email: %w", err)
|
||||
}
|
||||
|
||||
if principalID == 0 {
|
||||
return &sigVerUnverified, nil
|
||||
}
|
||||
|
||||
// Find the key info from the signature.
|
||||
|
||||
if result := v.Parse(ctx, object.GetSignedData().Signature, object.GetSHA()); result != "" {
|
||||
//nolint:exhaustive
|
||||
switch result {
|
||||
case enum.GitSignatureInvalid:
|
||||
return &sigVerInvalid, nil
|
||||
case enum.GitSignatureUnsupported:
|
||||
return &sigVerUnsupported, nil
|
||||
default:
|
||||
// Should not happen.
|
||||
return nil, fmt.Errorf("unexpected signature verification result=%q after signature parsing", result)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the key from the DB. If it's not there, the signature is unverified.
|
||||
|
||||
key, err := s.fetchKey(ctx, v, personalKey{
|
||||
principalID: principalID,
|
||||
keyID: v.KeyID(),
|
||||
keyFingerprint: v.KeyFingerprint(),
|
||||
}, principalID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
if key == nil {
|
||||
return &sigVerUnverified, nil
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
sigResult := &types.GitSignatureResult{
|
||||
RepoID: s.repoID,
|
||||
ObjectSHA: object.GetSHA(),
|
||||
ObjectTime: objectTime,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
Result: "", // the result will be set later
|
||||
PrincipalID: principalID,
|
||||
KeyScheme: v.KeyScheme(),
|
||||
KeyID: v.KeyID(),
|
||||
KeyFingerprint: v.KeyFingerprint(),
|
||||
}
|
||||
|
||||
// Using the key's properties, if possible override the verification result.
|
||||
|
||||
switch {
|
||||
case key.RevocationReason != nil:
|
||||
sigResult.Result = enum.GitSignatureRevoked
|
||||
return sigResult, nil
|
||||
case key.ValidFrom != nil && objectTime < *key.ValidFrom:
|
||||
sigResult.Result = enum.GitSignatureKeyExpired
|
||||
return sigResult, nil
|
||||
case key.ValidTo != nil && objectTime > *key.ValidTo:
|
||||
sigResult.Result = enum.GitSignatureKeyExpired
|
||||
return sigResult, nil
|
||||
}
|
||||
|
||||
// Verify the git object signature using the key from the database.
|
||||
|
||||
sigResult.Result = v.Verify(
|
||||
ctx,
|
||||
[]byte(key.Content),
|
||||
object.GetSignedData().SignedContent,
|
||||
object.GetSHA(),
|
||||
*signer)
|
||||
|
||||
return sigResult, nil
|
||||
}
|
||||
|
||||
// verifier is interface to verify a git object signature.
|
||||
// It's implemented by keypgp.Verify and keyssh.Verify.
|
||||
type verifier interface {
|
||||
// Parse parses the provided signature and extracts info about the signing key (ID/Fingerprint).
|
||||
Parse(
|
||||
ctx context.Context,
|
||||
signature []byte,
|
||||
objectSHA sha.SHA,
|
||||
) enum.GitSignatureResult
|
||||
|
||||
// Key fetches the key from the DB.
|
||||
Key(
|
||||
ctx context.Context,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
principalID int64,
|
||||
) (*types.PublicKey, error)
|
||||
|
||||
// Verify checks if the signed content matches signature.
|
||||
Verify(
|
||||
ctx context.Context,
|
||||
key []byte,
|
||||
signedContent []byte,
|
||||
objectSHA sha.SHA,
|
||||
committer types.Signature,
|
||||
) enum.GitSignatureResult
|
||||
|
||||
// KeyScheme returns the signing key's scheme.
|
||||
KeyScheme() enum.PublicKeyScheme
|
||||
|
||||
// KeyID returns the signing key ID. Use after a call to the Parse method
|
||||
KeyID() string
|
||||
|
||||
// KeyFingerprint returns the signing key fingerprint. Use after a call to the Parse method
|
||||
KeyFingerprint() string
|
||||
}
|
||||
|
||||
// signedObject is interface used to verify signature.
|
||||
// It's implemented by types.Commit and types.CommitTag.
|
||||
type signedObject interface {
|
||||
GetSHA() sha.SHA
|
||||
SetSignature(sig *types.GitSignatureResult)
|
||||
GetSigner() *types.Signature
|
||||
GetSignedData() *types.SignedData
|
||||
}
|
||||
|
||||
var sigVerUnverified = types.GitSignatureResult{
|
||||
Result: enum.GitSignatureUnverified,
|
||||
}
|
||||
|
||||
var sigVerUnsupported = types.GitSignatureResult{
|
||||
Result: enum.GitSignatureUnsupported,
|
||||
}
|
||||
|
||||
var sigVerInvalid = types.GitSignatureResult{
|
||||
Result: enum.GitSignatureInvalid,
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 publickey
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// SSHSignatureBlob describes a lightweight SSH signature format.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L34
|
||||
type SSHSignatureBlob struct {
|
||||
MagicPreamble [6]byte
|
||||
Version uint32
|
||||
PublicKey []byte
|
||||
Namespace string
|
||||
Reserved string
|
||||
HashAlgorithm string
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// SSHMessageWrapper represents SSH signed data.
|
||||
// https://github.com/openssh/openssh-portable/blob/V_9_9_P2/PROTOCOL.sshsig#L81
|
||||
type SSHMessageWrapper struct {
|
||||
Namespace string
|
||||
Reserved string
|
||||
HashAlgorithm string
|
||||
Hash []byte
|
||||
}
|
||||
|
||||
func SSHHash(hashAlgorithm string) (hash.Hash, error) {
|
||||
switch hashAlgorithm {
|
||||
case "sha256":
|
||||
return sha256.New(), nil
|
||||
case "sha512":
|
||||
return sha512.New(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashAlgorithm)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import (
|
|||
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideSSHAuthService,
|
||||
ProvideSignatureVerifyService,
|
||||
)
|
||||
|
||||
func ProvideSSHAuthService(
|
||||
|
|
@ -30,3 +31,16 @@ func ProvideSSHAuthService(
|
|||
) SSHAuthService {
|
||||
return NewSSHAuthService(publicKeyStore, pCache)
|
||||
}
|
||||
|
||||
func ProvideSignatureVerifyService(
|
||||
principalStore store.PrincipalStore,
|
||||
publicKeyStore store.PublicKeyStore,
|
||||
publicKeySubKeyStore store.PublicKeySubKeyStore,
|
||||
gitSignatureResultStore store.GitSignatureResultStore,
|
||||
) SignatureVerifyService {
|
||||
return NewSignatureVerifyService(
|
||||
principalStore,
|
||||
publicKeyStore,
|
||||
publicKeySubKeyStore,
|
||||
gitSignatureResultStore)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1141,6 +1141,9 @@ type (
|
|||
// Create creates a new public key.
|
||||
Create(ctx context.Context, publicKey *types.PublicKey) error
|
||||
|
||||
// Update updates a public key.
|
||||
Update(ctx context.Context, publicKey *types.PublicKey) error
|
||||
|
||||
// DeleteByIdentifier deletes a public key.
|
||||
DeleteByIdentifier(ctx context.Context, principalID int64, identifier string) error
|
||||
|
||||
|
|
@ -1174,6 +1177,25 @@ type (
|
|||
|
||||
PublicKeySubKeyStore interface {
|
||||
Create(ctx context.Context, publicKeyID int64, subKeyIDs []string) error
|
||||
List(ctx context.Context, publicKeyID int64) ([]string, error)
|
||||
}
|
||||
|
||||
GitSignatureResultStore interface {
|
||||
Map(
|
||||
ctx context.Context,
|
||||
repoID int64,
|
||||
objectSHAs []sha.SHA,
|
||||
) (map[sha.SHA]types.GitSignatureResult, error)
|
||||
|
||||
Create(ctx context.Context, sigResult types.GitSignatureResult) error
|
||||
TryCreateAll(ctx context.Context, sigResult []*types.GitSignatureResult) error
|
||||
|
||||
UpdateAll(
|
||||
ctx context.Context,
|
||||
result enum.GitSignatureResult,
|
||||
principalID int64,
|
||||
keyIDs, keyFingerprints []string,
|
||||
) error
|
||||
}
|
||||
|
||||
GitspaceEventStore interface {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/store/database"
|
||||
"github.com/harness/gitness/store/database/dbtx"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var _ store.GitSignatureResultStore = GitSignatureResultStore{}
|
||||
|
||||
// NewGitSignatureResultStore returns a new GitSignatureResultStore.
|
||||
func NewGitSignatureResultStore(db *sqlx.DB) GitSignatureResultStore {
|
||||
return GitSignatureResultStore{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// GitSignatureResultStore implements a store.GitSignatureResultStore backed by a relational database.
|
||||
type GitSignatureResultStore struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
const (
|
||||
gitSignatureResultColumns = `
|
||||
git_signature_result_repo_id
|
||||
,git_signature_result_object_sha
|
||||
,git_signature_result_object_time
|
||||
,git_signature_result_created
|
||||
,git_signature_result_updated
|
||||
,git_signature_result_result
|
||||
,git_signature_result_principal_id
|
||||
,git_signature_result_key_scheme
|
||||
,git_signature_result_key_id
|
||||
,git_signature_result_key_fingerprint`
|
||||
|
||||
gitSignatureResultInsertQuery = `
|
||||
INSERT INTO git_signature_results (` + gitSignatureResultColumns + `
|
||||
) values (
|
||||
:git_signature_result_repo_id
|
||||
,:git_signature_result_object_sha
|
||||
,:git_signature_result_object_time
|
||||
,:git_signature_result_created
|
||||
,:git_signature_result_updated
|
||||
,:git_signature_result_result
|
||||
,:git_signature_result_principal_id
|
||||
,:git_signature_result_key_scheme
|
||||
,:git_signature_result_key_id
|
||||
,:git_signature_result_key_fingerprint
|
||||
)`
|
||||
)
|
||||
|
||||
type gitSignatureResult struct {
|
||||
RepoID int64 `db:"git_signature_result_repo_id"`
|
||||
ObjectSHA string `db:"git_signature_result_object_sha"`
|
||||
ObjectTime int64 `db:"git_signature_result_object_time"`
|
||||
Created int64 `db:"git_signature_result_created"`
|
||||
Updated int64 `db:"git_signature_result_updated"`
|
||||
Result string `db:"git_signature_result_result"`
|
||||
PrincipalID int64 `db:"git_signature_result_principal_id"`
|
||||
KeyScheme string `db:"git_signature_result_key_scheme"`
|
||||
KeyID string `db:"git_signature_result_key_id"`
|
||||
KeyFingerprint string `db:"git_signature_result_key_fingerprint"`
|
||||
}
|
||||
|
||||
func (s GitSignatureResultStore) Map(
|
||||
ctx context.Context,
|
||||
repoID int64,
|
||||
objectSHAs []sha.SHA,
|
||||
) (map[sha.SHA]types.GitSignatureResult, error) {
|
||||
stmt := database.Builder.
|
||||
Select(gitSignatureResultColumns).
|
||||
From("git_signature_results").
|
||||
Where("git_signature_result_repo_id = ?", repoID).
|
||||
Where(squirrel.Eq{"git_signature_result_object_sha": objectSHAs})
|
||||
|
||||
sql, args, err := stmt.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert query to sql: %w", err)
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
sigVers := make([]gitSignatureResult, 0)
|
||||
if err = db.SelectContext(ctx, &sigVers, sql, args...); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err,
|
||||
"failed to execute list git signature verification results query")
|
||||
}
|
||||
|
||||
sigVerMap := map[sha.SHA]types.GitSignatureResult{}
|
||||
for _, sigVer := range sigVers {
|
||||
o := mapToGitSignatureResult(sigVer)
|
||||
sigVerMap[o.ObjectSHA] = o
|
||||
}
|
||||
|
||||
return sigVerMap, nil
|
||||
}
|
||||
|
||||
func (s GitSignatureResultStore) Create(
|
||||
ctx context.Context,
|
||||
sigResult types.GitSignatureResult,
|
||||
) error {
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
sigResultInternal := mapToInternalGitSignatureResult(sigResult)
|
||||
|
||||
query, arg, err := db.BindNamed(gitSignatureResultInsertQuery, &sigResultInternal)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err,
|
||||
"Failed to bind git signature verification result object")
|
||||
}
|
||||
|
||||
if _, err = db.ExecContext(ctx, query, arg...); err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err,
|
||||
"Insert git signature verification result query failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s GitSignatureResultStore) TryCreateAll(
|
||||
ctx context.Context,
|
||||
sigResults []*types.GitSignatureResult,
|
||||
) error {
|
||||
if len(sigResults) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
const sql = gitSignatureResultInsertQuery + `
|
||||
ON CONFLICT DO NOTHING`
|
||||
|
||||
stmt, err := db.PrepareNamedContext(ctx, sql)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err,
|
||||
"Failed to prepare git signature verification result statement")
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
for _, sigResult := range sigResults {
|
||||
_, err = stmt.Exec(mapToInternalGitSignatureResult(*sigResult))
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err,
|
||||
"Failed to insert git signature verification result")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s GitSignatureResultStore) UpdateAll(
|
||||
ctx context.Context,
|
||||
result enum.GitSignatureResult,
|
||||
principalID int64,
|
||||
keyIDs, keyFingerprints []string,
|
||||
) error {
|
||||
query := database.Builder.
|
||||
Update("git_signature_results").
|
||||
Set("git_signature_result_result", result).
|
||||
Set("git_signature_result_updated", time.Now().UnixMilli()).
|
||||
Where("git_signature_result_principal_id = ?", principalID)
|
||||
|
||||
if len(keyIDs) > 0 {
|
||||
query = query.Where(squirrel.Eq{"git_signature_result_key_id": keyIDs})
|
||||
}
|
||||
if len(keyFingerprints) > 0 {
|
||||
query = query.Where(squirrel.Eq{"git_signature_result_key_fingerprint": keyFingerprints})
|
||||
}
|
||||
|
||||
sql, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert query to sql: %w", err)
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
_, err = db.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err, "failed to update git signatures")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapToInternalGitSignatureResult(sigVer types.GitSignatureResult) gitSignatureResult {
|
||||
return gitSignatureResult{
|
||||
RepoID: sigVer.RepoID,
|
||||
ObjectSHA: sigVer.ObjectSHA.String(),
|
||||
ObjectTime: sigVer.ObjectTime,
|
||||
Created: sigVer.Created,
|
||||
Updated: sigVer.Updated,
|
||||
Result: string(sigVer.Result),
|
||||
PrincipalID: sigVer.PrincipalID,
|
||||
KeyScheme: string(sigVer.KeyScheme),
|
||||
KeyID: sigVer.KeyID,
|
||||
KeyFingerprint: sigVer.KeyFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
func mapToGitSignatureResult(sigVer gitSignatureResult) types.GitSignatureResult {
|
||||
objectSHA, _ := sha.New(sigVer.ObjectSHA)
|
||||
return types.GitSignatureResult{
|
||||
RepoID: sigVer.RepoID,
|
||||
ObjectSHA: objectSHA,
|
||||
ObjectTime: sigVer.ObjectTime,
|
||||
Created: sigVer.Created,
|
||||
Updated: sigVer.Updated,
|
||||
Result: enum.GitSignatureResult(sigVer.Result),
|
||||
PrincipalID: sigVer.PrincipalID,
|
||||
KeyScheme: enum.PublicKeyScheme(sigVer.KeyScheme),
|
||||
KeyID: sigVer.KeyID,
|
||||
KeyFingerprint: sigVer.KeyFingerprint,
|
||||
}
|
||||
}
|
||||
|
|
@ -474,7 +474,7 @@ func (s *MembershipStore) mapToMembershipSpaces(ctx context.Context,
|
|||
res[i].Membership = mapToMembership(&m.membership)
|
||||
space, err := mapToSpace(ctx, s.db, s.spacePathStore, &m.space)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("faild to map space %d: %w", m.space.ID, err)
|
||||
return nil, fmt.Errorf("failed to map space %d: %w", m.space.ID, err)
|
||||
}
|
||||
res[i].Space = *space
|
||||
if addedBy, ok := infoMap[m.membership.CreatedBy]; ok {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX idx_git_signature_results_principal_id;
|
||||
DROP TABLE git_signature_results;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
CREATE TABLE git_signature_results (
|
||||
git_signature_result_repo_id INTEGER NOT NULL,
|
||||
git_signature_result_object_sha TEXT NOT NULL,
|
||||
git_signature_result_object_time BIGINT NOT NULL,
|
||||
git_signature_result_created BIGINT NOT NULL,
|
||||
git_signature_result_updated BIGINT NOT NULL,
|
||||
git_signature_result_result TEXT NOT NULL,
|
||||
git_signature_result_principal_id INTEGER NOT NULL,
|
||||
git_signature_result_key_scheme TEXT NOT NULL,
|
||||
git_signature_result_key_id TEXT NOT NULL,
|
||||
git_signature_result_key_fingerprint TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT pk_git_signature_results PRIMARY KEY (git_signature_result_repo_id, git_signature_result_object_sha),
|
||||
|
||||
CONSTRAINT fk_git_signature_results_repo_id FOREIGN KEY (git_signature_result_repo_id)
|
||||
REFERENCES repositories (repo_id)
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_git_signature_result_principal_id FOREIGN KEY (git_signature_result_principal_id)
|
||||
REFERENCES principals (principal_id)
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_git_signature_results_principal_id
|
||||
ON git_signature_results (git_signature_result_principal_id);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX idx_git_signature_results_principal_id;
|
||||
DROP TABLE git_signature_results;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
CREATE TABLE git_signature_results (
|
||||
git_signature_result_repo_id INTEGER NOT NULL,
|
||||
git_signature_result_object_sha TEXT NOT NULL,
|
||||
git_signature_result_object_time BIGINT NOT NULL,
|
||||
git_signature_result_created BIGINT NOT NULL,
|
||||
git_signature_result_updated BIGINT NOT NULL,
|
||||
git_signature_result_result TEXT NOT NULL,
|
||||
git_signature_result_principal_id INTEGER NOT NULL,
|
||||
git_signature_result_key_scheme TEXT NOT NULL,
|
||||
git_signature_result_key_id TEXT NOT NULL,
|
||||
git_signature_result_key_fingerprint TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT pk_git_signature_results PRIMARY KEY (git_signature_result_repo_id, git_signature_result_object_sha),
|
||||
|
||||
CONSTRAINT fk_git_signature_results_repo_id FOREIGN KEY (git_signature_result_repo_id)
|
||||
REFERENCES repositories (repo_id)
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_git_signature_result_principal_id FOREIGN KEY (git_signature_result_principal_id)
|
||||
REFERENCES principals (principal_id)
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_git_signature_results_principal_id
|
||||
ON git_signature_results (git_signature_result_principal_id);
|
||||
|
|
@ -186,6 +186,32 @@ func (s PublicKeyStore) Create(ctx context.Context, key *types.PublicKey) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s PublicKeyStore) Update(ctx context.Context, publicKey *types.PublicKey) error {
|
||||
const sqlQuery = `
|
||||
UPDATE public_keys
|
||||
SET
|
||||
public_key_valid_from = :public_key_valid_from
|
||||
,public_key_valid_to = :public_key_valid_to
|
||||
,public_key_revocation_reason = :public_key_revocation_reason
|
||||
WHERE public_key_id = :public_key_id`
|
||||
|
||||
dbPublicKey := mapToInternalPublicKey(publicKey)
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
query, arg, err := db.BindNamed(sqlQuery, dbPublicKey)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err, "Failed to bind public key object")
|
||||
}
|
||||
|
||||
_, err = db.ExecContext(ctx, query, arg...)
|
||||
if err != nil {
|
||||
return database.ProcessSQLErrorf(ctx, err, "Failed to update public key")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByIdentifier deletes a public key.
|
||||
func (s PublicKeyStore) DeleteByIdentifier(ctx context.Context, principalID int64, identifier string) error {
|
||||
const sqlQuery = `DELETE FROM public_keys WHERE public_key_principal_id = $1 and LOWER(public_key_identifier) = $2`
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type PublicKeySubKeyStore struct {
|
|||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// Create creates a new public key.
|
||||
// Create creates subkeys for the provided public key.
|
||||
func (s PublicKeySubKeyStore) Create(ctx context.Context, publicKeyID int64, pgpKeyIDs []string) error {
|
||||
if len(pgpKeyIDs) == 0 {
|
||||
return nil
|
||||
|
|
@ -66,3 +66,37 @@ func (s PublicKeySubKeyStore) Create(ctx context.Context, publicKeyID int64, pgp
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List return all sub keys from a public key.
|
||||
func (s PublicKeySubKeyStore) List(ctx context.Context, publicKeyID int64) ([]string, error) {
|
||||
const sqlQuery = `
|
||||
SELECT public_key_sub_key_id
|
||||
FROM public_key_sub_keys
|
||||
WHERE public_key_sub_key_public_key_id = $1`
|
||||
|
||||
db := dbtx.GetAccessor(ctx, s.db)
|
||||
|
||||
rows, err := db.QueryContext(ctx, sqlQuery, publicKeyID)
|
||||
if err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to query for public key subkeys")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []string
|
||||
|
||||
for rows.Next() {
|
||||
var subKeyID string
|
||||
if err := rows.Scan(&subKeyID); err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to scan subkey ID")
|
||||
}
|
||||
|
||||
result = append(result, subKeyID)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return nil, database.ProcessSQLErrorf(ctx, err, "failed to list subkeys")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ var WireSet = wire.NewSet(
|
|||
ProvidePluginStore,
|
||||
ProvidePublicKeyStore,
|
||||
ProvidePublicKeySubKeyStore,
|
||||
ProvideGitSignatureResultStore,
|
||||
ProvideInfraProviderConfigStore,
|
||||
ProvideInfraProviderResourceStore,
|
||||
ProvideGitspaceConfigStore,
|
||||
|
|
@ -327,6 +328,10 @@ func ProvidePublicKeySubKeyStore(db *sqlx.DB) store.PublicKeySubKeyStore {
|
|||
return NewPublicKeySubKeyStore(db)
|
||||
}
|
||||
|
||||
func ProvideGitSignatureResultStore(db *sqlx.DB) store.GitSignatureResultStore {
|
||||
return NewGitSignatureResultStore(db)
|
||||
}
|
||||
|
||||
// ProvideBranchStore provides a branch store.
|
||||
func ProvideBranchStore(db *sqlx.DB) store.BranchStore {
|
||||
return NewBranchStore(db)
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
tokenStore := database.ProvideTokenStore(db)
|
||||
publicKeyStore := database.ProvidePublicKeyStore(db)
|
||||
publicKeySubKeyStore := database.ProvidePublicKeySubKeyStore(db)
|
||||
gitSignatureResultStore := database.ProvideGitSignatureResultStore(db)
|
||||
eventsConfig := server.ProvideEventsConfig(config)
|
||||
eventsSystem, err := events.ProvideSystem(eventsConfig, universalClient)
|
||||
if err != nil {
|
||||
|
|
@ -214,7 +215,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
return nil, err
|
||||
}
|
||||
favoriteStore := database.ProvideFavoriteStore(db)
|
||||
controller := user.ProvideController(transactor, principalUID, authorizer, principalStore, tokenStore, membershipStore, publicKeyStore, publicKeySubKeyStore, reporter, repoFinder, favoriteStore)
|
||||
controller := user.ProvideController(transactor, principalUID, authorizer, principalStore, tokenStore, membershipStore, publicKeyStore, publicKeySubKeyStore, gitSignatureResultStore, reporter, repoFinder, favoriteStore)
|
||||
serviceController := service.NewController(principalUID, authorizer, principalStore)
|
||||
bootstrapBootstrap := bootstrap.ProvideBootstrap(config, controller, serviceController)
|
||||
authenticator := authn.ProvideAuthenticator(config, principalStore, tokenStore)
|
||||
|
|
@ -307,7 +308,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
|||
}
|
||||
remoteauthService := remoteauth.ProvideRemoteAuth(tokenStore, principalStore)
|
||||
lfsController := lfs.ProvideController(authorizer, repoFinder, repoStore, principalStore, lfsObjectStore, blobStore, remoteauthService, provider, settingsService)
|
||||
repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, spaceFinder, repoFinder, repository, codeownersService, eventsReporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, usergroupService, rulesService, streamer, lfsController, favoriteStore)
|
||||
signatureVerifyService := publickey.ProvideSignatureVerifyService(principalStore, publicKeyStore, publicKeySubKeyStore, gitSignatureResultStore)
|
||||
repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, spaceFinder, repoFinder, repository, codeownersService, eventsReporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, usergroupService, rulesService, streamer, lfsController, favoriteStore, signatureVerifyService)
|
||||
reposettingsController := reposettings.ProvideController(authorizer, repoFinder, settingsService, auditService)
|
||||
stageStore := database.ProvideStageStore(db)
|
||||
schedulerScheduler, err := scheduler.ProvideScheduler(stageStore, mutexManager)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -30,7 +31,6 @@ import (
|
|||
|
||||
"github.com/djherbis/buffer"
|
||||
"github.com/djherbis/nio/v3"
|
||||
"iter"
|
||||
)
|
||||
|
||||
// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function.
|
||||
|
|
@ -168,11 +168,11 @@ func catFileObjects(
|
|||
|
||||
for objectName := range iter {
|
||||
if _, err := writer.Write([]byte(objectName)); err != nil {
|
||||
return fmt.Errorf("faild to write object sha to cat-file stdin: %w", err)
|
||||
return fmt.Errorf("failed to write object sha to cat-file stdin: %w", err)
|
||||
}
|
||||
|
||||
if _, err := writer.Write([]byte{'\n'}); err != nil {
|
||||
return fmt.Errorf("faild to write EOL to cat-file stdin: %w", err)
|
||||
return fmt.Errorf("failed to write EOL to cat-file stdin: %w", err)
|
||||
}
|
||||
|
||||
output, err := ReadBatchHeaderLine(reader)
|
||||
|
|
@ -180,14 +180,14 @@ func catFileObjects(
|
|||
if errors.Is(err, io.EOF) || errors.IsNotFound(err) {
|
||||
return fmt.Errorf("object %s does not exist", objectName)
|
||||
}
|
||||
return fmt.Errorf("faild to read cat-file object header line: %w", err)
|
||||
return fmt.Errorf("failed to read cat-file object header line: %w", err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
_, err = io.CopyN(buf, reader, output.Size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("faild to read object raw data: %w", err)
|
||||
return fmt.Errorf("failed to read object raw data: %w", err)
|
||||
}
|
||||
|
||||
_, err = reader.Discard(1)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/services/publickey"
|
||||
"github.com/harness/gitness/app/services/publickey/keyssh"
|
||||
"github.com/harness/gitness/git/sha"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
|
@ -270,6 +273,12 @@ Rv18ZouJpO2LRIXdZpxAE=
|
|||
},
|
||||
}
|
||||
|
||||
objectSHA := sha.Must("123456789")
|
||||
person := types.Signature{
|
||||
Identity: types.Identity{Name: "Michelangelo", Email: "michelangelo@harness.io"},
|
||||
When: time.Now(),
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
object, err := Object([]byte(test.data))
|
||||
|
|
@ -282,55 +291,27 @@ Rv18ZouJpO2LRIXdZpxAE=
|
|||
t.Errorf("failed:\n%s\n", diff)
|
||||
}
|
||||
|
||||
if len(object.Signature) == 0 || object.SignatureType != "SSH SIGNATURE" {
|
||||
if len(object.Signature) == 0 {
|
||||
// skip testing signed content because the data doesn't contain a signature
|
||||
return
|
||||
}
|
||||
|
||||
// If git object from the test contains a signature,
|
||||
// we verify if object.SignedContent is correct
|
||||
// (if it's possible to verify the signature from the content).
|
||||
ctx := context.Background()
|
||||
var verify keyssh.Verify
|
||||
|
||||
block, rest := pem.Decode(object.Signature)
|
||||
if block == nil || len(rest) > 0 || block.Type != object.SignatureType {
|
||||
t.Errorf("failed to decode signature")
|
||||
return
|
||||
signature := object.Signature
|
||||
content := object.SignedContent
|
||||
|
||||
if status := verify.Parse(ctx, signature, objectSHA); status != "" {
|
||||
t.Errorf("failed to extract key from the signature: %s", status)
|
||||
}
|
||||
|
||||
var signature publickey.SSHSignatureBlob
|
||||
if err := ssh.Unmarshal(block.Bytes, &signature); err != nil {
|
||||
t.Errorf("failed to parse signature: %s", err.Error())
|
||||
return
|
||||
}
|
||||
// we use the public key directly from the signature
|
||||
publicKey, _ := ssh.ParsePublicKey(verify.SignaturePublicKey())
|
||||
pk := ssh.MarshalAuthorizedKey(publicKey)
|
||||
|
||||
sshSig := ssh.Signature{}
|
||||
if err := ssh.Unmarshal(signature.Signature, &sshSig); err != nil {
|
||||
t.Errorf("failed to unmarshal ssh signature: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h, _ := publickey.SSHHash(signature.HashAlgorithm)
|
||||
h.Write(object.SignedContent)
|
||||
|
||||
key, err := ssh.ParsePublicKey(signature.PublicKey) // we get the public key directly from the signature
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse signature key: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
signedMessage := ssh.Marshal(publickey.SSHMessageWrapper{
|
||||
Namespace: signature.Namespace,
|
||||
HashAlgorithm: signature.HashAlgorithm,
|
||||
Hash: h.Sum(nil),
|
||||
})
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.Write(signature.MagicPreamble[:])
|
||||
buf.Write(signedMessage)
|
||||
|
||||
err = key.Verify(buf.Bytes(), &sshSig)
|
||||
if err != nil {
|
||||
t.Errorf("failed to verify signature: %s", err.Error())
|
||||
return
|
||||
if status := verify.Verify(ctx, pk, content, objectSHA, person); status != enum.GitSignatureGood {
|
||||
t.Errorf("failed to verify the signature: %s", status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ package sha
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
|
@ -141,3 +142,7 @@ func (s SHA) JSONSchema() (jsonschema.Schema, error) {
|
|||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func (s SHA) Value() (driver.Value, error) {
|
||||
return s.str, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ func (r *SharedRepo) CommitTree(
|
|||
// Signed-off-by
|
||||
_, _ = messageBytes.WriteString("\n")
|
||||
_, _ = messageBytes.WriteString("Signed-off-by: ")
|
||||
_, _ = messageBytes.WriteString(fmt.Sprintf("%s <%s>", committer.Identity.Name, committer.Identity.Email))
|
||||
_, _ = messageBytes.WriteString(fmt.Sprintf("%s <%s>\n", committer.Identity.Name, committer.Identity.Email))
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/harness/gitness/app/api/request"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
"github.com/harness/gitness/app/services/publickey"
|
||||
"github.com/harness/gitness/app/services/publickey/keyssh"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/api"
|
||||
"github.com/harness/gitness/types"
|
||||
|
|
@ -377,7 +378,7 @@ func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
|||
log := getLoggerWithRequestID(ctx.SessionID())
|
||||
request.WithRequestIDSSH(ctx, getRequestID(ctx.SessionID()))
|
||||
|
||||
if slices.Contains(publickey.DisallowedTypes, key.Type()) {
|
||||
if slices.Contains(keyssh.DisallowedTypes, key.Type()) {
|
||||
log.Warn().Msgf("public key type not supported: %s", key.Type())
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
|
||||
package types
|
||||
|
||||
import "github.com/harness/gitness/git/sha"
|
||||
import (
|
||||
"github.com/harness/gitness/git/sha"
|
||||
)
|
||||
|
||||
// CommitFilesResponse holds commit id.
|
||||
type CommitFilesResponse struct {
|
||||
|
|
@ -27,3 +29,8 @@ type FileReference struct {
|
|||
Path string `json:"path"`
|
||||
SHA sha.SHA `json:"blob_sha"`
|
||||
}
|
||||
|
||||
type PathDetails struct {
|
||||
Path string `json:"path"`
|
||||
LastCommit *Commit `json:"last_commit,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,3 +99,48 @@ func (s RevocationReason) Sanitize() (RevocationReason, bool) {
|
|||
func GetAllRevocationReasons() ([]RevocationReason, RevocationReason) {
|
||||
return revocationReasons, ""
|
||||
}
|
||||
|
||||
// GitSignatureResult is outcome of a git object's signature verification.
|
||||
type GitSignatureResult string
|
||||
|
||||
const (
|
||||
// GitSignatureInvalid is used when the signature itself is malformed.
|
||||
// Shouldn't be stored to the DB.
|
||||
GitSignatureInvalid GitSignatureResult = "invalid"
|
||||
|
||||
// GitSignatureUnsupported is used when the system is unable to verify the signature
|
||||
// because the signature version is not supported by the system.
|
||||
// Shouldn't be stored to the DB.
|
||||
GitSignatureUnsupported GitSignatureResult = "unsupported"
|
||||
|
||||
// GitSignatureUnverified is used when the signer is not in the DB or
|
||||
// when the key to verify the signature is missing.
|
||||
// Shouldn't be stored to the DB.
|
||||
GitSignatureUnverified GitSignatureResult = "unverified"
|
||||
|
||||
// GitSignatureGood is used when the signature is cryptographically valid for the signed object.
|
||||
GitSignatureGood GitSignatureResult = "good"
|
||||
|
||||
// GitSignatureBad is used when the signature is bad (doesn’t match the object contents).
|
||||
GitSignatureBad GitSignatureResult = "bad"
|
||||
|
||||
// GitSignatureKeyExpired is used when the content's timestamp is not within the key's validity period.
|
||||
GitSignatureKeyExpired GitSignatureResult = "key_expired"
|
||||
|
||||
// GitSignatureRevoked is used when the signature is valid, but is signed with a revoked key.
|
||||
GitSignatureRevoked GitSignatureResult = "revoked"
|
||||
)
|
||||
|
||||
var gitSignatureResults = sortEnum([]GitSignatureResult{
|
||||
GitSignatureInvalid,
|
||||
GitSignatureUnsupported,
|
||||
GitSignatureUnverified,
|
||||
GitSignatureGood,
|
||||
GitSignatureBad,
|
||||
GitSignatureKeyExpired,
|
||||
GitSignatureRevoked,
|
||||
})
|
||||
|
||||
func (GitSignatureResult) Enum() []interface{} {
|
||||
return toInterfaceSlice(gitSignatureResults)
|
||||
}
|
||||
|
|
|
|||
53
types/git.go
53
types/git.go
|
|
@ -99,8 +99,33 @@ type Commit struct {
|
|||
Committer Signature `json:"committer"`
|
||||
SignedData *SignedData `json:"-"`
|
||||
Stats *CommitStats `json:"stats,omitempty"`
|
||||
|
||||
Signature *GitSignatureResult `json:"signature"`
|
||||
}
|
||||
|
||||
func (c *Commit) GetSHA() sha.SHA { return c.SHA }
|
||||
func (c *Commit) SetSignature(sig *GitSignatureResult) { c.Signature = sig }
|
||||
func (c *Commit) GetSigner() *Signature { return &c.Committer }
|
||||
func (c *Commit) GetSignedData() *SignedData { return c.SignedData }
|
||||
|
||||
type CommitTag struct {
|
||||
Name string `json:"name"`
|
||||
SHA sha.SHA `json:"sha"`
|
||||
IsAnnotated bool `json:"is_annotated"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Tagger *Signature `json:"tagger,omitempty"`
|
||||
SignedData *SignedData `json:"-"`
|
||||
Commit *Commit `json:"commit,omitempty"`
|
||||
|
||||
Signature *GitSignatureResult `json:"signature"`
|
||||
}
|
||||
|
||||
func (t *CommitTag) GetSHA() sha.SHA { return t.SHA }
|
||||
func (t *CommitTag) SetSignature(sig *GitSignatureResult) { t.Signature = sig }
|
||||
func (t *CommitTag) GetSigner() *Signature { return t.Tagger }
|
||||
func (t *CommitTag) GetSignedData() *SignedData { return t.SignedData }
|
||||
|
||||
type Signature struct {
|
||||
Identity Identity `json:"identity"`
|
||||
When time.Time `json:"when"`
|
||||
|
|
@ -125,7 +150,33 @@ type RenameDetails struct {
|
|||
}
|
||||
|
||||
type ListCommitResponse struct {
|
||||
Commits []Commit `json:"commits"`
|
||||
Commits []*Commit `json:"commits"`
|
||||
RenameDetails []RenameDetails `json:"rename_details"`
|
||||
TotalCommits int `json:"total_commits,omitempty"`
|
||||
}
|
||||
|
||||
type GitSignatureResult struct {
|
||||
RepoID int64 `json:"-"`
|
||||
ObjectSHA sha.SHA `json:"-"`
|
||||
ObjectTime int64 `json:"-"`
|
||||
|
||||
// Created is the timestamp when the signature was first verified.
|
||||
Created int64 `json:"created,omitempty"`
|
||||
|
||||
// Updated is the timestamp when result has been updated (i.e. because of key revocation).
|
||||
Updated int64 `json:"updated,omitempty"`
|
||||
|
||||
// Result is the result of the signature verification.
|
||||
Result enum.GitSignatureResult `json:"result"`
|
||||
|
||||
// PrincipalID is owner of the key with which signature has been checked.
|
||||
PrincipalID int64 `json:"-"`
|
||||
|
||||
KeyScheme enum.PublicKeyScheme `json:"key_scheme,omitempty"`
|
||||
|
||||
// KeyID is the ID of the key with which signature has been checked.
|
||||
KeyID string `json:"key_id,omitempty"`
|
||||
|
||||
// KeyFingerprint is the fingerprint of the key with which signature has been checked.
|
||||
KeyFingerprint string `json:"key_fingerprint,omitempty"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue