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:
Marko Gaćeša 2025-07-02 11:13:37 +00:00 committed by Harness
parent 0bacf9d63d
commit 7d0ffbfbc0
45 changed files with 1973 additions and 334 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 == "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
DROP INDEX idx_git_signature_results_principal_id;
DROP TABLE git_signature_results;

View File

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

View File

@ -0,0 +1,2 @@
DROP INDEX idx_git_signature_results_principal_id;
DROP TABLE git_signature_results;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (doesnt 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)
}

View File

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