This commit is contained in:
Oliver Gondža 2026-04-21 10:13:20 +02:00 committed by GitHub
commit bdbcd2a4a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 6814 additions and 2769 deletions

View file

@ -1,4 +1,4 @@
# Prevent vendor directory from being copied to ensure we are not not pulling unexpected cruft from
# Prevent vendor directory from being copied to ensure we are not not pulling unexpected cruft from
# a user's workspace, and are only building off of what is locked by dep.
.vscode/
.idea/
@ -26,4 +26,4 @@ examples/
!hack/gpg-wrapper.sh
!hack/git-verify-wrapper.sh
!hack/tool-versions.sh
!hack/install.sh
!hack/install.sh

View file

@ -43,7 +43,7 @@ RUN touch ssh_known_hosts && \
WORKDIR /app/config
RUN mkdir -p tls && \
mkdir -p gpg/source && \
mkdir -p gpg/keys
mkdir -p gpg/keys
COPY .tilt-bin/argocd_linux /usr/local/bin/argocd
@ -59,4 +59,4 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
RUN mkdir -p /tilt
# overridden by Tiltfile
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "dlv", "exec", "--continue", "--accept-multiclient", "--headless", "--listen=:2345", "--api-version=2"]
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "dlv", "exec", "--continue", "--accept-multiclient", "--headless", "--listen=:2345", "--api-version=2"]

View file

@ -19,7 +19,6 @@ import (
"github.com/argoproj/argo-cd/v3/applicationset/services"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/gpg"
)
var _ Generator = (*GitGenerator)(nil)
@ -70,7 +69,7 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
noRevisionCache := appSet.RefreshRequired()
verifyCommit := false
var sourceIntegrity *argoprojiov1alpha1.SourceIntegrity
// When the project field is templated, the contents of the git repo are required to run the git generator and get the templated value,
// but git generator cannot be called without verifying the commit signature.
@ -83,11 +82,13 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
if controllerNamespace == "" {
controllerNamespace = appSet.Namespace
}
if err := client.Get(context.TODO(), types.NamespacedName{Name: project, Namespace: controllerNamespace}, appProject); err != nil {
return nil, fmt.Errorf("error getting project %s: %w", project, err)
}
// we need to verify the signature on the Git revision if GPG is enabled
verifyCommit = len(appProject.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled()
sourceIntegrity = appProject.EffectiveSourceIntegrity()
} else {
log.WithField("appset", appSet.Name).Infof("Cannot enforce eventual source integrity, app project name is templated")
}
// If the project field is templated, we cannot resolve the project name, so we pass an empty string to the repo-server.
@ -98,9 +99,9 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
var res []map[string]any
switch {
case len(appSetGenerator.Git.Directories) != 0:
res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, sourceIntegrity, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
case len(appSetGenerator.Git.Files) != 0:
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, sourceIntegrity, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
default:
return nil, ErrEmptyAppSetGenerator
}
@ -114,8 +115,8 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
// generateParamsForGitDirectories generates parameters for an ApplicationSet using a directory-based Git generator.
// It fetches all directories from the given Git repository and revision, optionally using a revision cache and verifying commits.
// It then filters the directories based on the generator's configuration and renders parameters for the resulting applications
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, verifyCommit)
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, sourceIntegrity *argoprojiov1alpha1.SourceIntegrity, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, sourceIntegrity)
if err != nil {
return nil, fmt.Errorf("error getting directories from repo: %w", err)
}
@ -141,7 +142,7 @@ func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoproj
// generateParamsForGitFiles generates parameters for an ApplicationSet using a file-based Git generator.
// It retrieves and processes specified files from the Git repository, supporting both YAML and JSON formats,
// and returns a list of parameter maps extracted from the content.
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, sourceIntegrity *argoprojiov1alpha1.SourceIntegrity, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
// fileContentMap maps absolute file paths to their byte content
fileContentMap := make(map[string][]byte)
var includePatterns []string
@ -164,7 +165,7 @@ func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1al
project,
includePattern,
noRevisionCache,
verifyCommit,
sourceIntegrity,
)
if err != nil {
return nil, err
@ -181,7 +182,7 @@ func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1al
project,
excludePattern,
noRevisionCache,
verifyCommit,
sourceIntegrity,
)
if err != nil {
return nil, err

View file

@ -7,6 +7,7 @@ package mocks
import (
"context"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
mock "github.com/stretchr/testify/mock"
)
@ -38,8 +39,8 @@ func (_m *Repos) EXPECT() *Repos_Expecter {
}
// GetDirectories provides a mock function for the type Repos
func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error) {
ret := _mock.Called(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error) {
ret := _mock.Called(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
if len(ret) == 0 {
panic("no return value specified for GetDirectories")
@ -47,18 +48,18 @@ func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision
var r0 []string
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) ([]string, error)); ok {
return returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, *v1alpha1.SourceIntegrity) ([]string, error)); ok {
return returnFunc(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) []string); ok {
r0 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, *v1alpha1.SourceIntegrity) []string); ok {
r0 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, bool, bool) error); ok {
r1 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, bool, *v1alpha1.SourceIntegrity) error); ok {
r1 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)
} else {
r1 = ret.Error(1)
}
@ -76,12 +77,12 @@ type Repos_GetDirectories_Call struct {
// - revision string
// - project string
// - noRevisionCache bool
// - verifyCommit bool
func (_e *Repos_Expecter) GetDirectories(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, noRevisionCache interface{}, verifyCommit interface{}) *Repos_GetDirectories_Call {
return &Repos_GetDirectories_Call{Call: _e.mock.On("GetDirectories", ctx, repoURL, revision, project, noRevisionCache, verifyCommit)}
// - sourceIntegrity *v1alpha1.SourceIntegrity
func (_e *Repos_Expecter) GetDirectories(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, noRevisionCache interface{}, sourceIntegrity interface{}) *Repos_GetDirectories_Call {
return &Repos_GetDirectories_Call{Call: _e.mock.On("GetDirectories", ctx, repoURL, revision, project, noRevisionCache, sourceIntegrity)}
}
func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool)) *Repos_GetDirectories_Call {
func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity)) *Repos_GetDirectories_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@ -103,9 +104,9 @@ func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL s
if args[4] != nil {
arg4 = args[4].(bool)
}
var arg5 bool
var arg5 *v1alpha1.SourceIntegrity
if args[5] != nil {
arg5 = args[5].(bool)
arg5 = args[5].(*v1alpha1.SourceIntegrity)
}
run(
arg0,
@ -124,14 +125,14 @@ func (_c *Repos_GetDirectories_Call) Return(strings []string, err error) *Repos_
return _c
}
func (_c *Repos_GetDirectories_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error)) *Repos_GetDirectories_Call {
func (_c *Repos_GetDirectories_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error)) *Repos_GetDirectories_Call {
_c.Call.Return(run)
return _c
}
// GetFiles provides a mock function for the type Repos
func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error) {
ret := _mock.Called(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error) {
ret := _mock.Called(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
if len(ret) == 0 {
panic("no return value specified for GetFiles")
@ -139,18 +140,18 @@ func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision strin
var r0 map[string][]byte
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) (map[string][]byte, error)); ok {
return returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, *v1alpha1.SourceIntegrity) (map[string][]byte, error)); ok {
return returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) map[string][]byte); ok {
r0 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, *v1alpha1.SourceIntegrity) map[string][]byte); ok {
r0 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]byte)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool, bool) error); ok {
r1 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool, *v1alpha1.SourceIntegrity) error); ok {
r1 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)
} else {
r1 = ret.Error(1)
}
@ -169,12 +170,12 @@ type Repos_GetFiles_Call struct {
// - project string
// - pattern string
// - noRevisionCache bool
// - verifyCommit bool
func (_e *Repos_Expecter) GetFiles(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, pattern interface{}, noRevisionCache interface{}, verifyCommit interface{}) *Repos_GetFiles_Call {
return &Repos_GetFiles_Call{Call: _e.mock.On("GetFiles", ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)}
// - sourceIntegrity *v1alpha1.SourceIntegrity
func (_e *Repos_Expecter) GetFiles(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, pattern interface{}, noRevisionCache interface{}, sourceIntegrity interface{}) *Repos_GetFiles_Call {
return &Repos_GetFiles_Call{Call: _e.mock.On("GetFiles", ctx, repoURL, revision, project, pattern, noRevisionCache, sourceIntegrity)}
}
func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool)) *Repos_GetFiles_Call {
func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity)) *Repos_GetFiles_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@ -200,9 +201,9 @@ func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string,
if args[5] != nil {
arg5 = args[5].(bool)
}
var arg6 bool
var arg6 *v1alpha1.SourceIntegrity
if args[6] != nil {
arg6 = args[6].(bool)
arg6 = args[6].(*v1alpha1.SourceIntegrity)
}
run(
arg0,
@ -222,7 +223,7 @@ func (_c *Repos_GetFiles_Call) Return(stringToBytes map[string][]byte, err error
return _c
}
func (_c *Repos_GetFiles_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error)) *Repos_GetFiles_Call {
func (_c *Repos_GetFiles_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error)) *Repos_GetFiles_Call {
_c.Call.Return(run)
return _c
}

View file

@ -20,10 +20,10 @@ type argoCDService struct {
type Repos interface {
// GetFiles returns content of files (not directories) within the target repo
GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache, verifyCommit bool) (map[string][]byte, error)
GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error)
// GetDirectories returns a list of directories (not files) within the target repo
GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache, verifyCommit bool) ([]string, error)
GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error)
}
func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclient.Clientset, newFileGlobbingEnabled bool) Repos {
@ -50,7 +50,7 @@ func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclie
}
}
func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache, verifyCommit bool) (map[string][]byte, error) {
func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project, pattern string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) (map[string][]byte, error) {
repo, err := a.getRepository(ctx, repoURL, project)
if err != nil {
return nil, fmt.Errorf("error in GetRepository: %w", err)
@ -63,8 +63,9 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project
Path: pattern,
NewGitFileGlobbingEnabled: a.newFileGlobbingEnabled,
NoRevisionCache: noRevisionCache,
VerifyCommit: verifyCommit,
SourceIntegrity: sourceIntegrity,
}
fileResponse, err := a.getGitFilesFromRepoServer(ctx, fileRequest)
if err != nil {
return nil, fmt.Errorf("error retrieving Git files: %w", err)
@ -72,7 +73,7 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL, revision, project
return fileResponse.GetMap(), nil
}
func (a *argoCDService) GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache, verifyCommit bool) ([]string, error) {
func (a *argoCDService) GetDirectories(ctx context.Context, repoURL, revision, project string, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity) ([]string, error) {
repo, err := a.getRepository(ctx, repoURL, project)
if err != nil {
return nil, fmt.Errorf("error in GetRepository: %w", err)
@ -83,7 +84,7 @@ func (a *argoCDService) GetDirectories(ctx context.Context, repoURL, revision, p
SubmoduleEnabled: a.submoduleEnabled,
Revision: revision,
NoRevisionCache: noRevisionCache,
VerifyCommit: verifyCommit,
SourceIntegrity: sourceIntegrity,
}
dirResponse, err := a.getGitDirectoriesFromRepoServer(ctx, dirRequest)

View file

@ -28,7 +28,7 @@ func TestGetDirectories(t *testing.T) {
repoURL string
revision string
noRevisionCache bool
verifyCommit bool
sourceIntegrity *v1alpha1.SourceIntegrity
}
tests := []struct {
name string
@ -80,7 +80,7 @@ func TestGetDirectories(t *testing.T) {
submoduleEnabled: tt.fields.submoduleEnabled,
getGitDirectoriesFromRepoServer: tt.fields.getGitDirectories,
}
got, err := a.GetDirectories(tt.args.ctx, tt.args.repoURL, tt.args.revision, "", tt.args.noRevisionCache, tt.args.verifyCommit)
got, err := a.GetDirectories(tt.args.ctx, tt.args.repoURL, tt.args.revision, "", tt.args.noRevisionCache, tt.args.sourceIntegrity)
if !tt.wantErr(t, err, fmt.Sprintf("GetDirectories(%v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.noRevisionCache)) {
return
}
@ -101,7 +101,7 @@ func TestGetFiles(t *testing.T) {
revision string
pattern string
noRevisionCache bool
verifyCommit bool
sourceIntegrity *v1alpha1.SourceIntegrity
}
tests := []struct {
name string
@ -159,7 +159,7 @@ func TestGetFiles(t *testing.T) {
submoduleEnabled: tt.fields.submoduleEnabled,
getGitFilesFromRepoServer: tt.fields.getGitFiles,
}
got, err := a.GetFiles(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, "", tt.args.noRevisionCache, tt.args.verifyCommit)
got, err := a.GetFiles(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, "", tt.args.noRevisionCache, tt.args.sourceIntegrity)
if !tt.wantErr(t, err, fmt.Sprintf("GetFiles(%v, %v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, tt.args.noRevisionCache)) {
return
}

103
assets/swagger.json generated
View file

@ -6152,12 +6152,15 @@
"server": {
"type": "string"
},
"sourceIntegrityResult": {
"$ref": "#/definitions/v1alpha1SourceIntegrityCheckResult"
},
"sourceType": {
"type": "string"
},
"verifyResult": {
"type": "string",
"title": "Raw response of git verify-commit operation (always the empty string for Helm)"
"description": "Raw response of git verify-commit operation (always the empty string for Helm)\nDeprecated: Use SourceIntegrityResult for more detailed information. VerifyResult will be removed with the next major version.",
"type": "string"
}
}
},
@ -7041,6 +7044,9 @@
"$ref": "#/definitions/v1alpha1SignatureKey"
}
},
"sourceIntegrity": {
"$ref": "#/definitions/v1alpha1SourceIntegrity"
},
"sourceNamespaces": {
"type": "array",
"title": "SourceNamespaces defines the namespaces application resources are allowed to be created in",
@ -10278,9 +10284,12 @@
}
},
"signatureInfo": {
"description": "SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.",
"description": "SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.\n\nDeprecated: Use SourceIntegrityResult for more detailed information. SignatureInfo will be removed with the next major version.",
"type": "string"
},
"sourceIntegrityResult": {
"$ref": "#/definitions/v1alpha1SourceIntegrityCheckResult"
},
"tags": {
"type": "array",
"title": "Tags specifies any tags currently attached to the revision\nFloating tags can move from one revision to another",
@ -10637,6 +10646,94 @@
}
}
},
"v1alpha1SourceIntegrity": {
"type": "object",
"properties": {
"git": {
"$ref": "#/definitions/v1alpha1SourceIntegrityGit"
}
}
},
"v1alpha1SourceIntegrityCheckResult": {
"description": "SourceIntegrityCheckResult represents a conclusion of the SourceIntegrity evaluation.\nEach check performed on a source(es), holds a check item representing all checks performed.",
"type": "object",
"properties": {
"checks": {
"description": "Checks holds a list of checks performed, with their eventual problems. If a check is not specified here,\nit means it was not performed.",
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1SourceIntegrityCheckResultItem"
}
}
}
},
"v1alpha1SourceIntegrityCheckResultItem": {
"type": "object",
"properties": {
"name": {
"description": "Name of the check that is human-understandable pointing out to the kind of verification performed.",
"type": "string"
},
"problems": {
"description": "Problems is a list of messages explaining why the check failed. Empty list means the check has succeeded.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1alpha1SourceIntegrityGit": {
"type": "object",
"properties": {
"policies": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1SourceIntegrityGitPolicy"
}
}
}
},
"v1alpha1SourceIntegrityGitPolicy": {
"type": "object",
"properties": {
"gpg": {
"$ref": "#/definitions/v1alpha1SourceIntegrityGitPolicyGPG"
},
"repos": {
"type": "array",
"title": "List of repository criteria restricting repositories the policy will apply to",
"items": {
"$ref": "#/definitions/v1alpha1SourceIntegrityGitPolicyRepo"
}
}
}
},
"v1alpha1SourceIntegrityGitPolicyGPG": {
"description": "SourceIntegrityGitPolicyGPG verifies that the commit(s) are both correctly signed by a key in the repo-server keyring,\nand that they are signed by one of the key listed in Keys.\n\nThis policy can be deactivated through the ARGOCD_GPG_ENABLED environment variable.\n\nNote the listing of problematic commits/signatures reported when \"strict\" mode validation fails may not be complete.\nThis means that a user that has addressed all problems reported by source integrity check can run into\nfurther problematic signatures on a subsequent attempt. That happens namely when history contains seal commits signed\nwith gpg keys that are in the keyring, but not listed in Keys.",
"type": "object",
"properties": {
"keys": {
"description": "List of key IDs to trust. The keys need to be in the repository server keyring.",
"type": "array",
"items": {
"type": "string"
}
},
"mode": {
"type": "string"
}
}
},
"v1alpha1SourceIntegrityGitPolicyRepo": {
"type": "object",
"properties": {
"url": {
"description": "URL specifier, glob.",
"type": "string"
}
}
},
"v1alpha1SuccessfulHydrateOperation": {
"type": "object",
"title": "SuccessfulHydrateOperation contains information about the most recent successful hydrate operation",

View file

@ -31,10 +31,10 @@ import (
"github.com/argoproj/argo-cd/v3/util/cli"
"github.com/argoproj/argo-cd/v3/util/env"
"github.com/argoproj/argo-cd/v3/util/errors"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v3/util/healthz"
utilio "github.com/argoproj/argo-cd/v3/util/io"
"github.com/argoproj/argo-cd/v3/util/profile"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
"github.com/argoproj/argo-cd/v3/util/tls"
traceutil "github.com/argoproj/argo-cd/v3/util/trace"
)
@ -201,13 +201,13 @@ func NewCommand() *cobra.Command {
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf("%s:%d", metricsHost, metricsPort), mux)) }()
go func() { errors.CheckError(askPassServer.Run()) }()
if gpg.IsGPGEnabled() {
if sourceintegrity.IsGPGEnabled() {
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
err = gpg.InitializeGnuPG()
err = sourceintegrity.InitializeGnuPG()
errors.CheckError(err)
log.Infof("Populating GnuPG keyring with keys from %s", gnuPGSourcePath)
added, removed, err := gpg.SyncKeyRingFromDirectory(gnuPGSourcePath)
added, removed, err := sourceintegrity.SyncKeyRingFromDirectory(gnuPGSourcePath)
errors.CheckError(err)
log.Infof("Loaded %d (and removed %d) keys from keyring", len(added), len(removed))

View file

@ -26,8 +26,8 @@ import (
"github.com/argoproj/argo-cd/v3/util/cli"
"github.com/argoproj/argo-cd/v3/util/errors"
"github.com/argoproj/argo-cd/v3/util/git"
"github.com/argoproj/argo-cd/v3/util/gpg"
utilio "github.com/argoproj/argo-cd/v3/util/io"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
"github.com/argoproj/argo-cd/v3/util/templates"
)
@ -185,11 +185,12 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "add-signature-key PROJECT KEY-ID",
Short: "Add GnuPG signature key to project",
Short: "Add GnuPG signature key to project (DEPRECATED)",
Example: templates.Examples(`
# Add GnuPG signature key KEY-ID to project PROJECT
argocd proj add-signature-key PROJECT KEY-ID
`),
Deprecated: "Managing project signature keys through CLI is deprecated, migrate to Source Integrity defined in AppProject manifest",
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
@ -200,8 +201,9 @@ func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *c
projName := args[0]
signatureKey := args[1]
if !gpg.IsShortKeyID(signatureKey) && !gpg.IsLongKeyID(signatureKey) {
log.Fatalf("%s is not a valid GnuPG key ID", signatureKey)
_, err := sourceintegrity.KeyID(signatureKey)
if err != nil {
log.Fatal(err.Error())
}
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
@ -227,11 +229,12 @@ func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *c
func NewProjectRemoveSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "remove-signature-key PROJECT KEY-ID",
Short: "Remove GnuPG signature key from project",
Short: "Remove GnuPG signature key from project (DEPRECATED)",
Example: templates.Examples(`
# Remove GnuPG signature key KEY-ID from project PROJECT
argocd proj remove-signature-key PROJECT KEY-ID
`),
Deprecated: "Managing project signature keys through CLI is deprecated, migrate to Source Integrity defined in AppProject manifest",
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

View file

@ -15,7 +15,7 @@ import (
"github.com/argoproj/argo-cd/v3/pkg/apis/application"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/config"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
)
type ProjectOpts struct {
@ -128,10 +128,11 @@ func (opts *ProjectOpts) GetDestinationServiceAccounts() []v1alpha1.ApplicationD
func (opts *ProjectOpts) GetSignatureKeys() []v1alpha1.SignatureKey {
signatureKeys := make([]v1alpha1.SignatureKey, 0)
for _, keyStr := range opts.SignatureKeys {
if !gpg.IsShortKeyID(keyStr) && !gpg.IsLongKeyID(keyStr) {
log.Fatalf("'%s' is not a valid GnuPG key ID", keyStr)
keyId, err := sourceintegrity.KeyID(keyStr)
if err != nil {
log.Fatal(err.Error())
}
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: keyId})
}
return signatureKeys
}

View file

@ -1717,9 +1717,8 @@ func TestSetOperationStateOnDeletedApp(t *testing.T) {
func TestSetOperationStateLogRetries(t *testing.T) {
hook := utilTest.LogHook{}
logrus.AddHook(&hook)
t.Cleanup(func() {
logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{})
})
t.Cleanup(hook.CleanupHook)
ctrl := newFakeController(t.Context(), &fakeData{apps: []runtime.Object{}}, nil)
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
fakeAppCs.ReactionChain = nil

View file

@ -105,7 +105,7 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
}
// Fetch target objects from Git to know which hooks should exist
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(context.Background(), app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, true)
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(context.Background(), app, app.Spec.GetSources(), appLabelKey, revisions, false, false, nil, proj, true)
if err != nil {
return false, err
}

View file

@ -51,7 +51,7 @@ func (ctrl *ApplicationController) GetRepoObjs(ctx context.Context, origApp *app
delete(app.Annotations, appv1.AnnotationKeyManifestGeneratePaths)
// FIXME: use cache and revision cache
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(ctx, app, drySources, appLabelKey, dryRevisions, true, true, false, project, false)
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(ctx, app, drySources, appLabelKey, dryRevisions, true, true, nil, project, false)
if err != nil {
return nil, nil, fmt.Errorf("failed to get repo objects: %w", err)
}

View file

@ -22,6 +22,9 @@ import (
resourceutil "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/resource"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/syncwaves"
kubeutil "github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -41,7 +44,6 @@ import (
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
"github.com/argoproj/argo-cd/v3/util/db"
"github.com/argoproj/argo-cd/v3/util/gpg"
utilio "github.com/argoproj/argo-cd/v3/util/io"
"github.com/argoproj/argo-cd/v3/util/settings"
"github.com/argoproj/argo-cd/v3/util/stats"
@ -72,7 +74,7 @@ type managedResource struct {
type AppStateManager interface {
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error)
SyncAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, state *v1alpha1.OperationState)
GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
}
// comparisonResult holds the state of an application after the reconciliation
@ -128,7 +130,7 @@ type appStateManager struct {
// task to the repo-server. It returns the list of generated manifests as unstructured
// objects. It also returns the full response from all calls to the repo server as the
// second argument.
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
ts := stats.NewTimingStats()
helmRepos, err := m.db.ListHelmRepositories(ctx)
if err != nil {
@ -233,8 +235,9 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
keyManifestGenerateAnnotationVal, keyManifestGenerateAnnotationExists := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]
sourceCount := len(sources)
for i, source := range sources {
if len(revisions) < len(sources) || revisions[i] == "" {
if len(revisions) < sourceCount || revisions[i] == "" {
revisions[i] = source.TargetRevision
}
repo, err := m.db.GetRepository(ctx, source.RepoURL, proj.Name)
@ -242,6 +245,15 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
return nil, nil, false, fmt.Errorf("failed to get repo %q: %w", source.RepoURL, err)
}
syncedRevision := app.Status.Sync.Revision
if app.Spec.HasMultipleSources() {
if i < len(app.Status.Sync.Revisions) {
syncedRevision = app.Status.Sync.Revisions[i]
} else {
syncedRevision = ""
}
}
revision := revisions[i]
appNamespace := app.Spec.Destination.Namespace
@ -283,7 +295,7 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
KustomizeOptions: kustomizeSettings,
KubeVersion: serverVersion,
ApiVersions: apiVersions,
VerifySignature: verifySignature,
SourceIntegrity: sourceIntegrity,
HelmRepoCreds: helmRepoCreds,
TrackingMethod: trackingMethod,
EnabledSourceTypes: enabledSourceTypes,
@ -296,7 +308,7 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
InstallationID: installationID,
})
if err != nil {
genErr := fmt.Errorf("failed to generate manifest for source %d of %d: %w", i+1, len(sources), err)
genErr := fmt.Errorf("failed to generate manifest for source %d of %d: %w", i+1, sourceCount, err)
if app.Spec.SourceHydrator != nil && app.Spec.SourceHydrator.HydrateTo != nil && strings.Contains(err.Error(), path.ErrMessageAppPathDoesNotExist) {
genErr = fmt.Errorf("%w - waiting for an external process to update %s from %s", genErr, app.Spec.SourceHydrator.SyncSource.TargetBranch, app.Spec.SourceHydrator.HydrateTo.TargetBranch)
}
@ -305,10 +317,21 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
targetObj, err := unmarshalManifests(manifestInfo.Manifests)
if err != nil {
return nil, nil, false, fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, len(sources), err)
return nil, nil, false, fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, sourceCount, err)
}
targetObjs = append(targetObjs, targetObj...)
manifestInfos = append(manifestInfos, manifestInfo)
// Update eventual check problems with the ID of the current source. This is so users can attribute problems to correct sources
if sourceCount > 1 {
var sourceId string
if source.Name != "" {
sourceId = "source " + source.Name
} else {
sourceId = fmt.Sprintf("source %d of %d", i+1, sourceCount)
}
manifestInfo.SourceIntegrityResult.InjectSourceName(sourceId)
}
}
ts.AddCheckpoint("manifests_ms")
@ -503,45 +526,6 @@ func (m *appStateManager) getComparisonSettings() (string, map[string]v1alpha1.R
return appLabelKey, resourceOverrides, resFilter, installationID, trackingMethod, nil
}
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
// revision.
func verifyGnuPGSignature(revision string, project *v1alpha1.AppProject, manifestInfo *apiclient.ManifestResponse) []v1alpha1.ApplicationCondition {
now := metav1.Now()
conditions := make([]v1alpha1.ApplicationCondition, 0)
// We need to have some data in the verification result to parse, otherwise there was no signature
if manifestInfo.VerifyResult != "" {
verifyResult := gpg.ParseGitCommitVerification(manifestInfo.VerifyResult)
switch verifyResult.Result {
case gpg.VerifyResultGood:
// This is the only case we allow to sync to, but we need to make sure signing key is allowed
validKey := false
for _, k := range project.Spec.SignatureKeys {
if gpg.KeyID(k.KeyID) == gpg.KeyID(verifyResult.KeyID) && gpg.KeyID(k.KeyID) != "" {
validKey = true
break
}
}
if !validKey {
msg := fmt.Sprintf("Found good signature made with %s key %s, but this key is not allowed in AppProject",
verifyResult.Cipher, verifyResult.KeyID)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
}
case gpg.VerifyResultInvalid:
msg := fmt.Sprintf("Found signature made with %s key %s, but verification result was invalid: '%s'",
verifyResult.Cipher, verifyResult.KeyID, verifyResult.Message)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
default:
msg := fmt.Sprintf("Could not verify commit signature on revision '%s', check logs for more information.", revision)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
}
} else {
msg := fmt.Sprintf("Target revision %s in Git is not signed, but a signature is required", revision)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
}
return conditions
}
func isManagedNamespace(ns *unstructured.Unstructured, app *v1alpha1.Application) bool {
return ns != nil && ns.GetKind() == kubeutil.NamespaceKind && ns.GetName() == app.Spec.Destination.Namespace && app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.ManagedNamespaceMetadata != nil
}
@ -604,9 +588,6 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
return &comparisonResult{syncStatus: syncStatus, healthStatus: health.HealthStatusUnknown}, nil
}
// When signature keys are defined in the project spec, we need to verify the signature on the Git revision
verifySignature := len(project.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled()
// do best effort loading live and target state to present as much information about app state as possible
failedToLoadObjs := false
conditions := make([]v1alpha1.ApplicationCondition, 0)
@ -636,7 +617,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
}
}
targetObjs, manifestInfos, revisionsMayHaveChanges, err = m.GetRepoObjs(context.Background(), app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, true)
targetObjs, manifestInfos, revisionsMayHaveChanges, err = m.GetRepoObjs(context.Background(), app, sources, appLabelKey, revisions, noCache, noRevisionCache, project.EffectiveSourceIntegrity(), project, true)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
msg := "Failed to load target state: " + err.Error()
@ -658,10 +639,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
m.repoErrorCache.Delete(app.Name)
}
} else {
// Prevent applying local manifests for now when signature verification is enabled
// Prevent applying local manifests for now when source integrity is enforced
// This is also enforced on API level, but as a last resort, we also enforce it here
if gpg.IsGPGEnabled() && verifySignature {
msg := "Cannot use local manifests when signature verification is required"
if sourceintegrity.HasCriteria(project.EffectiveSourceIntegrity(), sources...) {
msg := "Cannot use local manifests when source integrity is enforced"
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
failedToLoadObjs = true
@ -1003,12 +984,15 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: "error setting app health: " + err.Error(), LastTransitionTime: &now})
}
// Git has already performed the signature verification via its GPG interface, and the result is available
// in the manifest info received from the repository server. We now need to form our opinion about the result
// and stop processing if we do not agree about the outcome.
for _, manifestInfo := range manifestInfos {
if gpg.IsGPGEnabled() && verifySignature && manifestInfo != nil {
conditions = append(conditions, verifyGnuPGSignature(manifestInfo.Revision, project, manifestInfo)...)
if manifestInfo != nil {
if err = manifestInfo.SourceIntegrityResult.AsError(); err != nil {
conditions = append(conditions, v1alpha1.ApplicationCondition{
Type: v1alpha1.ApplicationConditionComparisonError,
Message: err.Error(),
LastTransitionTime: &now,
})
}
}
}

View file

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"
@ -1097,17 +1096,7 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) {
assert.Empty(t, app.Status.History)
}
// helper function to read contents of a file to string
// panics on error
func mustReadFile(path string) string {
b, err := os.ReadFile(path)
if err != nil {
panic(err.Error())
}
return string(b)
}
var signedProj = v1alpha1.AppProject{
var projWithSourceIntegrity = v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: test.FakeArgoCDNamespace,
@ -1120,54 +1109,31 @@ var signedProj = v1alpha1.AppProject{
Namespace: "*",
},
},
SignatureKeys: []v1alpha1.SignatureKey{
{
KeyID: "4AEE18F83AFDEB23",
SourceIntegrity: &v1alpha1.SourceIntegrity{
Git: &v1alpha1.SourceIntegrityGit{
Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
Keys: []string{"4AEE18F83AFDEB23"},
},
}},
},
},
},
}
func TestSignedResponseNoSignatureRequired(t *testing.T) {
func TestNoSourceIntegrity(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
// We have a good signature response, but project does not require signed commits
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(t.Context(), &data, nil)
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "")
compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Empty(t, app.Status.Conditions)
}
// We have a bad signature response, but project does not require signed commits
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
SourceIntegrityResult: nil, // No verification requested
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
@ -1187,19 +1153,22 @@ func TestSignedResponseNoSignatureRequired(t *testing.T) {
}
}
func TestSignedResponseSignatureRequired(t *testing.T) {
func TestValidSourceIntegrity(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
// We have a good signature response, valid key, and signing is required - sync!
// Source integrity required, and it is valid - sync!
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: "Some/check",
Problems: []string{}, // Valid
}}},
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
@ -1208,7 +1177,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
@ -1217,16 +1186,19 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
assert.Empty(t, compRes.managedResources)
assert.Empty(t, app.Status.Conditions)
}
// We have a bad signature response and signing is required - do not sync
// Source integrity required, not valid - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: "Some/check",
Problems: []string{"The thing have failed to validate!"},
}}},
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
@ -1235,170 +1207,64 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Len(t, app.Status.Conditions, 1)
}
// We have a malformed signature response and signing is required - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_malformed1.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(t.Context(), &data, nil)
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Len(t, app.Status.Conditions, 1)
}
// We have no signature response (no signature made) and signing is required - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(t.Context(), &data, nil)
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Len(t, app.Status.Conditions, 1)
require.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "Some/check: The thing have failed to validate!")
}
// We have a good signature and signing is required, but key is not allowed - do not sync
// Source integrity required, unknown key - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: "Some/check",
Problems: []string{"The thing have failed to validate!"},
}}},
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(t.Context(), &data, nil)
testProj := signedProj
testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &testProj, revisions, sources, false, false, nil, false)
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "key is not allowed")
}
// Signature required and local manifests supplied - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
// it doesn't matter for our test whether local manifests are valid
localManifests := []string{"foobar"}
ctrl := newFakeController(t.Context(), &data, nil)
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, localManifests, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
require.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "The thing have failed to validate!")
}
t.Setenv("ARGOCD_GPG_ENABLED", "false")
// We have a bad signature response and signing would be required, but GPG subsystem is disabled - sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(t.Context(), &data, nil)
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, nil, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, v1alpha1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Empty(t, compRes.resources)
assert.Empty(t, compRes.managedResources)
assert.Empty(t, app.Status.Conditions)
}
// Signature required and local manifests supplied and GPG subsystem is disabled - sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "",
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: "Some/check",
Problems: []string{"The thing have failed to validate!"},
}}},
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
@ -1409,7 +1275,7 @@ func TestSignedResponseSignatureRequired(t *testing.T) {
sources = append(sources, app.Spec.GetSource())
revisions := make([]string, 0)
revisions = append(revisions, "abc123")
compRes, err := ctrl.appStateManager.CompareAppState(app, &signedProj, revisions, sources, false, false, localManifests, false)
compRes, err := ctrl.appStateManager.CompareAppState(app, &projWithSourceIntegrity, revisions, sources, false, false, localManifests, false)
require.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
@ -1637,11 +1503,10 @@ func TestUseDiffCache(t *testing.T) {
"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-svc\",\"namespace\":\"httpbin\"},\"spec\":{\"ports\":[{\"name\":\"http-port\",\"port\":7777,\"targetPort\":80},{\"name\":\"test\",\"port\":333}],\"selector\":{\"app\":\"httpbin\"}}}",
"{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-deployment\",\"namespace\":\"httpbin\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"httpbin\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"httpbin\"}},\"spec\":{\"containers\":[{\"image\":\"kennethreitz/httpbin\",\"imagePullPolicy\":\"Always\",\"name\":\"httpbin\",\"ports\":[{\"containerPort\":80}]}]}}}}",
},
Namespace: "",
Server: "",
Revision: revision,
SourceType: "Kustomize",
VerifyResult: "",
Namespace: "",
Server: "",
Revision: revision,
SourceType: "Kustomize",
},
}
}
@ -1983,7 +1848,7 @@ func TestCompareAppState_CallUpdateRevisionForPaths_ForOCI(t *testing.T) {
sources := make([]v1alpha1.ApplicationSource, 0)
sources = append(sources, source)
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false)
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, defaultProj.EffectiveSourceIntegrity(), &defaultProj, false)
require.NoError(t, err)
require.False(t, revisionsMayHaveChanges)
}
@ -2037,7 +1902,7 @@ func TestCompareAppState_CallUpdateRevisionForPaths_ForMultiSource(t *testing.T)
sources := app.Spec.Sources
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "0.0.1", revisions, false, false, false, &defaultProj, false)
_, _, revisionsMayHaveChanges, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "0.0.1", revisions, false, false, defaultProj.EffectiveSourceIntegrity(), &defaultProj, false)
require.NoError(t, err)
require.False(t, revisionsMayHaveChanges)
}
@ -2067,7 +1932,7 @@ func Test_GetRepoObjs_HydrateToAppPathNotExist(t *testing.T) {
ctrl := newFakeController(t.Context(), &fakeData{manifestResponse: &apiclient.ManifestResponse{}}, errors.New("env/prod/my-app: app path does not exist"))
source := app.Spec.GetSource()
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, false, &defaultProj, false)
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, nil, &defaultProj, false)
require.ErrorContains(t, err, "app path does not exist")
require.ErrorContains(t, err, "waiting for an external process to update env/prod from env/prod-next")
})
@ -2091,7 +1956,7 @@ func Test_GetRepoObjs_HydrateToAppPathNotExist(t *testing.T) {
ctrl := newFakeController(t.Context(), &fakeData{manifestResponse: &apiclient.ManifestResponse{}}, errors.New("env/prod/my-app: app path does not exist"))
source := app.Spec.GetSource()
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, false, &defaultProj, false)
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, []v1alpha1.ApplicationSource{source}, "app", []string{""}, true, false, nil, &defaultProj, false)
require.ErrorContains(t, err, "app path does not exist")
require.NotContains(t, err.Error(), "waiting for an external process")
})

View file

@ -169,11 +169,14 @@ func TestSyncComparisonError(t *testing.T) {
data := fakeData{
apps: []runtime.Object{app, defaultProject},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "something went wrong",
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
SourceIntegrityResult: &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: "GIT/GPG",
Problems: []string{"Unknown key 'XXX'"},
}}},
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}

View file

@ -117,3 +117,16 @@ spec:
# Applications to reside in. Details: https://argo-cd.readthedocs.io/en/stable/operator-manual/app-any-namespace/
sourceNamespaces:
- "argocd-apps-*"
# Source Integrity declares criteria for application source repositories, such as cryptographic signing, etc.
# https://argo-cd.readthedocs.io/en/latest/user-guide/source-integrity/
sourceIntegrity:
git:
policies:
- repos:
- url: 'https://github.com/foo/*'
- url: '!https://github.com/foo/bar.git'
gpg:
mode: strict
keys:
- "D56C4FCA57A46444"

View file

@ -29,7 +29,7 @@ data:
## New RBAC rules for GnuPG related features
The [GnuPG feature](../../user-guide/gpg-verification.md) has introduced a new
The [GnuPG feature](../../user-guide/source-integrity-git-gpg) has introduced a new
RBAC resource in Argo CD, `gpgkeys`.
Please adapt your RBAC rules with the appropriate permissions. The least set of
@ -40,6 +40,6 @@ p, <your-role>, gpgkeys, get, *, allow
```
More information can be found in the
[documentation](../../user-guide/gpg-verification.md#rbac-rules-for-managing-gnupg-keys)
[documentation](../../user-guide/source-integrity-git-gpg#keyring-rbac-rules).
From here on you can follow the [regular upgrade process](./overview.md).

View file

@ -290,7 +290,7 @@ In fact, it does not harm UX *and* improves security with:
#### Git history *sealing* for strict verification mode
A sealing commit is a gpg signed commit that works as a "seal of approval" attesting that all its ancestor commits were either signed by a trusted key, or reviewed and trusted by the commit author.
Argo CD verifying gpg signatures would then progres only as far back in the history as the most recent "seal" commits in each individual ancestral branch.
Argo CD verifying gpg signatures would then progress only as far back in the history as the most recent "seal" commits in each individual ancestral branch.
In practice, a commiter reviews all commits that are not signed or signed with untrusted keys from the previous "seal" and creates a (possibly empty) commit with a custom trailer.
Such commits can have the following organization level semantics:

View file

@ -86,7 +86,6 @@ argocd proj [flags]
* [argocd proj add-destination](argocd_proj_add-destination.md) - Add project destination
* [argocd proj add-destination-service-account](argocd_proj_add-destination-service-account.md) - Add project destination's default service account
* [argocd proj add-orphaned-ignore](argocd_proj_add-orphaned-ignore.md) - Add a resource to orphaned ignore list
* [argocd proj add-signature-key](argocd_proj_add-signature-key.md) - Add GnuPG signature key to project
* [argocd proj add-source](argocd_proj_add-source.md) - Add project source repository
* [argocd proj add-source-namespace](argocd_proj_add-source-namespace.md) - Add source namespace to the AppProject
* [argocd proj allow-cluster-resource](argocd_proj_allow-cluster-resource.md) - Adds a cluster-scoped API resource to the allow list and removes it from deny list
@ -101,7 +100,6 @@ argocd proj [flags]
* [argocd proj remove-destination](argocd_proj_remove-destination.md) - Remove project destination
* [argocd proj remove-destination-service-account](argocd_proj_remove-destination-service-account.md) - Remove default destination service account from the project
* [argocd proj remove-orphaned-ignore](argocd_proj_remove-orphaned-ignore.md) - Remove a resource from orphaned ignore list
* [argocd proj remove-signature-key](argocd_proj_remove-signature-key.md) - Remove GnuPG signature key from project
* [argocd proj remove-source](argocd_proj_remove-source.md) - Remove project source repository
* [argocd proj remove-source-namespace](argocd_proj_remove-source-namespace.md) - Removes the source namespace from the AppProject
* [argocd proj role](argocd_proj_role.md) - Manage a project's roles

View file

@ -2,7 +2,7 @@
## argocd proj add-signature-key
Add GnuPG signature key to project
Add GnuPG signature key to project (DEPRECATED)
```
argocd proj add-signature-key PROJECT KEY-ID [flags]

View file

@ -2,7 +2,7 @@
## argocd proj remove-signature-key
Remove GnuPG signature key from project
Remove GnuPG signature key from project (DEPRECATED)
```
argocd proj remove-signature-key PROJECT KEY-ID [flags]

View file

@ -1,335 +1,6 @@
# GnuPG signature verification
## Overview
As of v1.7 it is possible to configure ArgoCD to only sync against commits
that are signed in Git using GnuPG. Signature verification is configured on
project level.
If a project is configured to enforce signature verification, all applications
associated with this project must have the commits in the source repositories
signed with a GnuPG public key known to ArgoCD. ArgoCD will refuse to sync to
any revision that does not have a valid signature made by one of the configured
keys. The controller will emit a `ResourceComparison` error if it tries to sync
to a revision that is either not signed, or is signed by an unknown or not
allowed public key.
By default, signature verification is enabled but not enforced. If you wish to
completely disable the GnuPG functionality in ArgoCD, you have to set the
environment variable `ARGOCD_GPG_ENABLED` to `"false"` in the pod templates of
the `argocd-server`, `argocd-repo-server`, `argocd-application-controller` and
`argocd-applicationset-controller` deployment manifests.
Verification of GnuPG signatures is only supported with Git repositories. It is
not possible using Helm repositories.
> [!NOTE]
> **A few words about trust**
> **GPG Signature verification is deprecated and will be removed in the next major Argo CD version.**
>
> ArgoCD uses a very simple trust model for the keys you import: Once the key
> is imported, ArgoCD will trust it. ArgoCD does not support more complex
> trust models, and it is not necessary (nor possible) to sign the public keys
> you are going to import into ArgoCD.
> [!NOTE]
> Signature verification is not supported for the templated `project` field when
> using the Git generator.
## Signature verification targets
If signature verification is enforced, ArgoCD will verify the signature using
following strategy:
* If `target revision` is a pointer to a commit object (i.e. a branch name, the
name of a reference such as `HEAD` or a commit SHA), ArgoCD will perform the
signature verification on the commit object the name points to, i.e. a commit.
* If `target revision` resolves to a tag and the tag is a lightweight tag, the
behaviour is same as if `target revision` would be a pointer to a commit
object. However, if the tag is annotated, the target revision will point to
a *tag* object and thus, the signature verification is performed on the tag
object, i.e. the tag itself must be signed (using `git tag -s`).
## Enforcing signature verification
To configure enforcing of signature verification, the following steps must be
performed:
* Import the GnuPG public key(s) used for signing commits in ArgoCD
* Configure a project to enforce signature verification for given keys
Once you have configured one or more keys to be required for verification for
a given project, enforcement is active for all applications associated with
this project.
> [!WARNING]
> If signature verification is enforced, you will not be able to sync from
> local sources (i.e. `argocd app sync --local`) anymore.
## RBAC rules for managing GnuPG keys
The appropriate resource notation for Argo CD's RBAC implementation to allow
the managing of GnuPG keys is `gpgkeys`.
To allow listing of keys for a role named `role:myrole`, use:
```
p, role:myrole, gpgkeys, get, *, allow
```
To allow adding keys for a role named `role:myrole`, use:
```
p, role:myrole, gpgkeys, create, *, allow
```
And finally, to allow deletion of keys for a role named `role:myrole`, use:
```
p, role:myrole, gpgkeys, delete, *, allow
```
## Importing GnuPG public keys
You can configure the GnuPG public keys that ArgoCD will use for verification
of commit signatures using either the CLI, the web UI or configuring it using
declarative setup.
> [!NOTE]
> After you have imported a GnuPG key, it may take a while until the key is
> propagated within the cluster, even if listed as configured. If you still
> cannot sync to commits signed by the already imported key, please see the
> troubleshooting section below.
Users wanting to manage the GnuPG public key configuration require the RBAC
permissions for `gpgkeys` resources.
### Manage public keys using the CLI
To configure GnuPG public keys using the CLI, use the `argocd gpg` command.
#### Listing all configured keys
To list all configured keys known to ArgoCD, use the `argocd gpg list`
sub-command:
```bash
argocd gpg list
```
#### Show information about a certain key
To get information about a specific key, use the `argocd gpg get` sub-command:
```bash
argocd gpg get <key-id>
```
#### Importing a key
To import a new key to ArgoCD, use the `argocd gpg add` sub-command:
```bash
argocd gpg add --from <path-to-key>
```
The key to be imported can be either in binary or ASCII-armored format.
#### Removing a key from configuration
To remove a previously configured key from the configuration, use the
`argocd gpg rm` sub-command:
```bash
argocd gpg rm <key-id>
```
### Manage public keys using the Web UI
Basic key management functionality for listing, importing and removing GnuPG
public keys is implemented in the Web UI. You can find the configuration
module from the **Settings** page in the **GnuPG keys** module.
Please note that when you configure keys using the Web UI, the key must be
imported in ASCII armored format for now.
### Manage public keys in declarative setup
ArgoCD stores public keys internally in the `argocd-gpg-keys-cm` ConfigMap
resource, with the public GnuPG key's ID as its name and the ASCII armored
key data as string value, i.e. the entry for the GitHub's web-flow signing
key would look like follows:
```yaml
4AEE18F83AFDEB23: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
x15OklctmrZtqre5kwPUosG3/B2/ikuPYElcHgGPL4uL5Em6S5C/oozfkYzhwRrT
SQzvYjsE4I34To4UdE9KA97wrQjGoz2Bx72WDLyWwctD3DKQtYeHXswXXtXwKfjQ
7Fy4+Bf5IPh76dA8NJ6UtjjLIDlKqdxLW4atHe6xWFaJ+XdLUtsAroZcXBeWDCPa
buXCDscJcLJRKZVc62gOZXXtPfoHqvUPp3nuLA4YjH9bphbrMWMf810Wxz9JTd3v
yWgGqNY0zbBqeZoGv+TuExlRHT8ASGFS9SVDABEBAAG0NUdpdEh1YiAod2ViLWZs
b3cgY29tbWl0IHNpZ25pbmcpIDxub3JlcGx5QGdpdGh1Yi5jb20+iQEiBBMBCAAW
BQJZlGhBCRBK7hj4Ov3rIwIbAwIZAQAAmQEH/iATWFmi2oxlBh3wAsySNCNV4IPf
DDMeh6j80WT7cgoX7V7xqJOxrfrqPEthQ3hgHIm7b5MPQlUr2q+UPL22t/I+ESF6
9b0QWLFSMJbMSk+BXkvSjH9q8jAO0986/pShPV5DU2sMxnx4LfLfHNhTzjXKokws
+8ptJ8uhMNIDXfXuzkZHIxoXk3rNcjDN5c5X+sK8UBRH092BIJWCOfaQt7v7wig5
4Ra28pM9GbHKXVNxmdLpCFyzvyMuCmINYYADsC848QQFFwnd4EQnupo6QvhEVx1O
j7wDwvuH5dCrLuLwtwXaQh0onG4583p0LGms2Mf5F+Ick6o/4peOlBoZz48=
=Bvzs
-----END PGP PUBLIC KEY BLOCK-----
```
## Configuring a project to enforce signature verification
Once you have imported the GnuPG keys to ArgoCD, you must now configure the
project to enforce the verification of commit signatures with the imported
keys.
### Configuring using the CLI
#### Adding a key ID to list of allowed keys
To add a key ID to the list of allowed GnuPG keys for a project, you can use
the `argocd proj add-signature-key` command, i.e. the following command would
add the key ID `4AEE18F83AFDEB23` to the project named `myproj`:
```bash
argocd proj add-signature-key myproj 4AEE18F83AFDEB23
```
#### Removing a key ID from the list of allowed keys
Similarly, you can remove a key ID from the list of allowed GnuPG keys for a
project using the `argocd proj remove-signature-key` command, i.e. to remove
the key added above from project `myproj`, use the command:
```bash
argocd proj remove-signature-key myproj 4AEE18F83AFDEB23
```
#### Showing allowed key IDs for a project
To see which key IDs are allowed for a given project, you can inspect the
output of the `argocd proj get` command, i.e. for a project named `gpg`:
```bash
$ argocd proj get gpg
Name: gpg
Description: GnuPG verification
Destinations: *,*
Repositories: *
Allowed Cluster Resources: */*
Denied Namespaced Resources: <none>
Signature keys: 4AEE18F83AFDEB23, 07E34825A909B250
Orphaned Resources: disabled
```
#### Override list of key IDs
You can also explicitly set the currently allowed keys with one or more new keys
using the `argocd proj set` command in combination with the `--signature-keys`
flag, which you can use to specify a comma separated list of allowed key IDs:
```bash
argocd proj set myproj --signature-keys 4AEE18F83AFDEB23,07E34825A909B250
```
The `--signature-keys` flag can also be used on project creation, i.e. the
`argocd proj create` command.
### Configure using the Web UI
You can configure the GnuPG key IDs required for signature verification using
the web UI, in the Project configuration. Navigate to the **Settings** page
and select the **Projects** module, then click on the project you want to
configure.
From the project's details page, click **Edit** and find the
**Required signature keys** section, where you can add or remove the key IDs
for signature verification. After you have modified your project, click
**Update** to save the changes.
### Configure using declarative setup
You can specify the key IDs required for signature verification in the project
manifest within the `signatureKeys` section, i.e:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: gpg
namespace: argocd
spec:
clusterResourceWhitelist:
- group: '*'
kind: '*'
description: GnuPG verification
destinations:
- namespace: '*'
server: '*'
namespaceResourceWhitelist:
- group: '*'
kind: '*'
signatureKeys:
- keyID: 4AEE18F83AFDEB23
sourceRepos:
- '*'
```
`signatureKeys` is an array of `SignatureKey` objects, whose only property is
`keyID` at the moment.
## Troubleshooting
### Disabling the feature
The GnuPG feature can be completely disabled if desired. In order to disable it,
set the environment variable `ARGOCD_GPG_ENABLED` to `false` for the pod
templates of the `argocd-server`, `argocd-repo-server`, `argocd-application-controller`
and `argocd-applicationset-controller` deployments.
After the pods have been restarted, the GnuPG feature is disabled.
### GnuPG key ring
The GnuPG key ring used for signature verification is maintained within the
pods of `argocd-repo-server`. The keys in the keyring are synchronized to the
configuration stored in the `argocd-gpg-keys-cm` ConfigMap resource, which is
volume-mounted to the `argocd-repo-server` pods.
> [!NOTE]
> The GnuPG key ring in the pods is transient and gets recreated from the
> configuration on each restart of the pods. You should never add or remove
> keys manually to the key ring, because your changes will be lost. Also,
> any of the private keys found in the key ring are transient and will be
> regenerated upon each restart. The private key is only used to build the
> trust DB for the running pod.
To check whether the keys are actually in sync, you can `kubectl exec` into the
repository server's pods and inspect the key ring, which is located at path
`/app/config/gpg/keys`
```bash
$ kubectl exec -it argocd-repo-server-7d6bdfdf6d-hzqkg bash
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$ GNUPGHOME=/app/config/gpg/keys gpg --list-keys
/app/config/gpg/keys/pubring.kbx
--------------------------------
pub rsa2048 2020-06-15 [SC] [expires: 2020-12-12]
D48F075D818A813C436914BC9324F0D2144753B1
uid [ultimate] Anon Ymous (ArgoCD key signing key) <noreply@argoproj.io>
pub rsa2048 2017-08-16 [SC]
5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23
uid [ultimate] GitHub (web-flow commit signing) <noreply@github.com>
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$
```
If the key ring stays out of sync with your configuration after you have added
or removed keys for a longer period of time, you might want to restart your
`argocd-repo-server` pods. If such a problem persists, please consider raising
a bug report.
> Consult [Source Integrity Verification](./source-integrity.md) for the new and expanded alternative.

View file

@ -0,0 +1,373 @@
# Git GnuPG signature verification
## Overview
Verify that commits in the source repository are correctly signed with one of the blessed GnuPG keys.
> [!NOTE]
> **A few words about trust**
>
> ArgoCD uses a very simple trust model for the keys you import: Once the key
> is imported, ArgoCD will trust it. ArgoCD does not support more complex
> trust models, and it is not necessary (nor possible) to sign the public keys
> you are going to import into ArgoCD.
> [!NOTE]
> **Compatibility notice**
>
> The GnuPG verification was first introduced in v1.7 as a project-wide constraint configured by `signatureKeys`.
> As of v**TODO**, it is supported as one of the methods for source integrity verification, but it is using a different declaration format.
> Keys configured in `signatureKeys` will continue to be supported, but they cannot be used together with `sourceIntegrity`.
> See below on how to convert the legacy `signatureKeys` configuration to `sourceIntegrity`.
Verification of GnuPG signatures is only supported with Git repositories. It is
not possible when using Helm or OCI application sources.
The GnuPG verification requires populating the Argo CD GnuPG keyring, and configuring source integrity policies for your repositories.
## Managing Argo CD GnuPG keyring
All the GnuPG keys Argo CD is going to trust must be introduced in its keyring first.
### Keyring RBAC rules
The appropriate resource notation for Argo CD's RBAC implementation to allow
the managing of GnuPG keys is `gpgkeys`.
To allow *listing* of keys for a role named `role:myrole`, use:
```
p, role:myrole, gpgkeys, get, *, allow
```
To allow *adding* keys for a role named `role:myrole`, use:
```
p, role:myrole, gpgkeys, create, *, allow
```
And finally, to allow *deletion* of keys for a role named `role:myrole`, use:
```
p, role:myrole, gpgkeys, delete, *, allow
```
### Keyring management
You can configure the GnuPG public keys that ArgoCD will use for verification
of commit signatures using either the CLI, the web UI or configuring it using
declarative setup.
> [!NOTE]
> After you have imported a GnuPG key, it may take a while until the key is
> propagated within the cluster, even if listed as configured. If you still
> cannot sync to commits signed by the already imported key, please see the
> troubleshooting section below.
#### Manage public keys using the CLI
To configure GnuPG public keys using the CLI, use the `argocd gpg` command.
##### Listing all configured keys
To list all configured keys known to ArgoCD, use the `argocd gpg list`
sub-command:
```bash
argocd gpg list
```
##### Show information about a certain key
To get information about a specific key, use the `argocd gpg get` sub-command:
```bash
argocd gpg get <key-id>
```
##### Importing a key
To import a new *public* key to ArgoCD, use the `argocd gpg add` sub-command:
```bash
argocd gpg add --from <path-to-key>
```
The key to be imported can be either in binary or ASCII-armored format.
##### Removing a key from the configuration
To remove a previously configured key from the configuration, use the
`argocd gpg rm` sub-command:
```bash
argocd gpg rm <key-id>
```
#### Manage public keys using the Web UI
Basic key management functionality for listing, importing and removing GnuPG
public keys is implemented in the Web UI. You can find the configuration
module from the **Settings** page in the **GnuPG keys** module.
Please note that when you configure keys using the Web UI, the key must be
imported in ASCII armored format for now.
#### Manage public keys in declarative setup
ArgoCD stores public keys internally in the `argocd-gpg-keys-cm` ConfigMap
resource, with the public GnuPG key's ID as its name and the ASCII armored
key data as string value, i.e. the entry for the GitHub's web-flow signing
key would look like follows:
```yaml
4AEE18F83AFDEB23: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
x15OklctmrZtqre5kwPUosG3/B2/ikuPYElcHgGPL4uL5Em6S5C/oozfkYzhwRrT
SQzvYjsE4I34To4UdE9KA97wrQjGoz2Bx72WDLyWwctD3DKQtYeHXswXXtXwKfjQ
7Fy4+Bf5IPh76dA8NJ6UtjjLIDlKqdxLW4atHe6xWFaJ+XdLUtsAroZcXBeWDCPa
buXCDscJcLJRKZVc62gOZXXtPfoHqvUPp3nuLA4YjH9bphbrMWMf810Wxz9JTd3v
yWgGqNY0zbBqeZoGv+TuExlRHT8ASGFS9SVDABEBAAG0NUdpdEh1YiAod2ViLWZs
b3cgY29tbWl0IHNpZ25pbmcpIDxub3JlcGx5QGdpdGh1Yi5jb20+iQEiBBMBCAAW
BQJZlGhBCRBK7hj4Ov3rIwIbAwIZAQAAmQEH/iATWFmi2oxlBh3wAsySNCNV4IPf
DDMeh6j80WT7cgoX7V7xqJOxrfrqPEthQ3hgHIm7b5MPQlUr2q+UPL22t/I+ESF6
9b0QWLFSMJbMSk+BXkvSjH9q8jAO0986/pShPV5DU2sMxnx4LfLfHNhTzjXKokws
+8ptJ8uhMNIDXfXuzkZHIxoXk3rNcjDN5c5X+sK8UBRH092BIJWCOfaQt7v7wig5
4Ra28pM9GbHKXVNxmdLpCFyzvyMuCmINYYADsC848QQFFwnd4EQnupo6QvhEVx1O
j7wDwvuH5dCrLuLwtwXaQh0onG4583p0LGms2Mf5F+Ick6o/4peOlBoZz48=
=Bvzs
-----END PGP PUBLIC KEY BLOCK-----
```
## Policies for GnuPG signature verification
The GnuPG commit signature verification is configured through one or multiple Git `gpg` policies.
The policies are configured as illustrated:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
spec:
sourceIntegrity:
git:
policies:
- repos:
- url: "https://github.com/my-group/*"
- url: "!https://github.com/my-group/ignored.git"
gpg:
mode: "none|head|strict"
keys:
- "D56C4FCA57A46444"
```
The `repos ` key contains a list of glob-style patterns matched against the URL of the source to verify.
Given strategy will be used when matched some of the positive globs, while not matched by any of the negative ones (starting with `!`).
Only one policy is applied per source repository, and sources not matched by any policy will not have its integrity verified.
Note that a multi-source application can have each of its source repositories validated against a different policy.
### The `gpg` verification policy
The Git commit signature verification is an alternative to calling `git verify-commit`/`git verify-tag` with configured keyring and making sure the key ID used for the signatures is among the configured Key IDs in the source integrity policy.
If the target revision points to a commit or tags that do not satisfy those criteria, it will not be synced.
The `keys` key lists the set of key IDs to trust for signed commits.
If a commit in the repository is signed by an ID not specified in the list of trusted signers, the verification will fail.
The `mode` defines how thorough the GnuPG verification is:
##### Verification mode `none`
Verification is not performed for this strategy, and no following strategies are tried.
Note this accepts unsigned commits as well as commits with a signature that is invalid in some sense (expired, unverifiable, etc.).
##### Verification mode `head`
Verify only the commit/tag pointed to by the target revision of the source.
If the revision is an annotated tag, it is the tag's signature that is verified, not the commit's signature (i.e. the tag itself must be signed using `git tag -s`).
Otherwise, if target revision is a branch name, reference name (such as `HEAD`), or a commit SHA Argo CD verifies the commit's GnuPG signature.
##### Verification mode `strict`
Verify target revision and all its ancestors.
This makes sure there is no unsigned change in the history as well.
If the revision is an annotated tag, the tag's signature is verified together with the commit history, including the commit it points to.
There are situations where verifying the entire history is not practical - typically in case the history contains unsigned commits, or commits signed with keys that are no longer trusted.
This happens when GnuPG verification is introduced later to the git repository, or when formerly accepted keys get removed, revoked, or rotated.
While this can be addressed by re-signing with git rebase, there is a better way that does not require rewriting the Git history.
###### Commit seal-signing with `strict` mode
A sealing commit is a GnuPG signed commit that works as a "seal of approval" attesting that all its ancestor commits were either signed by a trusted key, or reviewed and trusted by the author of the sealing commit.
Argo CD verifying GnuPG signatures would then progress only as far back in the history as the most recent "seal" commits in each individual ancestral branch.
In practice, a committer first *reviews* all commits that are not signed or signed with untrusted keys from the previous "seal commit" and creates a new, possibly empty commit with a custom Git trailer in its message.
Such commits can have the following organization-level semantics:
- "From now on, we are going to GnuPG sign all commits in this repository. There is no point in verifying the unsigned ones from before."
- "I merge these changes from untrusted external contributor, and I approve of them."
- "I am removing the GnuPG key of Bob. All his previous commits are trusted, but no new ones will be. Happy retirement, Bob!"
- "I am replacing my old key with a new one. Trust my commits signed with the old key before this seal commit, but only trust my new key after this seal commit."
To create a seal commit, run `git commit --signoff --gpg-sign --trailer="Argocd-gpg-seal: <justification>"` and push to branch pulled by Argo CD.
Using seal commits is preferable to rewriting git history as it eliminates the room for eventual rebasing mistakes that would jeopardize either source integrity or correctness of the repository data.
## Upgrade to Source Integrity Verification
To migrate from the legacy declaration to the new source verification policies, remove `.spec.signatureKeys` and then define desired policies in `.spec.sourceIntegrity.git.policies`.
To achieve the legacy Argo CD verification behavior in a project, use the following config:
```yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
spec:
sourceIntegrity:
git:
policies:
- repos:
- url: "*" # For any repository in the project
gpg:
mode: "head" # Verify only the HEAD of the target revision
keys:
- "..." # Keys from .spec.signatureKeys
```
When `.spec.sourceIntegrity` is not defined but `.spec.signatureKeys` is, Argo CD will do similar conversion behind the scenes.
Though it is advised to perform the migration as source integrity config allows for greater flexibility, and `.spec.signatureKeys` will be a subject of removal in future releases.
## Downgrade from Source Integrity Verification
At downgrade time, reintroduce `.spec.signatureKeys` in any AppProject, populate it with all the keys from `.spec.sourceIntegrity.git.policies`, and then delete the `.spec.sourceIntegrity` section.
Mind the legacy functionality lacks many of the new features—it will be all "head" mode for all project repositories.
As an alternative to downgrade, consult the troubleshooting section here.
## Legacy signature key management (DEPRECATED)
The project-wide signature keys can be managed through UI and CLI.
Note they are being replaced by the source integrity policies, so users are advised to migrate away from these.
### Configuring using the CLI (DEPRECATED)
#### Adding a key ID to the list of allowed keys
To add a key ID to the list of allowed GnuPG keys for a project, you can use
the `argocd proj add-signature-key` command, i.e. the following command would
add the key ID `4AEE18F83AFDEB23` to the project named `myproj`:
```bash
# DEPRECATED
argocd proj add-signature-key myproj 4AEE18F83AFDEB23
```
#### Removing a key ID from the list of allowed keys
Similarly, you can remove a key ID from the list of allowed GnuPG keys for a
project using the `argocd proj remove-signature-key` command, i.e. to remove
the key added above from project `myproj`, use the command:
```bash
# DEPRECATED
argocd proj remove-signature-key myproj 4AEE18F83AFDEB23
```
#### Showing allowed key IDs for a project
To see which key IDs are allowed for a given project, you can inspect the
output of the `argocd proj get` command, i.e. for a project named `gpg`:
```bash
# DEPRECATED
$ argocd proj get gpg
Name: gpg
Description: GnuPG verification
Destinations: *,*
Repositories: *
Allowed Cluster Resources: */*
Denied Namespaced Resources: <none>
Signature keys: 4AEE18F83AFDEB23, 07E34825A909B250
Orphaned Resources: disabled
```
#### Override list of key IDs
You can also explicitly set the currently allowed keys with one or more new keys
using the `argocd proj set` command in combination with the `--signature-keys`
flag, which you can use to specify a comma separated list of allowed key IDs:
```bash
# DEPRECATED
argocd proj set myproj --signature-keys 4AEE18F83AFDEB23,07E34825A909B250
```
The `--signature-keys` flag can also be used on project creation, i.e. the
`argocd proj create` command.
### Configure using the Web UI (DEPRECATED)
You can configure the GnuPG key IDs required for signature verification using
the web UI, in the Project configuration. Navigate to the **Settings** page
and select the **Projects** module, then click on the project you want to
configure.
From the project's details page, click **Edit** and find the
**Required signature keys** section, where you can add or remove the key IDs
for signature verification. After you have modified your project, click
**Update** to save the changes.
## Troubleshooting
### Disabling the feature
The GnuPG feature can be completely disabled if desired. In order to disable it,
set the environment variable `ARGOCD_GPG_ENABLED` to `false` for the pod
templates of the `argocd-server`, `argocd-repo-server`, `argocd-application-controller`
and `argocd-applicationset-controller` deployment manifests.
After the pods have been restarted, the GnuPG feature is disabled.
### Inspecting GnuPG key ring
The GnuPG key ring used for signature verification is maintained within the
pods of `argocd-repo-server`. The keys in the keyring are synchronized to the
configuration stored in the `argocd-gpg-keys-cm` ConfigMap resource, which is
volume-mounted to the `argocd-repo-server` pods.
> [!NOTE]
> The GnuPG key ring in the pods is transient and gets recreated from the
> configuration on each restart of the pods. You should never add or remove
> keys manually to the key ring in the pod, because your changes will be lost. Also,
> any of the private keys found in the key ring are transient and will be
> regenerated upon each restart. The private key is only used to build the
> trust DB for the running pod.
To check whether the keys are actually in sync, you can `kubectl exec` into the
repository server's pods and inspect the key ring, which is located at path
`/app/config/gpg/keys`
```bash
$ kubectl exec -it argocd-repo-server-7d6bdfdf6d-hzqkg bash
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$ GNUPGHOME=/app/config/gpg/keys gpg --list-keys
/app/config/gpg/keys/pubring.kbx
--------------------------------
pub rsa2048 2020-06-15 [SC] [expires: 2020-12-12]
D48F075D818A813C436914BC9324F0D2144753B1
uid [ultimate] Anon Ymous (ArgoCD key signing key) <noreply@argoproj.io>
pub rsa2048 2017-08-16 [SC]
5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23
uid [ultimate] GitHub (web-flow commit signing) <noreply@github.com>
argocd@argocd-repo-server-7d6bdfdf6d-hzqkg:~$
```
If the key ring stays out of sync with your configuration after you have added
or removed keys for a longer period of time, you might want to restart your
`argocd-repo-server` pods. If such a problem persists, please consider raising
a bug report.

View file

@ -0,0 +1,29 @@
# Overview
Argo CD permits declaring criteria for application sources integrity that, when not met, will prevent an application from syncing with a `ResourceComparison` error.
This is useful to verify the sources have not been tampered with by an unauthorized contributor.
Each Application Project can have its criteria configured in `AppProject`'s `.spec.sourceIntegrity`.
The criteria distinguish a type of verification they perform, and to which sources they apply.
Each application can be a subject of multiple checks, and the sync will be enabled only when all criteria are met.
> [!NOTE]
> Source Integrity Verification is only configured through `AppProject` manifests at this point. CLI and UI are not supported.
> [!NOTE]
> Signature verification is not supported for the Application Sets populated by the git generator when they have the `project` field templated.
> [!WARNING]
> If source integrity is enforced, you will not be able to sync from local sources (i.e. `argocd app sync --local`) anymore.
## Supported methods
- [Git GnuPG verification](./source-integrity-git-gpg.md) verifies that Git commits are GnuPG Signed. This is a modern method of the commit signature verification originally configured in `AppProjects`'s `signatureKeys`.
## Multi-source applications
Each individual application source can be a subject of a different set of source integrity criteria, if desirable.
This is necessary if the sources are of a different type, such as Git and Helm.
But even different repositories of the same type can utilize different methods of verification, or their different configurations.
This is useful when an application combines sources maintained by different groups of people, or according to different contribution (and signing) guidelines.

2
go.mod
View file

@ -115,7 +115,7 @@ require (
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
k8s.io/kubectl v0.34.0
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.21.0

View file

@ -1,4 +1,7 @@
#!/bin/sh
# DEPRECATED: To be removed in the next major version when Signature verification is replaced with Source Integrity.
# Wrapper script to perform GPG signature validation on git commit SHAs and
# annotated tags.
#

View file

@ -30467,6 +30467,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

View file

@ -30467,6 +30467,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

View file

@ -268,6 +268,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

View file

@ -30467,6 +30467,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

View file

@ -30467,6 +30467,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

View file

@ -30467,6 +30467,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

49
manifests/install.yaml generated
View file

@ -30467,6 +30467,55 @@ spec:
- keyID
type: object
type: array
sourceIntegrity:
description: |-
SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
properties:
git:
description: Git - policies for git source verification
properties:
policies:
items:
properties:
gpg:
description: Verify GPG commit/tag signatures
properties:
keys:
description: List of key IDs to trust. The keys
need to be in the repository server keyring.
items:
type: string
type: array
mode:
type: string
required:
- keys
- mode
type: object
repos:
description: List of repository criteria restricting
repositories the policy will apply to
items:
properties:
url:
description: URL specifier, glob.
type: string
required:
- url
type: object
type: array
required:
- gpg
- repos
type: object
type: array
required:
- policies
type: object
required:
- git
type: object
sourceNamespaces:
description: SourceNamespaces defines the namespaces application resources
are allowed to be created in

View file

@ -180,7 +180,9 @@ nav:
- user-guide/private-repositories.md
- user-guide/plugins.md
- user-guide/multiple_sources.md
- GnuPG verification: user-guide/gpg-verification.md
- Source Integrity Verification:
- user-guide/source-integrity.md
- Git GnuPG verification: user-guide/source-integrity-git-gpg.md
- user-guide/auto_sync.md
- Diffing:
- Diff Strategies: user-guide/diff-strategies.md

File diff suppressed because it is too large Load diff

View file

@ -111,6 +111,10 @@ message AppProjectSpec {
// DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.
repeated ApplicationDestinationServiceAccount destinationServiceAccounts = 14;
// SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
// Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
optional SourceIntegrity sourceIntegrity = 15;
}
// AppProjectStatus contains status information for AppProject CRs
@ -2362,10 +2366,14 @@ message RevisionMetadata {
optional string message = 4;
// SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.
//
// Deprecated: Use SourceIntegrityResult for more detailed information. SignatureInfo will be removed with the next major version.
optional string signatureInfo = 5;
// References contains references to information that's related to this commit in some way.
repeated RevisionReference references = 6;
optional SourceIntegrityCheckResult sourceIntegrityResult = 7;
}
// RevisionReference contains a reference to a some information that is related in some way to another commit. For now,
@ -2613,6 +2621,60 @@ message SourceHydratorStatus {
optional HydrateOperation currentOperation = 2;
}
message SourceIntegrity {
// Git - policies for git source verification
optional SourceIntegrityGit git = 1;
}
// SourceIntegrityCheckResult represents a conclusion of the SourceIntegrity evaluation.
// Each check performed on a source(es), holds a check item representing all checks performed.
message SourceIntegrityCheckResult {
// Checks holds a list of checks performed, with their eventual problems. If a check is not specified here,
// it means it was not performed.
repeated SourceIntegrityCheckResultItem checks = 1;
}
message SourceIntegrityCheckResultItem {
// Name of the check that is human-understandable pointing out to the kind of verification performed.
optional string name = 1;
// Problems is a list of messages explaining why the check failed. Empty list means the check has succeeded.
repeated string problems = 2;
}
message SourceIntegrityGit {
repeated SourceIntegrityGitPolicy policies = 1;
}
message SourceIntegrityGitPolicy {
// List of repository criteria restricting repositories the policy will apply to
repeated SourceIntegrityGitPolicyRepo repos = 1;
// Verify GPG commit/tag signatures
optional SourceIntegrityGitPolicyGPG gpg = 2;
}
// SourceIntegrityGitPolicyGPG verifies that the commit(s) are both correctly signed by a key in the repo-server keyring,
// and that they are signed by one of the key listed in Keys.
//
// This policy can be deactivated through the ARGOCD_GPG_ENABLED environment variable.
//
// Note the listing of problematic commits/signatures reported when "strict" mode validation fails may not be complete.
// This means that a user that has addressed all problems reported by source integrity check can run into
// further problematic signatures on a subsequent attempt. That happens namely when history contains seal commits signed
// with gpg keys that are in the keyring, but not listed in Keys.
message SourceIntegrityGitPolicyGPG {
optional string mode = 1;
// List of key IDs to trust. The keys need to be in the repository server keyring.
repeated string keys = 3;
}
message SourceIntegrityGitPolicyRepo {
// URL specifier, glob.
optional string url = 1;
}
// SuccessfulHydrateOperation contains information about the most recent successful hydrate operation
message SuccessfulHydrateOperation {
// DrySHA holds the resolved revision (sha) of the dry source as of the most recent reconciliation

View file

@ -0,0 +1,119 @@
package v1alpha1
import (
"errors"
"fmt"
)
type SourceIntegrity struct {
// Git - policies for git source verification
Git *SourceIntegrityGit `json:"git" protobuf:"bytes,1,name=git"` // A mandatory field until there are alternatives
}
type SourceIntegrityGit struct {
Policies []*SourceIntegrityGitPolicy `json:"policies" protobuf:"bytes,1,name=policies"`
}
type SourceIntegrityGitPolicy struct {
// List of repository criteria restricting repositories the policy will apply to
Repos []SourceIntegrityGitPolicyRepo `json:"repos" protobuf:"bytes,1,name=repos"`
// Verify GPG commit/tag signatures
GPG *SourceIntegrityGitPolicyGPG `json:"gpg" protobuf:"bytes,2,name=gpg"` // A mandatory field until there are alternatives
}
type SourceIntegrityGitPolicyRepo struct {
// URL specifier, glob.
URL string `json:"url" protobuf:"bytes,1,name=url"`
}
type SourceIntegrityGitPolicyGPGMode string
var (
// SourceIntegrityGitPolicyGPGModeNone performs no verification at all. This is useful for troubleshooting.
SourceIntegrityGitPolicyGPGModeNone SourceIntegrityGitPolicyGPGMode = "none"
// SourceIntegrityGitPolicyGPGModeHead verifies the current target revision, an annotated tag, or a commit.
SourceIntegrityGitPolicyGPGModeHead SourceIntegrityGitPolicyGPGMode = "head"
// SourceIntegrityGitPolicyGPGModeStrict verifies all ancestry of target revision all the way to git init or seal commits.
// If pointing to an annotated tag, it verifies both the tag signature and the commit history.
SourceIntegrityGitPolicyGPGModeStrict SourceIntegrityGitPolicyGPGMode = "strict"
)
// SourceIntegrityGitPolicyGPG verifies that the commit(s) are both correctly signed by a key in the repo-server keyring,
// and that they are signed by one of the key listed in Keys.
//
// This policy can be deactivated through the ARGOCD_GPG_ENABLED environment variable.
//
// Note the listing of problematic commits/signatures reported when "strict" mode validation fails may not be complete.
// This means that a user that has addressed all problems reported by source integrity check can run into
// further problematic signatures on a subsequent attempt. That happens namely when history contains seal commits signed
// with gpg keys that are in the keyring, but not listed in Keys.
type SourceIntegrityGitPolicyGPG struct {
Mode SourceIntegrityGitPolicyGPGMode `json:"mode" protobuf:"bytes,1,name=mode"`
// List of key IDs to trust. The keys need to be in the repository server keyring.
Keys []string `json:"keys" protobuf:"bytes,3,name=keys"`
}
// SourceIntegrityCheckResult represents a conclusion of the SourceIntegrity evaluation.
// Each check performed on a source(es), holds a check item representing all checks performed.
type SourceIntegrityCheckResult struct {
// Checks holds a list of checks performed, with their eventual problems. If a check is not specified here,
// it means it was not performed.
Checks []SourceIntegrityCheckResultItem `protobuf:"bytes,1,opt,name=checks"`
}
type SourceIntegrityCheckResultItem struct {
// Name of the check that is human-understandable pointing out to the kind of verification performed.
Name string `protobuf:"bytes,1,name=name"`
// Problems is a list of messages explaining why the check failed. Empty list means the check has succeeded.
Problems []string `protobuf:"bytes,2,name=problems"`
}
func (r *SourceIntegrityCheckResult) PassedChecks() (names []string) {
names = []string{}
for _, item := range r.Checks {
if len(item.Problems) == 0 {
names = append(names, item.Name)
}
}
return names
}
func (r *SourceIntegrityCheckResult) AsError() error {
if r == nil {
return nil
}
var errs []error
for _, check := range r.Checks {
for _, p := range check.Problems {
errs = append(errs, fmt.Errorf("%s: %s", check.Name, p))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// IsValid reports if some of the performed checks had any problem
func (r *SourceIntegrityCheckResult) IsValid() bool {
for _, item := range r.Checks {
if len(item.Problems) > 0 {
return false
}
}
return true
}
// InjectSourceName updates the names of the checks with a new prefix. This is to distinguish results reported when
// checking multiple sources.
func (r *SourceIntegrityCheckResult) InjectSourceName(sourceName string) {
if r == nil {
return
}
for chi, check := range r.Checks {
for pi, problem := range check.Problems {
r.Checks[chi].Problems[pi] = fmt.Sprintf("%s: %s", sourceName, problem)
}
}
}

View file

@ -1693,9 +1693,12 @@ type RevisionMetadata struct {
// Message contains the message associated with the revision, most likely the commit message.
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`
// SignatureInfo contains a hint on the signer if the revision was signed with GPG, and signature verification is enabled.
//
// Deprecated: Use SourceIntegrityResult for more detailed information. SignatureInfo will be removed with the next major version.
SignatureInfo string `json:"signatureInfo,omitempty" protobuf:"bytes,5,opt,name=signatureInfo"`
// References contains references to information that's related to this commit in some way.
References []RevisionReference `json:"references,omitempty" protobuf:"bytes,6,opt,name=references"`
References []RevisionReference `json:"references,omitempty" protobuf:"bytes,6,opt,name=references"`
SourceIntegrityResult *SourceIntegrityCheckResult `json:"sourceIntegrityResult,omitempty" protobuf:"bytes,7,opt,name=sourceIntegrityResult"`
}
// OCIMetadata contains metadata for a specific revision in an OCI repository
@ -2801,6 +2804,50 @@ type AppProjectSpec struct {
PermitOnlyProjectScopedClusters bool `json:"permitOnlyProjectScopedClusters,omitempty" protobuf:"bytes,13,opt,name=permitOnlyProjectScopedClusters"`
// DestinationServiceAccounts holds information about the service accounts to be impersonated for the application sync operation for each destination.
DestinationServiceAccounts []ApplicationDestinationServiceAccount `json:"destinationServiceAccounts,omitempty" protobuf:"bytes,14,name=destinationServiceAccounts"`
// SourceIntegrity represents a constraint on manifest sources integrity to be met before they can be used.
// Do not access directly, use EffectiveSourceIntegrity() for correct backwards compatibility handling.
SourceIntegrity *SourceIntegrity `json:"sourceIntegrity,omitempty" protobuf:"bytes,15,name=sourceIntegrity"`
}
// EffectiveSourceIntegrity incorporates the legacy SignatureKeys into SourceIntegrity, if possible
// SignatureKeys are added as a Git GPG policy for repos specified with `*`. If such a policy exists, the SignatureKeys
// are ignored with warning.
func (proj *AppProject) EffectiveSourceIntegrity() *SourceIntegrity {
var legacyKeys []string
for _, k := range proj.Spec.SignatureKeys {
legacyKeys = append(legacyKeys, k.KeyID)
}
if len(legacyKeys) == 0 {
// Already using the modern version
return proj.Spec.SourceIntegrity
}
migratedGit := &SourceIntegrityGit{
Policies: []*SourceIntegrityGitPolicy{{
Repos: []SourceIntegrityGitPolicyRepo{{URL: "*"}},
GPG: &SourceIntegrityGitPolicyGPG{
Mode: SourceIntegrityGitPolicyGPGModeHead,
Keys: legacyKeys,
},
}},
}
if proj.Spec.SourceIntegrity == nil {
log.Warnf("Creating project SourceIntegrity from legacy SignatureKeys specified in %s AppProject. Migrate them to SourceIntegrity.", proj.Name)
return &SourceIntegrity{Git: migratedGit}
}
if proj.Spec.SourceIntegrity.Git != nil {
log.Errorf("Both SourceIntegrity and SignatureKeys specified in %s AppProject. Ignoring SignatureKeys. Migrate them to SourceIntegrity.", proj.Name)
return proj.Spec.SourceIntegrity
}
log.Warnf("Merging SourceIntegrity with legacy SignatureKeys specified in %s AppProject. Migrate them to SourceIntegrity.", proj.Name)
// Merge with non-git checks without modifying the AppProject - use deep-copy and amend
deepCopy := proj.Spec.SourceIntegrity.DeepCopy()
deepCopy.Git = migratedGit
return deepCopy
}
// ClusterResourceRestrictionItem is a cluster resource that is restricted by the project's whitelist or blacklist

View file

@ -2374,6 +2374,65 @@ func TestAppProjectSpec_AddWindow(t *testing.T) {
}
}
func TestAppProject_EffectiveSourceIntegrity(t *testing.T) {
sourceIntegrity := func(repo string, mode SourceIntegrityGitPolicyGPGMode, keys ...string) *SourceIntegrity {
return &SourceIntegrity{
Git: &SourceIntegrityGit{
Policies: []*SourceIntegrityGitPolicy{{
Repos: []SourceIntegrityGitPolicyRepo{{URL: repo}},
GPG: &SourceIntegrityGitPolicyGPG{
Mode: mode,
Keys: keys,
},
}},
},
}
}
tests := []struct {
name string
spec AppProjectSpec
expected *SourceIntegrity
}{
{
name: "no old, no new",
spec: AppProjectSpec{},
expected: nil,
}, {
name: "no old, new unchanged",
spec: AppProjectSpec{
SourceIntegrity: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "FOO"),
},
expected: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "FOO"),
}, {
name: "old, no new",
spec: AppProjectSpec{
SignatureKeys: []SignatureKey{{"LEGACY"}, {"KEYS"}, {"FOUND"}},
},
expected: sourceIntegrity("*", SourceIntegrityGitPolicyGPGModeHead, "LEGACY", "KEYS", "FOUND"),
}, {
name: "old ignored, replaced",
spec: AppProjectSpec{
SignatureKeys: []SignatureKey{{"LEGACY"}, {"KEYS"}, {"FOUND"}},
SourceIntegrity: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "NEW_KEY"),
},
expected: sourceIntegrity("https://github.com/*", SourceIntegrityGitPolicyGPGModeStrict, "NEW_KEY"),
}, {
name: "old, not using Git checks",
spec: AppProjectSpec{
SignatureKeys: []SignatureKey{{KeyID: "LEGACY_KEY"}},
SourceIntegrity: &SourceIntegrity{}, // Once something non-git is supported, needs checking they merge
},
expected: sourceIntegrity("*", SourceIntegrityGitPolicyGPGModeHead, "LEGACY_KEY"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
appProj := &AppProject{Spec: tt.spec, ObjectMeta: metav1.ObjectMeta{Name: "sut"}}
assert.Equal(t, tt.expected, appProj.EffectiveSourceIntegrity())
})
}
}
func TestAppProjectSpecWindowWithDescription(t *testing.T) {
proj := newTestProjectWithSyncWindows()
require.NoError(t, proj.Spec.AddWindow("allow", "* * * * *", "1h", []string{"app1"}, []string{}, []string{}, false, "error", false, "Ticket AAAAA", false))

View file

@ -194,6 +194,11 @@ func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
*out = make([]ApplicationDestinationServiceAccount, len(*in))
copy(*out, *in)
}
if in.SourceIntegrity != nil {
in, out := &in.SourceIntegrity, &out.SourceIntegrity
*out = new(SourceIntegrity)
(*in).DeepCopyInto(*out)
}
return
}
@ -4074,6 +4079,11 @@ func (in *RevisionMetadata) DeepCopyInto(out *RevisionMetadata) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.SourceIntegrityResult != nil {
in, out := &in.SourceIntegrityResult, &out.SourceIntegrityResult
*out = new(SourceIntegrityCheckResult)
(*in).DeepCopyInto(*out)
}
return
}
@ -4474,6 +4484,161 @@ func (in *SourceHydratorStatus) DeepCopy() *SourceHydratorStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrity) DeepCopyInto(out *SourceIntegrity) {
*out = *in
if in.Git != nil {
in, out := &in.Git, &out.Git
*out = new(SourceIntegrityGit)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrity.
func (in *SourceIntegrity) DeepCopy() *SourceIntegrity {
if in == nil {
return nil
}
out := new(SourceIntegrity)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrityCheckResult) DeepCopyInto(out *SourceIntegrityCheckResult) {
*out = *in
if in.Checks != nil {
in, out := &in.Checks, &out.Checks
*out = make([]SourceIntegrityCheckResultItem, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityCheckResult.
func (in *SourceIntegrityCheckResult) DeepCopy() *SourceIntegrityCheckResult {
if in == nil {
return nil
}
out := new(SourceIntegrityCheckResult)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrityCheckResultItem) DeepCopyInto(out *SourceIntegrityCheckResultItem) {
*out = *in
if in.Problems != nil {
in, out := &in.Problems, &out.Problems
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityCheckResultItem.
func (in *SourceIntegrityCheckResultItem) DeepCopy() *SourceIntegrityCheckResultItem {
if in == nil {
return nil
}
out := new(SourceIntegrityCheckResultItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrityGit) DeepCopyInto(out *SourceIntegrityGit) {
*out = *in
if in.Policies != nil {
in, out := &in.Policies, &out.Policies
*out = make([]*SourceIntegrityGitPolicy, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(SourceIntegrityGitPolicy)
(*in).DeepCopyInto(*out)
}
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGit.
func (in *SourceIntegrityGit) DeepCopy() *SourceIntegrityGit {
if in == nil {
return nil
}
out := new(SourceIntegrityGit)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrityGitPolicy) DeepCopyInto(out *SourceIntegrityGitPolicy) {
*out = *in
if in.Repos != nil {
in, out := &in.Repos, &out.Repos
*out = make([]SourceIntegrityGitPolicyRepo, len(*in))
copy(*out, *in)
}
if in.GPG != nil {
in, out := &in.GPG, &out.GPG
*out = new(SourceIntegrityGitPolicyGPG)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGitPolicy.
func (in *SourceIntegrityGitPolicy) DeepCopy() *SourceIntegrityGitPolicy {
if in == nil {
return nil
}
out := new(SourceIntegrityGitPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrityGitPolicyGPG) DeepCopyInto(out *SourceIntegrityGitPolicyGPG) {
*out = *in
if in.Keys != nil {
in, out := &in.Keys, &out.Keys
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGitPolicyGPG.
func (in *SourceIntegrityGitPolicyGPG) DeepCopy() *SourceIntegrityGitPolicyGPG {
if in == nil {
return nil
}
out := new(SourceIntegrityGitPolicyGPG)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceIntegrityGitPolicyRepo) DeepCopyInto(out *SourceIntegrityGitPolicyRepo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceIntegrityGitPolicyRepo.
func (in *SourceIntegrityGitPolicyRepo) DeepCopy() *SourceIntegrityGitPolicyRepo {
if in == nil {
return nil
}
out := new(SourceIntegrityGitPolicyRepo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SuccessfulHydrateOperation) DeepCopyInto(out *SuccessfulHydrateOperation) {
*out = *in

View file

@ -47,8 +47,8 @@ type ManifestRequest struct {
KubeVersion string `protobuf:"bytes,14,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"`
// ApiVersions is the list of API versions from the destination cluster, used for rendering Helm charts.
ApiVersions []string `protobuf:"bytes,15,rep,name=apiVersions,proto3" json:"apiVersions,omitempty"`
// Request to verify the signature when generating the manifests (only for Git repositories)
VerifySignature bool `protobuf:"varint,16,opt,name=verifySignature,proto3" json:"verifySignature,omitempty"`
// Source integrity constrains to verify the sources before use
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,16,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
HelmRepoCreds []*v1alpha1.RepoCreds `protobuf:"bytes,17,rep,name=helmRepoCreds,proto3" json:"helmRepoCreds,omitempty"`
NoRevisionCache bool `protobuf:"varint,18,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
TrackingMethod string `protobuf:"bytes,19,opt,name=trackingMethod,proto3" json:"trackingMethod,omitempty"`
@ -186,11 +186,11 @@ func (m *ManifestRequest) GetApiVersions() []string {
return nil
}
func (m *ManifestRequest) GetVerifySignature() bool {
func (m *ManifestRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
if m != nil {
return m.VerifySignature
return m.SourceIntegrity
}
return false
return nil
}
func (m *ManifestRequest) GetHelmRepoCreds() []*v1alpha1.RepoCreds {
@ -717,12 +717,14 @@ type ManifestResponse struct {
Revision string `protobuf:"bytes,4,opt,name=revision,proto3" json:"revision,omitempty"`
SourceType string `protobuf:"bytes,6,opt,name=sourceType,proto3" json:"sourceType,omitempty"`
// Raw response of git verify-commit operation (always the empty string for Helm)
// Deprecated: Use SourceIntegrityResult for more detailed information. VerifyResult will be removed with the next major version.
VerifyResult string `protobuf:"bytes,7,opt,name=verifyResult,proto3" json:"verifyResult,omitempty"`
// Commands is the list of commands used to hydrate the manifests
Commands []string `protobuf:"bytes,8,rep,name=commands,proto3" json:"commands,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
Commands []string `protobuf:"bytes,8,rep,name=commands,proto3" json:"commands,omitempty"`
SourceIntegrityResult *v1alpha1.SourceIntegrityCheckResult `protobuf:"bytes,9,opt,name=sourceIntegrityResult,proto3" json:"sourceIntegrityResult,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ManifestResponse) Reset() { *m = ManifestResponse{} }
@ -807,6 +809,13 @@ func (m *ManifestResponse) GetCommands() []string {
return nil
}
func (m *ManifestResponse) GetSourceIntegrityResult() *v1alpha1.SourceIntegrityCheckResult {
if m != nil {
return m.SourceIntegrityResult
}
return nil
}
type ListRefsRequest struct {
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
@ -1329,12 +1338,11 @@ type RepoServerRevisionMetadataRequest struct {
// the repo
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
// the revision within the repo
Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"`
// whether to check signature on revision
CheckSignature bool `protobuf:"varint,3,opt,name=checkSignature,proto3" json:"checkSignature,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"`
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,3,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *RepoServerRevisionMetadataRequest) Reset() { *m = RepoServerRevisionMetadataRequest{} }
@ -1384,11 +1392,11 @@ func (m *RepoServerRevisionMetadataRequest) GetRevision() string {
return ""
}
func (m *RepoServerRevisionMetadataRequest) GetCheckSignature() bool {
func (m *RepoServerRevisionMetadataRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
if m != nil {
return m.CheckSignature
return m.SourceIntegrity
}
return false
return nil
}
type RepoServerRevisionChartDetailsRequest struct {
@ -1950,16 +1958,16 @@ func (m *HelmChartsResponse) GetItems() []*HelmChart {
}
type GitFilesRequest struct {
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
NewGitFileGlobbingEnabled bool `protobuf:"varint,5,opt,name=NewGitFileGlobbingEnabled,proto3" json:"NewGitFileGlobbingEnabled,omitempty"`
NoRevisionCache bool `protobuf:"varint,6,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
VerifyCommit bool `protobuf:"varint,7,opt,name=verifyCommit,proto3" json:"verifyCommit,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
NewGitFileGlobbingEnabled bool `protobuf:"varint,5,opt,name=NewGitFileGlobbingEnabled,proto3" json:"NewGitFileGlobbingEnabled,omitempty"`
NoRevisionCache bool `protobuf:"varint,6,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,7,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *GitFilesRequest) Reset() { *m = GitFilesRequest{} }
@ -2037,11 +2045,11 @@ func (m *GitFilesRequest) GetNoRevisionCache() bool {
return false
}
func (m *GitFilesRequest) GetVerifyCommit() bool {
func (m *GitFilesRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
if m != nil {
return m.VerifyCommit
return m.SourceIntegrity
}
return false
return nil
}
type GitFilesResponse struct {
@ -2093,14 +2101,14 @@ func (m *GitFilesResponse) GetMap() map[string][]byte {
}
type GitDirectoriesRequest struct {
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
NoRevisionCache bool `protobuf:"varint,4,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
VerifyCommit bool `protobuf:"varint,5,opt,name=verifyCommit,proto3" json:"verifyCommit,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"`
Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"`
NoRevisionCache bool `protobuf:"varint,4,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"`
SourceIntegrity *v1alpha1.SourceIntegrity `protobuf:"bytes,5,opt,name=sourceIntegrity,proto3" json:"sourceIntegrity,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *GitDirectoriesRequest) Reset() { *m = GitDirectoriesRequest{} }
@ -2164,11 +2172,11 @@ func (m *GitDirectoriesRequest) GetNoRevisionCache() bool {
return false
}
func (m *GitDirectoriesRequest) GetVerifyCommit() bool {
func (m *GitDirectoriesRequest) GetSourceIntegrity() *v1alpha1.SourceIntegrity {
if m != nil {
return m.VerifyCommit
return m.SourceIntegrity
}
return false
return nil
}
type GitDirectoriesResponse struct {
@ -2495,160 +2503,162 @@ func init() {
}
var fileDescriptor_dd8723cfcc820480 = []byte{
// 2447 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x1a, 0x4d, 0x6f, 0x1c, 0x49,
0xd5, 0xf3, 0x65, 0xcf, 0x3c, 0x7f, 0x8d, 0x2b, 0xb1, 0xd3, 0x99, 0x24, 0xc6, 0xdb, 0x90, 0x28,
0x9b, 0xec, 0x8e, 0x95, 0x44, 0xbb, 0x81, 0xec, 0xb2, 0x2b, 0xaf, 0x93, 0xd8, 0xde, 0xc4, 0x89,
0xe9, 0x64, 0x17, 0x05, 0x02, 0xa8, 0xa6, 0xa7, 0x3c, 0xd3, 0x3b, 0xfd, 0x51, 0xe9, 0xae, 0xf6,
0xe2, 0x48, 0x9c, 0x40, 0x5c, 0xe0, 0xc0, 0x69, 0x0f, 0xfc, 0x0f, 0xc4, 0x91, 0x13, 0x82, 0x0b,
0x12, 0xe2, 0xc2, 0x05, 0x09, 0x94, 0x1f, 0x82, 0x50, 0x7d, 0xf4, 0xe7, 0xf4, 0x8c, 0xbd, 0x99,
0xc4, 0x01, 0x2e, 0x76, 0x57, 0xd5, 0xab, 0xf7, 0x5e, 0xbd, 0xaf, 0x7a, 0xef, 0xd5, 0xc0, 0x25,
0x9f, 0x50, 0x2f, 0x20, 0xfe, 0x01, 0xf1, 0xd7, 0xc5, 0xa7, 0xc5, 0x3c, 0xff, 0x30, 0xf5, 0xd9,
0xa6, 0xbe, 0xc7, 0x3c, 0x04, 0xc9, 0x4c, 0xeb, 0x7e, 0xcf, 0x62, 0xfd, 0xb0, 0xd3, 0x36, 0x3d,
0x67, 0x1d, 0xfb, 0x3d, 0x8f, 0xfa, 0xde, 0x17, 0xe2, 0xe3, 0x5d, 0xb3, 0xbb, 0x7e, 0x70, 0x63,
0x9d, 0x0e, 0x7a, 0xeb, 0x98, 0x5a, 0xc1, 0x3a, 0xa6, 0xd4, 0xb6, 0x4c, 0xcc, 0x2c, 0xcf, 0x5d,
0x3f, 0xb8, 0x86, 0x6d, 0xda, 0xc7, 0xd7, 0xd6, 0x7b, 0xc4, 0x25, 0x3e, 0x66, 0xa4, 0x2b, 0x31,
0xb7, 0xce, 0xf5, 0x3c, 0xaf, 0x67, 0x93, 0x75, 0x31, 0xea, 0x84, 0xfb, 0xeb, 0xc4, 0xa1, 0x4c,
0x91, 0xd5, 0xff, 0x31, 0x0f, 0x8b, 0xbb, 0xd8, 0xb5, 0xf6, 0x49, 0xc0, 0x0c, 0xf2, 0x2c, 0x24,
0x01, 0x43, 0x4f, 0xa1, 0xca, 0x99, 0xd1, 0x4a, 0x6b, 0xa5, 0xcb, 0xb3, 0xd7, 0xb7, 0xdb, 0x09,
0x37, 0xed, 0x88, 0x1b, 0xf1, 0xf1, 0x13, 0xb3, 0xdb, 0x3e, 0xb8, 0xd1, 0xa6, 0x83, 0x5e, 0x9b,
0x73, 0xd3, 0x4e, 0x71, 0xd3, 0x8e, 0xb8, 0x69, 0x1b, 0xf1, 0xb1, 0x0c, 0x81, 0x15, 0xb5, 0xa0,
0xee, 0x93, 0x03, 0x2b, 0xb0, 0x3c, 0x57, 0x2b, 0xaf, 0x95, 0x2e, 0x37, 0x8c, 0x78, 0x8c, 0x34,
0x98, 0x71, 0xbd, 0x4d, 0x6c, 0xf6, 0x89, 0x56, 0x59, 0x2b, 0x5d, 0xae, 0x1b, 0xd1, 0x10, 0xad,
0xc1, 0x2c, 0xa6, 0xf4, 0x3e, 0xee, 0x10, 0xfb, 0x1e, 0x39, 0xd4, 0xaa, 0x62, 0x63, 0x7a, 0x8a,
0xef, 0xc5, 0x94, 0x3e, 0xc0, 0x0e, 0xd1, 0x6a, 0x62, 0x35, 0x1a, 0xa2, 0xf3, 0xd0, 0x70, 0xb1,
0x43, 0x02, 0x8a, 0x4d, 0xa2, 0xd5, 0xc5, 0x5a, 0x32, 0x81, 0x7e, 0x06, 0x4b, 0x29, 0xc6, 0x1f,
0x79, 0xa1, 0x6f, 0x12, 0x0d, 0xc4, 0xd1, 0x1f, 0x4e, 0x76, 0xf4, 0x8d, 0x3c, 0x5a, 0x63, 0x98,
0x12, 0xfa, 0x31, 0xd4, 0x84, 0xe6, 0xb5, 0xd9, 0xb5, 0xca, 0x2b, 0x95, 0xb6, 0x44, 0x8b, 0x5c,
0x98, 0xa1, 0x76, 0xd8, 0xb3, 0xdc, 0x40, 0x9b, 0x13, 0x14, 0x1e, 0x4f, 0x46, 0x61, 0xd3, 0x73,
0xf7, 0xad, 0xde, 0x2e, 0x76, 0x71, 0x8f, 0x38, 0xc4, 0x65, 0x7b, 0x02, 0xb9, 0x11, 0x11, 0x41,
0xcf, 0xa1, 0x39, 0x08, 0x03, 0xe6, 0x39, 0xd6, 0x73, 0xf2, 0x90, 0xf2, 0xbd, 0x81, 0x36, 0x2f,
0xa4, 0xf9, 0x60, 0x32, 0xc2, 0xf7, 0x72, 0x58, 0x8d, 0x21, 0x3a, 0xdc, 0x48, 0x06, 0x61, 0x87,
0x7c, 0x4e, 0x7c, 0x61, 0x5d, 0x0b, 0xd2, 0x48, 0x52, 0x53, 0xd2, 0x8c, 0x2c, 0x35, 0x0a, 0xb4,
0xc5, 0xb5, 0x8a, 0x34, 0xa3, 0x78, 0x0a, 0x5d, 0x86, 0xc5, 0x03, 0xe2, 0x5b, 0xfb, 0x87, 0x8f,
0xac, 0x9e, 0x8b, 0x59, 0xe8, 0x13, 0xad, 0x29, 0x4c, 0x31, 0x3f, 0x8d, 0x1c, 0x98, 0xef, 0x13,
0xdb, 0xe1, 0x22, 0xdf, 0xf4, 0x49, 0x37, 0xd0, 0x96, 0x84, 0x7c, 0xb7, 0x26, 0xd7, 0xa0, 0x40,
0x67, 0x64, 0xb1, 0x73, 0xc6, 0x5c, 0xcf, 0x50, 0x9e, 0x22, 0x7d, 0x04, 0x49, 0xc6, 0x72, 0xd3,
0xe8, 0x12, 0x2c, 0x30, 0x1f, 0x9b, 0x03, 0xcb, 0xed, 0xed, 0x12, 0xd6, 0xf7, 0xba, 0xda, 0x29,
0x21, 0x89, 0xdc, 0x2c, 0x32, 0x01, 0x11, 0x17, 0x77, 0x6c, 0xd2, 0x95, 0xb6, 0xf8, 0xf8, 0x90,
0x92, 0x40, 0x3b, 0x2d, 0x4e, 0x71, 0xa3, 0x9d, 0x8a, 0x50, 0xb9, 0x00, 0xd1, 0xbe, 0x33, 0xb4,
0xeb, 0x8e, 0xcb, 0xfc, 0x43, 0xa3, 0x00, 0x1d, 0x1a, 0xc0, 0x2c, 0x3f, 0x47, 0x64, 0x0a, 0xcb,
0xc2, 0x14, 0x76, 0x26, 0x93, 0xd1, 0x76, 0x82, 0xd0, 0x48, 0x63, 0x47, 0x6d, 0x40, 0x7d, 0x1c,
0xec, 0x86, 0x36, 0xb3, 0xa8, 0x4d, 0x24, 0x1b, 0x81, 0xb6, 0x22, 0xc4, 0x54, 0xb0, 0x82, 0xee,
0x01, 0xf8, 0x64, 0x3f, 0x82, 0x3b, 0x23, 0x4e, 0x7e, 0x75, 0xdc, 0xc9, 0x8d, 0x18, 0x5a, 0x9e,
0x38, 0xb5, 0x9d, 0x13, 0xe7, 0xc7, 0x20, 0x26, 0x53, 0xde, 0x2e, 0xdc, 0x5a, 0x13, 0x26, 0x56,
0xb0, 0xc2, 0x6d, 0x51, 0xcd, 0x8a, 0xa0, 0x75, 0x56, 0x5a, 0x6b, 0x6a, 0x0a, 0x6d, 0xc3, 0x37,
0xb0, 0xeb, 0x7a, 0x4c, 0x1c, 0x3f, 0x62, 0x65, 0x4b, 0x85, 0xf7, 0x3d, 0xcc, 0xfa, 0x81, 0xd6,
0x12, 0xbb, 0x8e, 0x02, 0xe3, 0x26, 0x61, 0xb9, 0x01, 0xc3, 0xb6, 0x2d, 0x80, 0x76, 0x6e, 0x6b,
0xe7, 0xa4, 0x49, 0x64, 0x67, 0x5b, 0x77, 0xe0, 0xcc, 0x08, 0xe5, 0xa2, 0x26, 0x54, 0x06, 0xe4,
0x50, 0x5c, 0x0a, 0x0d, 0x83, 0x7f, 0xa2, 0xd3, 0x50, 0x3b, 0xc0, 0x76, 0x48, 0x44, 0x18, 0xaf,
0x1b, 0x72, 0x70, 0xab, 0xfc, 0xed, 0x52, 0xeb, 0x97, 0x25, 0x58, 0xcc, 0x89, 0xaa, 0x60, 0xff,
0x8f, 0xd2, 0xfb, 0x5f, 0x81, 0xe3, 0xec, 0x3f, 0xc6, 0x7e, 0x8f, 0xb0, 0x14, 0x23, 0xfa, 0xdf,
0x4a, 0xa0, 0xe5, 0x74, 0xf8, 0x7d, 0x8b, 0xf5, 0xef, 0x5a, 0x36, 0x09, 0xd0, 0x4d, 0x98, 0xf1,
0xe5, 0x9c, 0xba, 0xea, 0xce, 0x8d, 0x51, 0xfd, 0xf6, 0x94, 0x11, 0x41, 0xa3, 0x8f, 0xa0, 0xee,
0x10, 0x86, 0xbb, 0x98, 0x61, 0xc5, 0xfb, 0x5a, 0xd1, 0x4e, 0x4e, 0x65, 0x57, 0xc1, 0x6d, 0x4f,
0x19, 0xf1, 0x1e, 0xf4, 0x1e, 0xd4, 0xcc, 0x7e, 0xe8, 0x0e, 0xc4, 0x25, 0x37, 0x7b, 0xfd, 0xc2,
0xa8, 0xcd, 0x9b, 0x1c, 0x68, 0x7b, 0xca, 0x90, 0xd0, 0x9f, 0x4c, 0x43, 0x95, 0x62, 0x9f, 0xe9,
0x77, 0xe1, 0x74, 0x11, 0x09, 0x7e, 0xb3, 0x9a, 0x7d, 0x62, 0x0e, 0x82, 0xd0, 0x51, 0x62, 0x8e,
0xc7, 0x08, 0x41, 0x35, 0xb0, 0x9e, 0x4b, 0x51, 0x57, 0x0c, 0xf1, 0xad, 0xbf, 0x0d, 0x4b, 0x43,
0xd4, 0xb8, 0x52, 0x25, 0x6f, 0x1c, 0xc3, 0x9c, 0x22, 0xad, 0x87, 0xb0, 0xfc, 0x58, 0xc8, 0x22,
0xbe, 0x5e, 0x4e, 0x22, 0x57, 0xd0, 0xb7, 0x61, 0x25, 0x4f, 0x36, 0xa0, 0x9e, 0x1b, 0x10, 0xee,
0x6c, 0x22, 0x1e, 0x5b, 0xa4, 0x9b, 0xac, 0x0a, 0x2e, 0xea, 0x46, 0xc1, 0x8a, 0xfe, 0x97, 0x32,
0xac, 0x18, 0x24, 0xf0, 0xec, 0x03, 0x12, 0x05, 0xcb, 0x93, 0x49, 0x77, 0x7e, 0x08, 0x15, 0x4c,
0xa9, 0x32, 0x93, 0x9d, 0x57, 0x96, 0x50, 0x18, 0x1c, 0x2b, 0x7a, 0x07, 0x96, 0xb0, 0xd3, 0xb1,
0x7a, 0xa1, 0x17, 0x06, 0xd1, 0xb1, 0x84, 0x51, 0x35, 0x8c, 0xe1, 0x05, 0x1e, 0x70, 0x02, 0xe1,
0x91, 0x3b, 0x6e, 0x97, 0xfc, 0x54, 0xe4, 0x50, 0x15, 0x23, 0x3d, 0x55, 0x74, 0xc7, 0xd4, 0x0a,
0xef, 0x18, 0xdd, 0x84, 0x33, 0x43, 0xe2, 0x54, 0xaa, 0x49, 0x27, 0x78, 0xa5, 0x5c, 0x82, 0x57,
0xc8, 0x70, 0x79, 0x04, 0xc3, 0xfa, 0x8b, 0x12, 0x34, 0x13, 0x37, 0x54, 0xe8, 0xcf, 0x43, 0xc3,
0x51, 0x73, 0x81, 0x56, 0x12, 0xd1, 0x35, 0x99, 0xc8, 0xe6, 0x7a, 0xe5, 0x7c, 0xae, 0xb7, 0x02,
0xd3, 0x32, 0x15, 0x57, 0x42, 0x52, 0xa3, 0x0c, 0xcb, 0xd5, 0x1c, 0xcb, 0xab, 0x00, 0x41, 0x1c,
0x0b, 0xb5, 0x69, 0xb1, 0x9a, 0x9a, 0x41, 0x3a, 0xcc, 0xc9, 0xcc, 0xc0, 0x20, 0x41, 0x68, 0x33,
0x6d, 0x46, 0x40, 0x64, 0xe6, 0x84, 0x67, 0x7a, 0x8e, 0x83, 0xdd, 0x6e, 0xa0, 0xd5, 0x05, 0xcb,
0xf1, 0x58, 0xf7, 0x60, 0xf1, 0xbe, 0xc5, 0xcf, 0xb7, 0x1f, 0x9c, 0x8c, 0x53, 0xbd, 0x0f, 0x55,
0x4e, 0x8c, 0x33, 0xd5, 0xf1, 0xb1, 0x6b, 0xf6, 0x49, 0x24, 0xc7, 0x78, 0xcc, 0xc3, 0x05, 0xc3,
0xbd, 0x40, 0x2b, 0x8b, 0x79, 0xf1, 0xad, 0xff, 0xbe, 0x2c, 0x39, 0xdd, 0xa0, 0x34, 0x78, 0xf3,
0xa5, 0x42, 0x71, 0xf2, 0x52, 0x19, 0x4e, 0x5e, 0x72, 0x2c, 0x7f, 0x9d, 0xe4, 0xe5, 0x15, 0x5d,
0x87, 0x7a, 0x08, 0x33, 0x1b, 0x94, 0x72, 0x46, 0xd0, 0x35, 0xa8, 0x62, 0x4a, 0xa5, 0xc0, 0x73,
0x91, 0x5f, 0x81, 0xf0, 0xff, 0x8a, 0x25, 0x01, 0xda, 0xba, 0x09, 0x8d, 0x78, 0xea, 0x28, 0xb2,
0x8d, 0x34, 0xd9, 0x35, 0x00, 0x99, 0x9d, 0xef, 0xb8, 0xfb, 0x1e, 0x57, 0x29, 0x77, 0x04, 0xb5,
0x55, 0x7c, 0xeb, 0xb7, 0x22, 0x08, 0xc1, 0xdb, 0x3b, 0x50, 0xb3, 0x18, 0x71, 0x22, 0xe6, 0x56,
0xd2, 0xcc, 0x25, 0x88, 0x0c, 0x09, 0xa4, 0xff, 0xa9, 0x0e, 0x67, 0xb9, 0xc6, 0x1e, 0x09, 0x17,
0xda, 0xa0, 0xf4, 0x36, 0x61, 0xd8, 0xb2, 0x83, 0xef, 0x85, 0xc4, 0x3f, 0x7c, 0xcd, 0x86, 0xd1,
0x83, 0x69, 0xe9, 0x81, 0x2a, 0xae, 0xbe, 0xf2, 0x42, 0x4d, 0xa1, 0x4f, 0xaa, 0xb3, 0xca, 0xeb,
0xa9, 0xce, 0x8a, 0xaa, 0xa5, 0xea, 0x09, 0x55, 0x4b, 0xa3, 0x0b, 0xe6, 0x54, 0x19, 0x3e, 0x9d,
0x2d, 0xc3, 0x0b, 0x2e, 0x88, 0x99, 0xe3, 0x16, 0x21, 0xf5, 0xc2, 0x22, 0xc4, 0x29, 0xf4, 0xe3,
0x86, 0x10, 0xf7, 0x77, 0xd3, 0x16, 0x38, 0xd2, 0xd6, 0x26, 0x29, 0x47, 0xe0, 0xb5, 0x96, 0x23,
0x9f, 0x65, 0xca, 0x0b, 0x59, 0xe0, 0xbf, 0x77, 0xbc, 0x33, 0x8d, 0x29, 0x34, 0xfe, 0xef, 0x92,
0xf4, 0x5f, 0x88, 0xdc, 0x8c, 0x7a, 0x89, 0x0c, 0xe2, 0xcb, 0x9e, 0xdf, 0x43, 0xfc, 0xda, 0x55,
0x41, 0x8b, 0x7f, 0xa3, 0xab, 0x50, 0xe5, 0x42, 0x56, 0xc9, 0xf3, 0x99, 0xb4, 0x3c, 0xb9, 0x26,
0x36, 0x28, 0x7d, 0x44, 0x89, 0x69, 0x08, 0x20, 0x74, 0x0b, 0x1a, 0xb1, 0xe1, 0x2b, 0xcf, 0x3a,
0x9f, 0xde, 0x11, 0xfb, 0x49, 0xb4, 0x2d, 0x01, 0xe7, 0x7b, 0xbb, 0x96, 0x4f, 0x4c, 0x91, 0x5a,
0xd6, 0x86, 0xf7, 0xde, 0x8e, 0x16, 0xe3, 0xbd, 0x31, 0x38, 0xba, 0x06, 0xd3, 0xb2, 0x23, 0x22,
0x3c, 0x68, 0xf6, 0xfa, 0xd9, 0xe1, 0x60, 0x1a, 0xed, 0x52, 0x80, 0xfa, 0x1f, 0x4b, 0xf0, 0x56,
0x62, 0x10, 0x91, 0x37, 0x45, 0xd9, 0xfd, 0x9b, 0xbf, 0x71, 0x2f, 0xc1, 0x82, 0x28, 0x27, 0x92,
0xc6, 0x88, 0xec, 0xd1, 0xe5, 0x66, 0xf5, 0xdf, 0x95, 0xe0, 0xe2, 0xf0, 0x39, 0x36, 0xfb, 0xd8,
0x67, 0xb1, 0x7a, 0x4f, 0xe2, 0x2c, 0xd1, 0x85, 0x57, 0x4e, 0x2e, 0xbc, 0xcc, 0xf9, 0x2a, 0xd9,
0xf3, 0xe9, 0x7f, 0x28, 0xc3, 0x6c, 0xca, 0x80, 0x8a, 0x2e, 0x4c, 0x9e, 0x0c, 0x0a, 0xbb, 0x15,
0x05, 0xa4, 0xb8, 0x14, 0x1a, 0x46, 0x6a, 0x06, 0x0d, 0x00, 0x28, 0xf6, 0xb1, 0x43, 0x18, 0xf1,
0x79, 0x24, 0xe7, 0x1e, 0x7f, 0x6f, 0xf2, 0xe8, 0xb2, 0x17, 0xe1, 0x34, 0x52, 0xe8, 0x79, 0x36,
0x2b, 0x48, 0x07, 0x2a, 0x7e, 0xab, 0x11, 0xfa, 0x12, 0x16, 0xf6, 0x2d, 0x9b, 0xec, 0x25, 0x8c,
0x4c, 0x0b, 0x46, 0x1e, 0x4e, 0xce, 0xc8, 0xdd, 0x34, 0x5e, 0x23, 0x47, 0x46, 0xbf, 0x02, 0xcd,
0xbc, 0x3f, 0x71, 0x26, 0x2d, 0x07, 0xf7, 0x62, 0x69, 0xa9, 0x91, 0x8e, 0xa0, 0x99, 0xf7, 0x1f,
0xfd, 0x9f, 0x65, 0x58, 0x8e, 0xd1, 0x6d, 0xb8, 0xae, 0x17, 0xba, 0xa6, 0x68, 0x32, 0x16, 0xea,
0xe2, 0x34, 0xd4, 0x98, 0xc5, 0xec, 0x38, 0xf1, 0x11, 0x03, 0x7e, 0x77, 0x31, 0xcf, 0xb3, 0x99,
0x45, 0x95, 0x82, 0xa3, 0xa1, 0xd4, 0xfd, 0xb3, 0xd0, 0xf2, 0x49, 0x57, 0x44, 0x82, 0xba, 0x11,
0x8f, 0xf9, 0x1a, 0xcf, 0x6a, 0x44, 0x8a, 0x2f, 0x85, 0x19, 0x8f, 0x85, 0xdd, 0x7b, 0xb6, 0x4d,
0x4c, 0x2e, 0x8e, 0x54, 0x11, 0x90, 0x9b, 0x15, 0xc5, 0x05, 0xf3, 0x2d, 0xb7, 0xa7, 0x4a, 0x00,
0x35, 0xe2, 0x7c, 0x62, 0xdf, 0xc7, 0x87, 0x2a, 0xf3, 0x97, 0x03, 0xf4, 0x21, 0x54, 0x1c, 0x4c,
0xd5, 0x45, 0x77, 0x25, 0x13, 0x1d, 0x8a, 0x24, 0xd0, 0xde, 0xc5, 0x54, 0xde, 0x04, 0x7c, 0x5b,
0xeb, 0x7d, 0xa8, 0x47, 0x13, 0x5f, 0x2b, 0x25, 0xfc, 0x02, 0xe6, 0x33, 0xc1, 0x07, 0x3d, 0x81,
0x95, 0xc4, 0xa2, 0xd2, 0x04, 0x55, 0x12, 0xf8, 0xd6, 0x91, 0x9c, 0x19, 0x23, 0x10, 0xe8, 0xcf,
0x60, 0x89, 0x9b, 0x8c, 0x70, 0xfc, 0x13, 0x2a, 0x6d, 0x3e, 0x80, 0x46, 0x4c, 0xb2, 0xd0, 0x66,
0x5a, 0x50, 0x3f, 0x88, 0x9a, 0xbf, 0xb2, 0xb6, 0x89, 0xc7, 0xfa, 0x06, 0xa0, 0x34, 0xbf, 0xea,
0x06, 0xba, 0x9a, 0x4d, 0x8a, 0x97, 0xf3, 0xd7, 0x8d, 0x00, 0x8f, 0x72, 0xe2, 0xbf, 0x97, 0x61,
0x71, 0xcb, 0x12, 0xdd, 0x94, 0x13, 0x0a, 0x72, 0x57, 0xa0, 0x19, 0x84, 0x1d, 0xc7, 0xeb, 0x86,
0x36, 0x51, 0x49, 0x81, 0xba, 0xe9, 0x87, 0xe6, 0xc7, 0x05, 0x3f, 0x2e, 0x2c, 0x8a, 0x59, 0x5f,
0x55, 0xbf, 0xe2, 0x1b, 0x7d, 0x08, 0x67, 0x1f, 0x90, 0x2f, 0xd5, 0x79, 0xb6, 0x6c, 0xaf, 0xd3,
0xb1, 0xdc, 0x5e, 0x44, 0x44, 0xf6, 0x05, 0x46, 0x03, 0x14, 0xa5, 0x8a, 0xd3, 0xc5, 0xa9, 0x62,
0x5c, 0x41, 0x6f, 0x7a, 0x8e, 0x63, 0x31, 0x95, 0x51, 0x66, 0xe6, 0xf4, 0x9f, 0x97, 0xa0, 0x99,
0x48, 0x56, 0xe9, 0xe6, 0xa6, 0xf4, 0x21, 0xa9, 0x99, 0x8b, 0x69, 0xcd, 0xe4, 0x41, 0x5f, 0xde,
0x7d, 0xe6, 0xd2, 0xee, 0xf3, 0xab, 0x32, 0x2c, 0x6f, 0x59, 0x2c, 0x0a, 0x5c, 0xd6, 0xff, 0x9a,
0x96, 0x0b, 0x74, 0x52, 0x3d, 0x9e, 0x4e, 0x6a, 0x05, 0x3a, 0x69, 0xc3, 0x4a, 0x5e, 0x18, 0x4a,
0x31, 0xa7, 0xa1, 0x46, 0x45, 0x7b, 0x5a, 0xf6, 0x15, 0xe4, 0x40, 0xff, 0x77, 0x1d, 0x2e, 0x7c,
0x46, 0xbb, 0x98, 0xc5, 0x3d, 0xa3, 0xbb, 0x9e, 0x2f, 0xfa, 0xd3, 0x27, 0x23, 0xc5, 0xdc, 0x1b,
0x62, 0x79, 0xec, 0x1b, 0x62, 0x65, 0xcc, 0x1b, 0x62, 0xf5, 0x58, 0x6f, 0x88, 0xb5, 0x13, 0x7b,
0x43, 0x1c, 0xae, 0xb5, 0xa6, 0x0b, 0x6b, 0xad, 0x27, 0x99, 0x7a, 0x64, 0x46, 0xb8, 0xcd, 0x77,
0xd2, 0x6e, 0x33, 0x56, 0x3b, 0x63, 0x1f, 0x3f, 0x72, 0x4f, 0x6f, 0xf5, 0x23, 0x9f, 0xde, 0x1a,
0xc3, 0x4f, 0x6f, 0xc5, 0xaf, 0x37, 0x30, 0xf2, 0xf5, 0xe6, 0x12, 0x2c, 0x04, 0x87, 0xae, 0x49,
0xba, 0x71, 0x27, 0x71, 0x56, 0x1e, 0x3b, 0x3b, 0x9b, 0xf1, 0x88, 0xb9, 0x9c, 0x47, 0xc4, 0x96,
0x3a, 0x9f, 0xb2, 0xd4, 0x22, 0x3f, 0x59, 0x18, 0x59, 0xe6, 0xe6, 0x1e, 0x56, 0x16, 0x8b, 0x1e,
0x56, 0xd0, 0x00, 0x9a, 0x11, 0x57, 0xb1, 0x02, 0x9a, 0x42, 0x01, 0x1f, 0x1f, 0x5f, 0x01, 0x8f,
0x72, 0x18, 0xa4, 0x1a, 0x86, 0x10, 0xff, 0xd7, 0x54, 0x76, 0xad, 0x5f, 0x97, 0x60, 0xb9, 0x90,
0xe9, 0x37, 0x53, 0x68, 0x7e, 0x0e, 0xab, 0xa3, 0x04, 0xac, 0x02, 0x97, 0x06, 0x33, 0x66, 0x1f,
0xbb, 0x3d, 0xd1, 0x12, 0x15, 0x9d, 0x0f, 0x35, 0x1c, 0x57, 0x19, 0x5d, 0xff, 0x6a, 0x0e, 0x96,
0x92, 0x8a, 0x87, 0xff, 0xb5, 0x4c, 0x82, 0x1e, 0x42, 0x33, 0x7a, 0x84, 0x8b, 0x9a, 0xd8, 0x68,
0xdc, 0x0b, 0x53, 0xeb, 0x7c, 0xf1, 0xa2, 0x64, 0x4d, 0x9f, 0x42, 0x26, 0x9c, 0xcd, 0x23, 0x4c,
0x1e, 0xb3, 0xbe, 0x35, 0x06, 0x73, 0x0c, 0x75, 0x14, 0x89, 0xcb, 0x25, 0xf4, 0x04, 0x16, 0xb2,
0x4f, 0x2e, 0x28, 0x93, 0x02, 0x16, 0xbe, 0x02, 0xb5, 0xf4, 0x71, 0x20, 0x31, 0xff, 0x4f, 0xb9,
0x55, 0x66, 0xde, 0x0c, 0x90, 0x9e, 0xed, 0x86, 0x14, 0xbd, 0xcf, 0xb4, 0xbe, 0x39, 0x16, 0x26,
0xc6, 0xfe, 0x01, 0xd4, 0xa3, 0x3e, 0x7a, 0x56, 0xcc, 0xb9, 0xee, 0x7a, 0xab, 0x99, 0xc5, 0xb7,
0x1f, 0xe8, 0x53, 0xe8, 0x23, 0x98, 0xe5, 0x60, 0x0f, 0x37, 0x77, 0x1e, 0xe3, 0xde, 0x4b, 0xed,
0xaf, 0x47, 0x7d, 0xe6, 0xe1, 0xcd, 0xa9, 0xee, 0x73, 0xeb, 0x54, 0x41, 0xc7, 0x57, 0x9f, 0x42,
0x1f, 0x4b, 0xfa, 0x7b, 0xea, 0x47, 0x14, 0x2b, 0x6d, 0xf9, 0x9b, 0x9d, 0x76, 0xf4, 0x9b, 0x9d,
0xf6, 0x1d, 0x87, 0xb2, 0xc3, 0x56, 0x41, 0x4b, 0x56, 0x21, 0x78, 0x0a, 0xf3, 0x5b, 0x84, 0x25,
0x1d, 0x14, 0x74, 0xf1, 0x58, 0x7d, 0xa6, 0x96, 0x9e, 0x07, 0x1b, 0x6e, 0xc2, 0xe8, 0x53, 0xe8,
0xab, 0x12, 0x9c, 0xda, 0x22, 0x2c, 0xdf, 0x93, 0x40, 0xef, 0x16, 0x13, 0x19, 0xd1, 0xbb, 0x68,
0x3d, 0x98, 0xd4, 0xa7, 0xb3, 0x68, 0xf5, 0x29, 0xf4, 0x9b, 0x12, 0x2c, 0x6c, 0x11, 0xae, 0xb7,
0x98, 0xa7, 0x6b, 0xe3, 0x79, 0x2a, 0xe8, 0x43, 0xb4, 0x26, 0xec, 0xff, 0xa5, 0xa8, 0xeb, 0x53,
0xe8, 0xb7, 0x25, 0x38, 0x93, 0x92, 0x55, 0x9a, 0xde, 0xcb, 0xf0, 0xf6, 0xe9, 0x84, 0x3f, 0xd7,
0x49, 0xa1, 0xd4, 0xa7, 0xd0, 0x9e, 0x30, 0x93, 0xa4, 0xcc, 0x41, 0x17, 0x0a, 0xeb, 0x99, 0x98,
0xfa, 0xea, 0xa8, 0xe5, 0xd8, 0x34, 0x3e, 0x85, 0xd9, 0x2d, 0xc2, 0xa2, 0x7c, 0x3b, 0x6b, 0xfc,
0xb9, 0x52, 0x28, 0x1b, 0x7d, 0xf2, 0x29, 0xba, 0x30, 0xe2, 0x25, 0x89, 0x2b, 0x95, 0x53, 0x66,
0xc3, 0x4f, 0x61, 0xf2, 0x9d, 0x35, 0xe2, 0xe2, 0x94, 0x54, 0x9f, 0x42, 0xcf, 0x60, 0xa5, 0x38,
0xfa, 0xa3, 0xb7, 0x8f, 0x7d, 0x05, 0xb7, 0xae, 0x1c, 0x07, 0x34, 0x22, 0xf9, 0xc9, 0xc6, 0x9f,
0x5f, 0xac, 0x96, 0xfe, 0xfa, 0x62, 0xb5, 0xf4, 0xaf, 0x17, 0xab, 0xa5, 0x1f, 0xdc, 0x38, 0xe2,
0x67, 0x7d, 0xa9, 0x5f, 0x0a, 0x62, 0x6a, 0x99, 0xb6, 0x45, 0x5c, 0xd6, 0x99, 0x16, 0x21, 0xe0,
0xc6, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x3f, 0x5c, 0xe7, 0x38, 0x48, 0x28, 0x00, 0x00,
// 2471 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x1a, 0x4d, 0x73, 0x1c, 0x47,
0x75, 0x3f, 0xa5, 0xdd, 0x27, 0x59, 0x5a, 0x75, 0x2c, 0x79, 0xbc, 0xb6, 0x85, 0x32, 0x60, 0x97,
0x63, 0x27, 0xab, 0xb2, 0x5d, 0x89, 0xc1, 0x09, 0x49, 0x29, 0xb2, 0x2d, 0x29, 0xb6, 0x6c, 0x31,
0x56, 0x02, 0x06, 0x03, 0xd5, 0x3b, 0xdb, 0x9a, 0x9d, 0xec, 0x7c, 0xb4, 0x67, 0x7a, 0x64, 0xd6,
0x55, 0x9c, 0xa0, 0x38, 0x51, 0x14, 0x27, 0x1f, 0xf8, 0x0b, 0x70, 0xa5, 0x38, 0x72, 0x84, 0x0b,
0x55, 0x14, 0x7f, 0x80, 0x94, 0xff, 0x02, 0x77, 0x8a, 0xea, 0x9e, 0x9e, 0xd9, 0x99, 0xd9, 0xd9,
0x95, 0xe2, 0x95, 0xd7, 0xc0, 0x45, 0x9a, 0xfe, 0x7a, 0xef, 0xf5, 0xfb, 0x7e, 0xaf, 0x17, 0x2e,
0x79, 0x84, 0xba, 0x3e, 0xf1, 0x0e, 0x89, 0xb7, 0x2e, 0x3e, 0x4d, 0xe6, 0x7a, 0xfd, 0xc4, 0x67,
0x8b, 0x7a, 0x2e, 0x73, 0x11, 0x0c, 0x66, 0x9a, 0xf7, 0x0d, 0x93, 0x75, 0x83, 0x76, 0x4b, 0x77,
0xed, 0x75, 0xec, 0x19, 0x2e, 0xf5, 0xdc, 0x2f, 0xc5, 0xc7, 0x7b, 0x7a, 0x67, 0xfd, 0xf0, 0xc6,
0x3a, 0xed, 0x19, 0xeb, 0x98, 0x9a, 0xfe, 0x3a, 0xa6, 0xd4, 0x32, 0x75, 0xcc, 0x4c, 0xd7, 0x59,
0x3f, 0xbc, 0x86, 0x2d, 0xda, 0xc5, 0xd7, 0xd6, 0x0d, 0xe2, 0x10, 0x0f, 0x33, 0xd2, 0x09, 0x21,
0x37, 0xcf, 0x19, 0xae, 0x6b, 0x58, 0x64, 0x5d, 0x8c, 0xda, 0xc1, 0xc1, 0x3a, 0xb1, 0x29, 0x93,
0x68, 0xd5, 0xdf, 0x2f, 0xc0, 0xe2, 0x2e, 0x76, 0xcc, 0x03, 0xe2, 0x33, 0x8d, 0x3c, 0x0d, 0x88,
0xcf, 0xd0, 0x13, 0xa8, 0x70, 0x62, 0x94, 0xe2, 0x5a, 0xf1, 0xf2, 0xdc, 0xf5, 0xed, 0xd6, 0x80,
0x9a, 0x56, 0x44, 0x8d, 0xf8, 0xf8, 0xa9, 0xde, 0x69, 0x1d, 0xde, 0x68, 0xd1, 0x9e, 0xd1, 0xe2,
0xd4, 0xb4, 0x12, 0xd4, 0xb4, 0x22, 0x6a, 0x5a, 0x5a, 0x7c, 0x2d, 0x4d, 0x40, 0x45, 0x4d, 0xa8,
0x79, 0xe4, 0xd0, 0xf4, 0x4d, 0xd7, 0x51, 0x4a, 0x6b, 0xc5, 0xcb, 0x75, 0x2d, 0x1e, 0x23, 0x05,
0x66, 0x1d, 0x77, 0x13, 0xeb, 0x5d, 0xa2, 0x94, 0xd7, 0x8a, 0x97, 0x6b, 0x5a, 0x34, 0x44, 0x6b,
0x30, 0x87, 0x29, 0xbd, 0x8f, 0xdb, 0xc4, 0xba, 0x47, 0xfa, 0x4a, 0x45, 0x1c, 0x4c, 0x4e, 0xf1,
0xb3, 0x98, 0xd2, 0x07, 0xd8, 0x26, 0x4a, 0x55, 0xac, 0x46, 0x43, 0x74, 0x1e, 0xea, 0x0e, 0xb6,
0x89, 0x4f, 0xb1, 0x4e, 0x94, 0x9a, 0x58, 0x1b, 0x4c, 0xa0, 0x9f, 0xc3, 0x52, 0x82, 0xf0, 0x47,
0x6e, 0xe0, 0xe9, 0x44, 0x01, 0x71, 0xf5, 0x87, 0x93, 0x5d, 0x7d, 0x23, 0x0b, 0x56, 0x1b, 0xc6,
0x84, 0x7e, 0x02, 0x55, 0x21, 0x79, 0x65, 0x6e, 0xad, 0x7c, 0xa2, 0xdc, 0x0e, 0xc1, 0x22, 0x07,
0x66, 0xa9, 0x15, 0x18, 0xa6, 0xe3, 0x2b, 0xf3, 0x02, 0xc3, 0xfe, 0x64, 0x18, 0x36, 0x5d, 0xe7,
0xc0, 0x34, 0x76, 0xb1, 0x83, 0x0d, 0x62, 0x13, 0x87, 0xed, 0x09, 0xe0, 0x5a, 0x84, 0x04, 0x3d,
0x87, 0x46, 0x2f, 0xf0, 0x99, 0x6b, 0x9b, 0xcf, 0xc9, 0x43, 0xca, 0xcf, 0xfa, 0xca, 0x29, 0xc1,
0xcd, 0x07, 0x93, 0x21, 0xbe, 0x97, 0x81, 0xaa, 0x0d, 0xe1, 0xe1, 0x4a, 0xd2, 0x0b, 0xda, 0xe4,
0x0b, 0xe2, 0x09, 0xed, 0x5a, 0x08, 0x95, 0x24, 0x31, 0x15, 0xaa, 0x91, 0x29, 0x47, 0xbe, 0xb2,
0xb8, 0x56, 0x0e, 0xd5, 0x28, 0x9e, 0x42, 0xcf, 0x60, 0xd1, 0x17, 0x92, 0xd9, 0x71, 0x18, 0x31,
0x3c, 0x93, 0xf5, 0x95, 0x86, 0x20, 0x7f, 0x77, 0x32, 0xf2, 0x1f, 0xa5, 0x81, 0x6a, 0x59, 0x2c,
0xc8, 0x86, 0x53, 0x5d, 0x62, 0xd9, 0x5c, 0x82, 0x9b, 0x1e, 0xe9, 0xf8, 0xca, 0x92, 0x10, 0xd7,
0xd6, 0xe4, 0x0a, 0x21, 0xc0, 0x69, 0x69, 0xe8, 0xe8, 0x32, 0x2c, 0x3a, 0xae, 0x26, 0x0d, 0x2f,
0x34, 0x39, 0x24, 0x4c, 0x2e, 0x3b, 0x8d, 0x2e, 0xc1, 0x02, 0xf3, 0xb0, 0xde, 0x33, 0x1d, 0x63,
0x97, 0xb0, 0xae, 0xdb, 0x51, 0xde, 0x12, 0x8c, 0xcd, 0xcc, 0x22, 0x1d, 0x10, 0x71, 0x70, 0xdb,
0x22, 0x9d, 0xf0, 0xae, 0xfb, 0x7d, 0x4a, 0x7c, 0xe5, 0xb4, 0xb8, 0xc5, 0x8d, 0x56, 0xc2, 0xe1,
0x65, 0xfc, 0x4d, 0xeb, 0xce, 0xd0, 0xa9, 0x3b, 0x0e, 0xf3, 0xfa, 0x5a, 0x0e, 0x38, 0xd4, 0x83,
0x39, 0x7e, 0x8f, 0x48, 0xb3, 0x96, 0x85, 0x68, 0x76, 0x26, 0xe3, 0xd1, 0xf6, 0x00, 0xa0, 0x96,
0x84, 0x8e, 0x5a, 0x80, 0xba, 0xd8, 0xdf, 0x0d, 0x2c, 0x66, 0x52, 0x8b, 0x84, 0x64, 0xf8, 0xca,
0x8a, 0x60, 0x53, 0xce, 0x0a, 0xba, 0x07, 0xe0, 0x91, 0x83, 0x68, 0xdf, 0x19, 0x71, 0xf3, 0xab,
0xe3, 0x6e, 0xae, 0xc5, 0xbb, 0xc3, 0x1b, 0x27, 0x8e, 0x73, 0xe4, 0xfc, 0x1a, 0x44, 0x67, 0xd2,
0x79, 0x08, 0x2f, 0xa1, 0x08, 0x8d, 0xcd, 0x59, 0xe1, 0xaa, 0x2d, 0x67, 0x85, 0x0f, 0x3c, 0x1b,
0x2a, 0x7f, 0x62, 0x0a, 0x6d, 0xc3, 0x37, 0xb0, 0xe3, 0xb8, 0x4c, 0x5c, 0x3f, 0x22, 0x65, 0x4b,
0x46, 0x8b, 0x3d, 0xcc, 0xba, 0xbe, 0xd2, 0x14, 0xa7, 0x8e, 0xda, 0xc6, 0x55, 0xc2, 0x74, 0x7c,
0x86, 0x2d, 0x4b, 0x6c, 0xda, 0xb9, 0xad, 0x9c, 0x0b, 0x55, 0x22, 0x3d, 0xdb, 0xbc, 0x03, 0x67,
0x46, 0x08, 0x17, 0x35, 0xa0, 0xdc, 0x23, 0x7d, 0x11, 0x63, 0xea, 0x1a, 0xff, 0x44, 0xa7, 0xa1,
0x7a, 0x88, 0xad, 0x80, 0x88, 0xa8, 0x50, 0xd3, 0xc2, 0xc1, 0xad, 0xd2, 0xb7, 0x8b, 0xcd, 0x5f,
0x15, 0x61, 0x31, 0xc3, 0xaa, 0x9c, 0xf3, 0x3f, 0x4e, 0x9e, 0x3f, 0x01, 0xc3, 0x39, 0xd8, 0xc7,
0x9e, 0x41, 0x58, 0x82, 0x10, 0xf5, 0x1f, 0x45, 0x50, 0x32, 0x32, 0xfc, 0xbe, 0xc9, 0xba, 0x77,
0x4d, 0x8b, 0xf8, 0xe8, 0x26, 0xcc, 0x7a, 0xe1, 0x9c, 0x8c, 0x9c, 0xe7, 0xc6, 0x88, 0x7e, 0xbb,
0xa0, 0x45, 0xbb, 0xd1, 0xc7, 0x50, 0xb3, 0x09, 0xc3, 0x1d, 0xcc, 0xb0, 0xa4, 0x7d, 0x2d, 0xef,
0x24, 0xc7, 0xb2, 0x2b, 0xf7, 0x6d, 0x17, 0xb4, 0xf8, 0x0c, 0x7a, 0x1f, 0xaa, 0x7a, 0x37, 0x70,
0x7a, 0x22, 0x66, 0xce, 0x5d, 0xbf, 0x30, 0xea, 0xf0, 0x26, 0xdf, 0xb4, 0x5d, 0xd0, 0xc2, 0xdd,
0x9f, 0xce, 0x40, 0x85, 0x62, 0x8f, 0xa9, 0x77, 0xe1, 0x74, 0x1e, 0x0a, 0x1e, 0xa8, 0xf5, 0x2e,
0xd1, 0x7b, 0x7e, 0x60, 0x4b, 0x36, 0xc7, 0x63, 0x84, 0xa0, 0xe2, 0x9b, 0xcf, 0x43, 0x56, 0x97,
0x35, 0xf1, 0xad, 0xbe, 0x03, 0x4b, 0x43, 0xd8, 0xb8, 0x50, 0x43, 0xda, 0x38, 0x84, 0x79, 0x89,
0x5a, 0x0d, 0x60, 0x79, 0x5f, 0xf0, 0x22, 0x8e, 0x56, 0xd3, 0x48, 0x3d, 0xd4, 0x6d, 0x58, 0xc9,
0xa2, 0xf5, 0xa9, 0xeb, 0xf8, 0x84, 0x1b, 0xdb, 0x21, 0xf1, 0xcc, 0x03, 0x93, 0x74, 0x06, 0xab,
0x82, 0x8a, 0x9a, 0x96, 0xb3, 0xa2, 0xfe, 0xad, 0x04, 0x2b, 0x1a, 0xf1, 0x5d, 0xeb, 0x90, 0x44,
0xce, 0x72, 0x3a, 0xd9, 0xd3, 0x8f, 0xa0, 0x8c, 0x29, 0x95, 0x6a, 0xb2, 0x73, 0x62, 0xf9, 0x89,
0xc6, 0xa1, 0xa2, 0x77, 0x61, 0x09, 0xdb, 0x6d, 0xd3, 0x08, 0xdc, 0xc0, 0x8f, 0xae, 0x25, 0x94,
0xaa, 0xae, 0x0d, 0x2f, 0x70, 0x87, 0x13, 0xc5, 0xb0, 0x0e, 0xf9, 0x99, 0x48, 0xc9, 0xca, 0x5a,
0x72, 0x2a, 0x2f, 0xc6, 0x54, 0x73, 0x63, 0x8c, 0xaa, 0xc3, 0x99, 0x21, 0x76, 0x4a, 0xd1, 0x24,
0xf3, 0xc5, 0x62, 0x26, 0x5f, 0xcc, 0x25, 0xb8, 0x34, 0x82, 0x60, 0xf5, 0x5f, 0x25, 0x68, 0x0c,
0xcc, 0x50, 0x82, 0x3f, 0x0f, 0x75, 0x5b, 0xce, 0xf9, 0x4a, 0x51, 0x78, 0xd7, 0xc1, 0x44, 0x3a,
0x75, 0x2c, 0x65, 0x53, 0xc7, 0x15, 0x98, 0x09, 0x33, 0x7b, 0xc9, 0x24, 0x39, 0x4a, 0x91, 0x5c,
0xc9, 0x90, 0xbc, 0x0a, 0xe0, 0xc7, 0xbe, 0x50, 0x99, 0x11, 0xab, 0x89, 0x19, 0xa4, 0xc2, 0xbc,
0xd0, 0x37, 0xae, 0x9b, 0x81, 0xc5, 0x94, 0x59, 0xb1, 0x23, 0x35, 0x27, 0x2c, 0xd3, 0xb5, 0x6d,
0xec, 0x74, 0x7c, 0xa5, 0x26, 0x48, 0x8e, 0xc7, 0xe8, 0x37, 0x45, 0x58, 0xce, 0xa4, 0x16, 0x12,
0x52, 0x5d, 0xe8, 0xcc, 0x0f, 0x4e, 0x34, 0x8d, 0xd9, 0xe4, 0x0e, 0x21, 0x84, 0xaf, 0xe5, 0xa3,
0x55, 0x5d, 0x58, 0xbc, 0x6f, 0x72, 0x86, 0x1f, 0xf8, 0xd3, 0xb1, 0xf2, 0x0f, 0xa0, 0xc2, 0x91,
0x71, 0x2e, 0xb5, 0x3d, 0xec, 0xe8, 0x5d, 0x12, 0x09, 0x36, 0x1e, 0x73, 0xff, 0xc5, 0xb0, 0xe1,
0x2b, 0x25, 0x31, 0x2f, 0xbe, 0xd5, 0x3f, 0x95, 0x42, 0x4a, 0x37, 0x28, 0xf5, 0xdf, 0x7c, 0x29,
0x94, 0x9f, 0x4d, 0x95, 0x87, 0xb3, 0xa9, 0x0c, 0xc9, 0x5f, 0x27, 0x9b, 0x3a, 0xa1, 0xf8, 0xac,
0x06, 0x30, 0xbb, 0x41, 0x29, 0x27, 0x04, 0x5d, 0x83, 0x0a, 0xa6, 0x34, 0x64, 0x78, 0x26, 0x14,
0xc9, 0x2d, 0xfc, 0xbf, 0x24, 0x49, 0x6c, 0x6d, 0xde, 0x84, 0x7a, 0x3c, 0x75, 0x14, 0xda, 0x7a,
0x12, 0xed, 0x1a, 0x40, 0x58, 0x7d, 0xec, 0x38, 0x07, 0x2e, 0x17, 0x29, 0xb7, 0x4c, 0x79, 0x54,
0x7c, 0xab, 0xb7, 0xa2, 0x1d, 0x82, 0xb6, 0x77, 0xa1, 0x6a, 0x32, 0x62, 0x47, 0xc4, 0xad, 0x24,
0x89, 0x1b, 0x00, 0xd2, 0xc2, 0x4d, 0xea, 0x5f, 0x6a, 0x70, 0x96, 0x4b, 0xec, 0x91, 0xb0, 0xe9,
0x0d, 0x4a, 0x6f, 0x13, 0x86, 0x4d, 0xcb, 0xff, 0x5e, 0x40, 0xbc, 0xfe, 0x6b, 0x56, 0x0c, 0x03,
0x66, 0x42, 0x63, 0x92, 0x8e, 0xfe, 0xc4, 0x0b, 0x51, 0x09, 0x7e, 0x50, 0x7d, 0x96, 0x5f, 0x4f,
0xf5, 0x99, 0x57, 0x0d, 0x56, 0xa6, 0x54, 0x0d, 0x8e, 0x6e, 0x08, 0x24, 0xda, 0x0c, 0x33, 0xe9,
0x36, 0x43, 0x4e, 0xc4, 0x9a, 0x3d, 0x6e, 0x55, 0x54, 0xcb, 0xad, 0x8a, 0xec, 0x5c, 0x3b, 0xae,
0x0b, 0x76, 0x7f, 0x37, 0xa9, 0x81, 0x23, 0x75, 0x6d, 0x92, 0xfa, 0x08, 0x5e, 0x6b, 0x7d, 0xf4,
0x79, 0xaa, 0xde, 0x09, 0x1b, 0x18, 0xef, 0x1f, 0xef, 0x4e, 0x63, 0x2a, 0x9f, 0xff, 0xbb, 0xaa,
0xe1, 0x97, 0x22, 0x59, 0xa4, 0xee, 0x80, 0x07, 0x71, 0xf6, 0xc1, 0xe3, 0x10, 0xcf, 0x03, 0xa4,
0xd3, 0xe2, 0xdf, 0xe8, 0x2a, 0x54, 0x38, 0x93, 0x65, 0x36, 0x7f, 0x26, 0xc9, 0x4f, 0x2e, 0x89,
0x0d, 0x4a, 0x1f, 0x51, 0xa2, 0x6b, 0x62, 0x13, 0xba, 0x05, 0xf5, 0x58, 0xf1, 0xa5, 0x65, 0x9d,
0x4f, 0x9e, 0x88, 0xed, 0x24, 0x3a, 0x36, 0xd8, 0xce, 0xcf, 0x76, 0x4c, 0x8f, 0xe8, 0x22, 0xd7,
0xad, 0x0e, 0x9f, 0xbd, 0x1d, 0x2d, 0xc6, 0x67, 0xe3, 0xed, 0xe8, 0x1a, 0xcc, 0x84, 0x1d, 0x1f,
0x61, 0x41, 0x73, 0xd7, 0xcf, 0x0e, 0x3b, 0xd3, 0xe8, 0x94, 0xdc, 0xa8, 0xbe, 0x28, 0xc1, 0xdb,
0x03, 0x85, 0x88, 0xac, 0x29, 0x2a, 0x37, 0xde, 0x7c, 0xc4, 0xcd, 0xe9, 0xfc, 0x94, 0xa7, 0xd1,
0xf9, 0x51, 0xff, 0x58, 0x84, 0x8b, 0xc3, 0x8c, 0xd9, 0xec, 0x62, 0x8f, 0xc5, 0xfa, 0x32, 0x0d,
0xe6, 0x44, 0x11, 0xb4, 0x34, 0x88, 0xa0, 0x29, 0x86, 0x95, 0xd3, 0x0c, 0x53, 0xff, 0x5c, 0x82,
0xb9, 0x84, 0x46, 0xe6, 0x45, 0x60, 0x9e, 0xee, 0x0a, 0x43, 0x10, 0x25, 0xb2, 0x88, 0x32, 0x75,
0x2d, 0x31, 0x83, 0x7a, 0x00, 0x14, 0x7b, 0xd8, 0x26, 0x8c, 0x78, 0x3c, 0x34, 0x70, 0x17, 0x72,
0x6f, 0x72, 0x77, 0xb5, 0x17, 0xc1, 0xd4, 0x12, 0xe0, 0x79, 0xbe, 0x2e, 0x50, 0xfb, 0x32, 0x20,
0xc8, 0x11, 0x7a, 0x06, 0x0b, 0x07, 0xa6, 0x45, 0xf6, 0x06, 0x84, 0xcc, 0x08, 0x42, 0x1e, 0x4e,
0x4e, 0xc8, 0xdd, 0x24, 0x5c, 0x2d, 0x83, 0x46, 0xbd, 0x02, 0x8d, 0xac, 0x81, 0x72, 0x22, 0x4d,
0x1b, 0x1b, 0x31, 0xb7, 0xe4, 0x48, 0x45, 0xd0, 0xc8, 0x1a, 0xa4, 0xfa, 0xcf, 0x12, 0x2c, 0xc7,
0xe0, 0x36, 0x1c, 0xc7, 0x0d, 0x1c, 0x5d, 0x74, 0x65, 0x73, 0x65, 0x71, 0x1a, 0xaa, 0xcc, 0x64,
0x56, 0x9c, 0x49, 0x89, 0x01, 0x0f, 0x86, 0xcc, 0x75, 0x2d, 0x66, 0x52, 0x29, 0xe0, 0x68, 0x18,
0xca, 0xfe, 0x69, 0x60, 0x7a, 0xa4, 0x23, 0x5c, 0x4b, 0x4d, 0x8b, 0xc7, 0x7c, 0x8d, 0xa7, 0x49,
0xa2, 0x88, 0x09, 0x99, 0x19, 0x8f, 0x79, 0x68, 0xd4, 0x5d, 0xcb, 0x22, 0x3a, 0x67, 0x47, 0xa2,
0xcc, 0xc9, 0xcc, 0x8a, 0xf2, 0x89, 0x79, 0xa6, 0x63, 0xc8, 0x22, 0x47, 0x8e, 0x38, 0x9d, 0xd8,
0xf3, 0x70, 0x5f, 0xd6, 0x36, 0xe1, 0x00, 0x7d, 0x04, 0x65, 0x1b, 0x53, 0x19, 0x39, 0xaf, 0xa4,
0xdc, 0x4d, 0x1e, 0x07, 0x5a, 0xbb, 0x98, 0x86, 0xa1, 0x85, 0x1f, 0x6b, 0x7e, 0x00, 0xb5, 0x68,
0xe2, 0x6b, 0xe5, 0x98, 0x5f, 0xc2, 0xa9, 0x94, 0x37, 0x43, 0x8f, 0x61, 0x65, 0xa0, 0x51, 0x49,
0x84, 0x32, 0xab, 0x7c, 0xfb, 0x48, 0xca, 0xb4, 0x11, 0x00, 0xd4, 0xa7, 0xb0, 0xc4, 0x55, 0x46,
0x18, 0xfe, 0x94, 0x6a, 0xa5, 0x0f, 0xa1, 0x1e, 0xa3, 0xcc, 0xd5, 0x99, 0x26, 0xd4, 0x0e, 0xa3,
0x6e, 0x79, 0x58, 0x2c, 0xc5, 0x63, 0x75, 0x03, 0x50, 0x92, 0x5e, 0x19, 0xd2, 0xae, 0xa6, 0xb3,
0xec, 0xe5, 0x6c, 0xfc, 0x12, 0xdb, 0xa3, 0x24, 0xfb, 0x0f, 0x65, 0x58, 0xdc, 0x32, 0x45, 0xbf,
0x68, 0x4a, 0x4e, 0xee, 0x0a, 0x34, 0xfc, 0xa0, 0x6d, 0xbb, 0x9d, 0xc0, 0x22, 0x32, 0xcb, 0x90,
0xa9, 0xc3, 0xd0, 0xfc, 0x38, 0xe7, 0xc7, 0x99, 0x45, 0x31, 0xeb, 0xca, 0xfa, 0x5e, 0x7c, 0xa3,
0x8f, 0xe0, 0xec, 0x03, 0xf2, 0x4c, 0xde, 0x67, 0xcb, 0x72, 0xdb, 0x6d, 0xd3, 0x31, 0x22, 0x24,
0x61, 0xe7, 0x63, 0xf4, 0x86, 0xbc, 0xdc, 0x73, 0x26, 0x3f, 0xf7, 0xcc, 0x89, 0x54, 0xb3, 0x53,
0x89, 0x54, 0xbf, 0x28, 0x42, 0x63, 0x20, 0x2e, 0x29, 0xf0, 0x9b, 0xa1, 0x61, 0x86, 0xe2, 0xbe,
0x98, 0x14, 0x77, 0x76, 0xeb, 0xab, 0xdb, 0xe4, 0x7c, 0xd2, 0x26, 0xbf, 0x2a, 0xc1, 0xf2, 0x96,
0xc9, 0x22, 0x6f, 0x68, 0xfe, 0xaf, 0xa9, 0x4e, 0x8e, 0xa0, 0x2b, 0xc7, 0x16, 0x74, 0x75, 0x2a,
0x82, 0x6e, 0xc1, 0x4a, 0x96, 0xc3, 0x52, 0xda, 0xa7, 0xa1, 0x4a, 0xc5, 0x53, 0x41, 0xd8, 0x52,
0x09, 0x07, 0xea, 0xbf, 0x6b, 0x70, 0xe1, 0x73, 0xda, 0xc1, 0x2c, 0xee, 0xdf, 0xdd, 0x75, 0x3d,
0xf1, 0x56, 0x30, 0x1d, 0xd1, 0x64, 0x9e, 0x87, 0x4b, 0x63, 0x9f, 0x87, 0xcb, 0x63, 0x9e, 0x87,
0x2b, 0xc7, 0x7a, 0x1e, 0xae, 0x4e, 0xed, 0x79, 0x78, 0xb8, 0xcc, 0x9c, 0xc9, 0x2d, 0x33, 0x1f,
0xa7, 0x4a, 0xb1, 0x59, 0x61, 0x8b, 0xdf, 0x49, 0xda, 0xe2, 0x58, 0xe9, 0x8c, 0x7d, 0x88, 0xca,
0xbc, 0xaa, 0xd6, 0x8e, 0x7c, 0x55, 0xad, 0x0f, 0xbf, 0xaa, 0xe6, 0xbf, 0xa4, 0xc1, 0xc8, 0x97,
0xb4, 0x4b, 0xb0, 0xe0, 0xf7, 0x1d, 0x9d, 0x74, 0xe2, 0xae, 0xee, 0x5c, 0x78, 0xed, 0xf4, 0x6c,
0xca, 0xcc, 0xe6, 0x33, 0x66, 0x16, 0x6b, 0xea, 0xa9, 0x84, 0xa6, 0xe6, 0x19, 0xdf, 0xc2, 0xc8,
0x0a, 0x3f, 0xf3, 0xc8, 0xb5, 0x98, 0xf7, 0xc8, 0x85, 0x7a, 0xd0, 0x88, 0xa8, 0x8a, 0x05, 0xd0,
0x10, 0x02, 0xf8, 0xe4, 0xf8, 0x02, 0x78, 0x94, 0x81, 0x10, 0x8a, 0x61, 0x08, 0xf0, 0x7f, 0x4d,
0x51, 0xdb, 0xfc, 0x75, 0x11, 0x96, 0x73, 0x89, 0x7e, 0x33, 0x35, 0xf6, 0x17, 0xb0, 0x3a, 0x8a,
0xc1, 0xd2, 0x71, 0x29, 0x30, 0xab, 0x77, 0xb1, 0x63, 0x88, 0x6e, 0xb0, 0x68, 0xfa, 0xc8, 0xe1,
0xb8, 0xa2, 0xf0, 0xfa, 0x8b, 0x79, 0x58, 0x1a, 0xd4, 0x66, 0xfc, 0xaf, 0xa9, 0x13, 0xf4, 0x10,
0x1a, 0xd1, 0x83, 0x68, 0xf4, 0xa0, 0x80, 0xc6, 0xbd, 0xf6, 0x35, 0xcf, 0xe7, 0x2f, 0x86, 0xa4,
0xa9, 0x05, 0xa4, 0xc3, 0xd9, 0x2c, 0xc0, 0xc1, 0xc3, 0xe2, 0xb7, 0xc6, 0x40, 0x8e, 0x77, 0x1d,
0x85, 0xe2, 0x72, 0x11, 0x3d, 0x86, 0x85, 0xf4, 0xf3, 0x17, 0x4a, 0x25, 0xab, 0xb9, 0x2f, 0x72,
0x4d, 0x75, 0xdc, 0x96, 0x98, 0xfe, 0x27, 0x5c, 0x2b, 0x53, 0xef, 0x37, 0x48, 0x4d, 0x37, 0x82,
0xf2, 0xde, 0xca, 0x9a, 0xdf, 0x1c, 0xbb, 0x27, 0x86, 0xfe, 0x21, 0xd4, 0xa2, 0x27, 0x84, 0x34,
0x9b, 0x33, 0x0f, 0x0b, 0xcd, 0x46, 0x1a, 0xde, 0x81, 0xaf, 0x16, 0xd0, 0xc7, 0x30, 0xc7, 0xb7,
0x3d, 0xdc, 0xdc, 0xd9, 0xc7, 0xc6, 0x2b, 0x9d, 0xaf, 0x45, 0x2d, 0xf6, 0xe1, 0xc3, 0x89, 0xc6,
0x7b, 0xf3, 0xad, 0x9c, 0x66, 0xb7, 0x5a, 0x40, 0x9f, 0x84, 0xf8, 0xf7, 0xe4, 0xef, 0x63, 0x56,
0x5a, 0xe1, 0xcf, 0xb1, 0x5a, 0xd1, 0xcf, 0xb1, 0x5a, 0x77, 0x6c, 0xca, 0xfa, 0xcd, 0x9c, 0x6e,
0xb4, 0x04, 0xf0, 0x04, 0x4e, 0x6d, 0x11, 0x36, 0x68, 0x1e, 0xa1, 0x8b, 0xc7, 0x6a, 0xb1, 0x35,
0xd5, 0xec, 0xb6, 0xe1, 0xfe, 0x93, 0x5a, 0x40, 0x2f, 0x8a, 0xf0, 0xd6, 0x16, 0x61, 0xd9, 0x76,
0x0c, 0x7a, 0x2f, 0x1f, 0xc9, 0x88, 0xb6, 0x4d, 0xf3, 0xc1, 0xa4, 0x36, 0x9d, 0x06, 0xab, 0x16,
0xd0, 0x6f, 0x8b, 0xb0, 0xb0, 0x45, 0xb8, 0xdc, 0x62, 0x9a, 0xae, 0x8d, 0xa7, 0x29, 0xa7, 0x63,
0xd2, 0x9c, 0xb0, 0xf5, 0x99, 0xc0, 0xae, 0x16, 0xd0, 0xef, 0x8a, 0x70, 0x26, 0xc1, 0xab, 0x24,
0xbe, 0x57, 0xa1, 0xed, 0xb3, 0x09, 0x7f, 0x89, 0x95, 0x00, 0xa9, 0x16, 0xd0, 0x9e, 0x50, 0x93,
0x41, 0x41, 0x86, 0x2e, 0xe4, 0x56, 0x5e, 0x31, 0xf6, 0xd5, 0x51, 0xcb, 0xb1, 0x6a, 0x7c, 0x06,
0x73, 0x5b, 0x84, 0x45, 0x49, 0x7c, 0x5a, 0xf9, 0x33, 0x45, 0x5b, 0xda, 0xfb, 0x64, 0xf3, 0x7e,
0xa1, 0xc4, 0x4b, 0x21, 0xac, 0x44, 0x4e, 0x99, 0x76, 0x3f, 0xb9, 0x19, 0x7d, 0x5a, 0x89, 0xf3,
0x53, 0x52, 0xb5, 0x80, 0x9e, 0xc2, 0x4a, 0xbe, 0xf7, 0x47, 0xef, 0x1c, 0x3b, 0x04, 0x37, 0xaf,
0x1c, 0x67, 0x6b, 0x84, 0xf2, 0xd3, 0x8d, 0xbf, 0xbe, 0x5c, 0x2d, 0xfe, 0xfd, 0xe5, 0x6a, 0xf1,
0xab, 0x97, 0xab, 0xc5, 0x1f, 0xde, 0x38, 0xe2, 0x17, 0x9b, 0x89, 0x1f, 0x81, 0x62, 0x6a, 0xea,
0x96, 0x49, 0x1c, 0xd6, 0x9e, 0x11, 0x2e, 0xe0, 0xc6, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x58,
0x77, 0x20, 0x64, 0x23, 0x2a, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -3515,17 +3525,19 @@ func (m *ManifestRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
dAtA[i] = 0x8a
}
}
if m.VerifySignature {
i--
if m.VerifySignature {
dAtA[i] = 1
} else {
dAtA[i] = 0
if m.SourceIntegrity != nil {
{
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintRepository(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0x80
dAtA[i] = 0x82
}
if len(m.ApiVersions) > 0 {
for iNdEx := len(m.ApiVersions) - 1; iNdEx >= 0; iNdEx-- {
@ -4034,6 +4046,18 @@ func (m *ManifestResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.SourceIntegrityResult != nil {
{
size, err := m.SourceIntegrityResult.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintRepository(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x4a
}
if len(m.Commands) > 0 {
for iNdEx := len(m.Commands) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Commands[iNdEx])
@ -4640,15 +4664,17 @@ func (m *RepoServerRevisionMetadataRequest) MarshalToSizedBuffer(dAtA []byte) (i
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.CheckSignature {
i--
if m.CheckSignature {
dAtA[i] = 1
} else {
dAtA[i] = 0
if m.SourceIntegrity != nil {
{
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintRepository(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x18
dAtA[i] = 0x1a
}
if len(m.Revision) > 0 {
i -= len(m.Revision)
@ -5161,15 +5187,17 @@ func (m *GitFilesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.VerifyCommit {
i--
if m.VerifyCommit {
dAtA[i] = 1
} else {
dAtA[i] = 0
if m.SourceIntegrity != nil {
{
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintRepository(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x38
dAtA[i] = 0x3a
}
if m.NoRevisionCache {
i--
@ -5302,15 +5330,17 @@ func (m *GitDirectoriesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.VerifyCommit {
i--
if m.VerifyCommit {
dAtA[i] = 1
} else {
dAtA[i] = 0
if m.SourceIntegrity != nil {
{
size, err := m.SourceIntegrity.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintRepository(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x28
dAtA[i] = 0x2a
}
if m.NoRevisionCache {
i--
@ -5703,8 +5733,9 @@ func (m *ManifestRequest) Size() (n int) {
n += 1 + l + sovRepository(uint64(l))
}
}
if m.VerifySignature {
n += 3
if m.SourceIntegrity != nil {
l = m.SourceIntegrity.Size()
n += 2 + l + sovRepository(uint64(l))
}
if len(m.HelmRepoCreds) > 0 {
for _, e := range m.HelmRepoCreds {
@ -5976,6 +6007,10 @@ func (m *ManifestResponse) Size() (n int) {
n += 1 + l + sovRepository(uint64(l))
}
}
if m.SourceIntegrityResult != nil {
l = m.SourceIntegrityResult.Size()
n += 1 + l + sovRepository(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -6219,8 +6254,9 @@ func (m *RepoServerRevisionMetadataRequest) Size() (n int) {
if l > 0 {
n += 1 + l + sovRepository(uint64(l))
}
if m.CheckSignature {
n += 2
if m.SourceIntegrity != nil {
l = m.SourceIntegrity.Size()
n += 1 + l + sovRepository(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
@ -6474,8 +6510,9 @@ func (m *GitFilesRequest) Size() (n int) {
if m.NoRevisionCache {
n += 2
}
if m.VerifyCommit {
n += 2
if m.SourceIntegrity != nil {
l = m.SourceIntegrity.Size()
n += 1 + l + sovRepository(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
@ -6527,8 +6564,9 @@ func (m *GitDirectoriesRequest) Size() (n int) {
if m.NoRevisionCache {
n += 2
}
if m.VerifyCommit {
n += 2
if m.SourceIntegrity != nil {
l = m.SourceIntegrity.Size()
n += 1 + l + sovRepository(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
@ -7093,10 +7131,10 @@ func (m *ManifestRequest) Unmarshal(dAtA []byte) error {
m.ApiVersions = append(m.ApiVersions, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
case 16:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field VerifySignature", wireType)
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
}
var v int
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
@ -7106,12 +7144,28 @@ func (m *ManifestRequest) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.VerifySignature = bool(v != 0)
if msglen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.SourceIntegrity == nil {
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
}
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 17:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field HelmRepoCreds", wireType)
@ -8711,6 +8765,42 @@ func (m *ManifestResponse) Unmarshal(dAtA []byte) error {
}
m.Commands = append(m.Commands, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
case 9:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrityResult", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.SourceIntegrityResult == nil {
m.SourceIntegrityResult = &v1alpha1.SourceIntegrityCheckResult{}
}
if err := m.SourceIntegrityResult.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])
@ -10417,10 +10507,10 @@ func (m *RepoServerRevisionMetadataRequest) Unmarshal(dAtA []byte) error {
m.Revision = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field CheckSignature", wireType)
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
}
var v int
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
@ -10430,12 +10520,28 @@ func (m *RepoServerRevisionMetadataRequest) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.CheckSignature = bool(v != 0)
if msglen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.SourceIntegrity == nil {
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
}
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])
@ -11942,10 +12048,10 @@ func (m *GitFilesRequest) Unmarshal(dAtA []byte) error {
}
m.NoRevisionCache = bool(v != 0)
case 7:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field VerifyCommit", wireType)
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
}
var v int
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
@ -11955,12 +12061,28 @@ func (m *GitFilesRequest) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.VerifyCommit = bool(v != 0)
if msglen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.SourceIntegrity == nil {
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
}
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])
@ -12300,10 +12422,10 @@ func (m *GitDirectoriesRequest) Unmarshal(dAtA []byte) error {
}
m.NoRevisionCache = bool(v != 0)
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field VerifyCommit", wireType)
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SourceIntegrity", wireType)
}
var v int
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
@ -12313,12 +12435,28 @@ func (m *GitDirectoriesRequest) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.VerifyCommit = bool(v != 0)
if msglen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.SourceIntegrity == nil {
m.SourceIntegrity = &v1alpha1.SourceIntegrity{}
}
if err := m.SourceIntegrity.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])

View file

@ -9,7 +9,7 @@ import (
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
)
const maxRecreateRetries = 5
@ -61,9 +61,9 @@ func StartGPGWatcher(sourcePath string) error {
// Force sync because we probably missed an event
forceSync = true
}
if gpg.IsShortKeyID(path.Base(event.Name)) || forceSync {
if sourceintegrity.IsShortKeyID(path.Base(event.Name)) || forceSync {
log.Infof("Updating GPG keyring on filesystem event")
added, removed, err := gpg.SyncKeyRingFromDirectory(sourcePath)
added, removed, err := sourceintegrity.SyncKeyRingFromDirectory(sourcePath)
if err != nil {
log.Errorf("Could not sync keyring: %s", err.Error())
} else {

View file

@ -24,6 +24,8 @@ import (
gocache "github.com/patrickmn/go-cache"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
"github.com/argoproj/argo-cd/v3/util/oci"
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
@ -59,7 +61,6 @@ import (
"github.com/argoproj/argo-cd/v3/util/cmp"
"github.com/argoproj/argo-cd/v3/util/git"
"github.com/argoproj/argo-cd/v3/util/glob"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v3/util/grpc"
"github.com/argoproj/argo-cd/v3/util/helm"
utilio "github.com/argoproj/argo-cd/v3/util/io"
@ -313,11 +314,13 @@ type operationContext struct {
appPath string
// output of 'git verify-(tag/commit)', if signature verification is enabled (otherwise "")
verificationResult string
//
// Deprecated: rely on sourceIntegrityResult. will be removed with the next major version.
verificationResult string
sourceIntegrityResult *v1alpha1.SourceIntegrityCheckResult
}
// The 'operation' function parameter of 'runRepoOperation' may call this function to retrieve
// the appPath or GPG verificationResult.
// The 'operation' function parameter of 'runRepoOperation' may call this function to retrieve operationContext data.
// Failure to generate either of these values will return an error which may be cached by
// the calling function (for example, 'runManifestGen')
type operationContextSrc = func() (*operationContext, error)
@ -331,7 +334,7 @@ func (s *Service) runRepoOperation(
revision string,
repo *v1alpha1.Repository,
source *v1alpha1.ApplicationSource,
verifyCommit bool,
sourceIntegrity *v1alpha1.SourceIntegrity,
cacheFn func(cacheKey string, refSourceCommitSHAs cache.ResolvedRevisions, firstInvocation bool) (bool, error),
operation func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) error,
settings operationSettings,
@ -423,7 +426,7 @@ func (s *Service) runRepoOperation(
}
return operation(ociPath, revision, revision, func() (*operationContext, error) {
return &operationContext{appPath, ""}, nil
return &operationContext{appPath, "", nil}, nil
})
} else if source.IsHelm() {
if settings.noCache {
@ -458,7 +461,7 @@ func (s *Service) runRepoOperation(
}
}
return operation(chartPath, revision, revision, func() (*operationContext, error) {
return &operationContext{chartPath, ""}, nil
return &operationContext{chartPath, "", nil}, nil
})
}
closer, err := s.repoLock.Lock(gitClient.Root(), revision, settings.allowConcurrent, func(clean bool) (goio.Closer, error) {
@ -508,27 +511,30 @@ func (s *Service) runRepoOperation(
// Here commitSHA refers to the SHA of the actual commit, whereas revision refers to the branch/tag name etc
// We use the commitSHA to generate manifests and store them in cache, and revision to retrieve them from cache
return operation(gitClient.Root(), commitSHA, revision, func() (*operationContext, error) {
var signature string
if verifyCommit {
// When the revision is an annotated tag, we need to pass the unresolved revision (i.e. the tag name)
// to the verification routine. For everything else, we work with the SHA that the target revision is
// pointing to (i.e. the resolved revision).
var rev string
if gitClient.IsAnnotatedTag(revision) {
rev = unresolvedRevision
} else {
rev = revision
}
signature, err = gitClient.VerifyCommitSignature(rev)
if err != nil {
return nil, err
}
// Pass in the originalRevision to have access to the eventual tag name. Use resolved revision only if the originalRevision is unspecified.
var rev string
if unresolvedRevision != "" {
rev = unresolvedRevision
} else {
rev = revision
}
sourceIntegrityResult, _, err := sourceintegrity.VerifyGit(sourceIntegrity, gitClient, rev)
if err != nil {
return nil, err
}
// Computed and passed to preserve API backwards compatibility only. Decisions are made based on SourceIntegrityResult.
verificationResult, err := gitClient.VerifyCommitSignature(rev) // nolint:staticcheck
if err != nil {
return nil, err
}
appPath, err := apppathutil.Path(gitClient.Root(), source.Path)
if err != nil {
return nil, err
}
return &operationContext{appPath, signature}, nil
return &operationContext{appPath, verificationResult, sourceIntegrityResult}, nil
})
}
@ -667,7 +673,7 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
}
settings := operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, noRevisionCache: q.NoRevisionCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()}
err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources)
err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.SourceIntegrity, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources)
// if the tarDoneCh message is sent it means that the manifest
// generation is being managed by the cmp-server. In this case
@ -729,7 +735,7 @@ func (s *Service) GenerateManifestWithFiles(stream apiclient.RepoServerService_G
if err != nil {
return nil, fmt.Errorf("failed to get app path: %w", err)
}
return &operationContext{appPath, ""}, nil
return &operationContext{appPath, "", nil}, nil
}, req)
var res *apiclient.ManifestResponse
@ -975,6 +981,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
MostRecentError: "",
}
manifestGenResult.Revision = commitSHA
manifestGenResult.SourceIntegrityResult = opContext.sourceIntegrityResult
manifestGenResult.VerifyResult = opContext.verificationResult
err = s.cache.SetManifests(cacheKey, appSourceCopy, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, &manifestGenCacheEntry, refSourceCommitSHAs, q.InstallationID)
if err != nil {
@ -2395,7 +2402,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD
}
settings := operationSettings{allowConcurrent: q.Source.AllowsConcurrentProcessing(), noCache: q.NoCache, noRevisionCache: q.NoCache || q.NoRevisionCache}
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, cacheFn, operation, settings, len(q.RefSources) > 0, q.RefSources)
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, nil, cacheFn, operation, settings, len(q.RefSources) > 0, q.RefSources)
return res, err
}
@ -2643,19 +2650,13 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer
}
metadata, err := s.cache.GetRevisionMetadata(q.Repo.Repo, q.Revision)
if err == nil {
// The logic here is that if a signature check on metadata is requested,
// but there is none in the cache, we handle as if we have a cache miss
// and re-generate the metadata. Otherwise, if there is signature info
// in the metadata, but none was requested, we remove it from the data
// that we return.
if !q.CheckSignature || metadata.SignatureInfo != "" {
// The SourceIntegrity criteria could have changed since this was cached - it could have been added, removed, or changed.
// If present in request or the cached version, treat this as a cache miss.
if q.SourceIntegrity == nil && metadata.SourceIntegrityResult == nil {
log.Infof("revision metadata cache hit: %s/%s", q.Repo.Repo, q.Revision)
if !q.CheckSignature {
metadata.SignatureInfo = ""
}
return metadata, nil
}
log.Infof("revision metadata cache hit, but need to regenerate due to missing signature info: %s/%s", q.Repo.Repo, q.Revision)
log.Infof("revision metadata cache hit, but need to regenerate due to source integrity checks: %s/%s", q.Repo.Repo, q.Revision)
} else {
if !errors.Is(err, cache.ErrCacheMiss) {
log.Warnf("revision metadata cache error %s/%s: %v", q.Repo.Repo, q.Revision, err)
@ -2681,30 +2682,14 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer
defer utilio.Close(closer)
m, err := gitClient.RevisionMetadata(q.Revision)
sourceIntegrityResult, legacySignatureInfo, err := sourceintegrity.VerifyGit(q.SourceIntegrity, gitClient, q.Revision)
if err != nil {
return nil, err
}
// Run gpg verify-commit on the revision
signatureInfo := ""
if gpg.IsGPGEnabled() && q.CheckSignature {
cs, err := gitClient.VerifyCommitSignature(q.Revision)
if err != nil {
log.Errorf("error verifying signature of commit '%s' in repo '%s': %v", q.Revision, q.Repo.Repo, err)
return nil, err
}
if cs != "" {
vr := gpg.ParseGitCommitVerification(cs)
if vr.Result == gpg.VerifyResultUnknown {
signatureInfo = "UNKNOWN signature: " + vr.Message
} else {
signatureInfo = fmt.Sprintf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
}
} else {
signatureInfo = "Revision is not signed."
}
m, err := gitClient.RevisionMetadata(q.Revision)
if err != nil {
return nil, err
}
relatedRevisions := make([]v1alpha1.RevisionReference, len(m.References))
@ -2724,7 +2709,16 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer
},
}
}
metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: &metav1.Time{Time: m.Date}, Tags: m.Tags, Message: m.Message, SignatureInfo: signatureInfo, References: relatedRevisions}
metadata = &v1alpha1.RevisionMetadata{
Author: m.Author,
Date: &metav1.Time{Time: m.Date},
Tags: m.Tags,
Message: m.Message,
// TODO remove with next major version
SignatureInfo: legacySignatureInfo,
References: relatedRevisions,
SourceIntegrityResult: sourceIntegrityResult,
}
_ = s.cache.SetRevisionMetadata(q.Repo.Repo, q.Revision, metadata)
return metadata, nil
}
@ -3110,10 +3104,6 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ
return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err)
}
if err := verifyCommitSignature(request.VerifyCommit, gitClient, revision, repo); err != nil {
return nil, err
}
// check the cache and return the results if present
if cachedFiles, err := s.cache.GetGitFiles(repo.Repo, revision, gitPath); err == nil {
log.Debugf("cache hit for repo: %s revision: %s pattern: %s", repo.Repo, revision, gitPath)
@ -3134,6 +3124,14 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ
}
defer utilio.Close(closer)
sourceIntegrityResult, _, err := sourceintegrity.VerifyGit(request.SourceIntegrity, gitClient, revision)
if err != nil {
return nil, err
}
if err := sourceIntegrityResult.AsError(); err != nil {
return nil, err
}
gitFiles, err := gitClient.LsFiles(gitPath, enableNewGitFileGlobbing)
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to list files. repo %s with revision %s pattern %s: %v", repo.Repo, revision, gitPath, err)
@ -3159,26 +3157,6 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ
}, nil
}
func verifyCommitSignature(verifyCommit bool, gitClient git.Client, revision string, repo *v1alpha1.Repository) error {
if gpg.IsGPGEnabled() && verifyCommit {
cs, err := gitClient.VerifyCommitSignature(revision)
if err != nil {
log.Errorf("error verifying signature of commit '%s' in repo '%s': %v", revision, repo.Repo, err)
return err
}
if cs == "" {
return fmt.Errorf("revision %s is not signed", revision)
}
vr := gpg.ParseGitCommitVerification(cs)
if vr.Result == gpg.VerifyResultUnknown {
return fmt.Errorf("UNKNOWN signature: %s", vr.Message)
}
log.Debugf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
}
return nil
}
func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDirectoriesRequest) (*apiclient.GitDirectoriesResponse, error) {
repo := request.GetRepo()
revision := request.GetRevision()
@ -3192,10 +3170,6 @@ func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDir
return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err)
}
if err := verifyCommitSignature(request.VerifyCommit, gitClient, revision, repo); err != nil {
return nil, err
}
// check the cache and return the results if present
if cachedPaths, err := s.cache.GetGitDirectories(repo.Repo, revision); err == nil {
log.Debugf("cache hit for repo: %s revision: %s", repo.Repo, revision)
@ -3216,6 +3190,14 @@ func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDir
}
defer utilio.Close(closer)
sourceIntegrityResult, _, err := sourceintegrity.VerifyGit(request.SourceIntegrity, gitClient, revision)
if err != nil {
return nil, err
}
if err := sourceIntegrityResult.AsError(); err != nil {
return nil, err
}
repoRoot := gitClient.Root()
var paths []string
if err := filepath.WalkDir(repoRoot, func(path string, entry fs.DirEntry, fnErr error) error {

View file

@ -25,8 +25,8 @@ message ManifestRequest {
string kubeVersion = 14;
// ApiVersions is the list of API versions from the destination cluster, used for rendering Helm charts.
repeated string apiVersions = 15;
// Request to verify the signature when generating the manifests (only for Git repositories)
bool verifySignature = 16;
// Source integrity constrains to verify the sources before use
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 16;
repeated github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.RepoCreds helmRepoCreds = 17;
bool noRevisionCache = 18;
string trackingMethod = 19;
@ -98,9 +98,11 @@ message ManifestResponse {
string revision = 4;
string sourceType = 6;
// Raw response of git verify-commit operation (always the empty string for Helm)
// Deprecated: Use SourceIntegrityResult for more detailed information. VerifyResult will be removed with the next major version.
string verifyResult = 7;
// Commands is the list of commands used to hydrate the manifests
repeated string commands = 8;
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrityCheckResult sourceIntegrityResult = 9;
}
message ListRefsRequest {
@ -163,8 +165,7 @@ message RepoServerRevisionMetadataRequest {
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.Repository repo = 1;
// the revision within the repo
string revision = 2;
// whether to check signature on revision
bool checkSignature = 3;
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 3;
}
message RepoServerRevisionChartDetailsRequest {
@ -247,7 +248,7 @@ message GitFilesRequest {
string path = 4;
bool NewGitFileGlobbingEnabled = 5;
bool noRevisionCache = 6;
bool verifyCommit = 7;
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 7;
}
message GitFilesResponse {
@ -260,7 +261,7 @@ message GitDirectoriesRequest {
bool submoduleEnabled = 2;
string revision = 3;
bool noRevisionCache = 4;
bool verifyCommit = 5;
github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.SourceIntegrity sourceIntegrity = 5;
}
message GitDirectoriesResponse {
@ -345,7 +346,7 @@ service RepoServerService {
// Get the meta-data (author, date, tags, message) for a specific revision of the OCI image
rpc GetOCIMetadata(RepoServerRevisionChartDetailsRequest) returns (github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.OCIMetadata) {
}
// Get the chart details (author, date, tags, message) for a specific revision of the repo
rpc GetRevisionChartDetails(RepoServerRevisionChartDetailsRequest) returns (github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.ChartDetails) {
}

View file

@ -55,10 +55,55 @@ import (
"github.com/argoproj/argo-cd/v3/util/settings"
)
const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
`
var sourceIntegrityReqStrict = &v1alpha1.SourceIntegrity{
Git: &v1alpha1.SourceIntegrityGit{
Policies: []*v1alpha1.SourceIntegrityGitPolicy{
{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{{URL: "*"}},
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
Keys: []string{"f24e21389b25a3c1", "ffffffffff25a3c1"},
},
},
},
},
}
var LsSignaturesMockOk = func(_ string, _ bool) (info []git.RevisionSignatureInfo, err error) {
return []git.RevisionSignatureInfo{
{
Revision: "d71589b8001a0bd78bb311cb03c9d129c6f91de1",
VerificationResult: git.GPGVerificationResultGood,
SignatureKeyID: "f24e21389b25a3c1",
Date: "Fri Oct 31 14:42:39 2025 +0100",
AuthorIdentity: "Jane Doe <jdoe@acme.com>",
},
}, nil
}
var LsSignaturesMockGitError = func(_ string, _ bool) (info []git.RevisionSignatureInfo, err error) {
return []git.RevisionSignatureInfo{{
Revision: "171589b8001a0bd78bb311cb03c9d129c6f91de1",
VerificationResult: git.GPGVerificationResultExpiredKey,
SignatureKeyID: "EXPIRED",
Date: "Fri Oct 31 14:42:39 2025 +0100",
AuthorIdentity: "Late Fred <lfred@acme.com>",
}, {
Revision: "111589b8001a0bd78bb311cb03c9d129c6f91de1",
VerificationResult: git.GPGVerificationResultUnsigned,
SignatureKeyID: "",
Date: "Fri Oct 31 14:42:39 2025 +0100",
AuthorIdentity: "Unsigned <unsigned@acme.com>",
}}, nil
}
var sourceIntegrityResultGitError = &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: "GIT/GPG",
Problems: []string{
"Failed verifying revision 171589b8001a0bd78bb311cb03c9d129c6f91de1 by 'Late Fred <lfred@acme.com>': signed with expired key (key_id=EXPIRED)",
"Failed verifying revision 111589b8001a0bd78bb311cb03c9d129c6f91de1 by 'Unsigned <unsigned@acme.com>': unsigned (key_id=)",
},
}}}
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *ocimocks.Client, *iomocks.TempPaths)
@ -102,7 +147,7 @@ func newCacheMocksWithOpts(repoCacheExpiration, revisionCacheExpiration, revisio
}
}
func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
func newServiceWithMocks(t *testing.T, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
t.Helper()
root, err := filepath.Abs(root)
if err != nil {
@ -116,12 +161,9 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
gitClient.EXPECT().LsRemote(mock.Anything).Return(mock.Anything, nil)
gitClient.EXPECT().CommitSHA().Return(mock.Anything, nil)
gitClient.EXPECT().Root().Return(root)
gitClient.EXPECT().RepoURL().Return("https://fake.com/fake_group/fake_repo.git")
gitClient.EXPECT().IsAnnotatedTag(mock.Anything).Return(false)
if signed {
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return(testSignature, nil)
} else {
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
}
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
chart := "my-chart"
oobChart := "out-of-bounds-chart"
@ -176,13 +218,7 @@ func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *git
func newService(t *testing.T, root string) *Service {
t.Helper()
service, _, _ := newServiceWithMocks(t, root, false)
return service
}
func newServiceWithSignature(t *testing.T, root string) *Service {
t.Helper()
service, _, _ := newServiceWithMocks(t, root, true)
service, _, _ := newServiceWithMocks(t, root)
return service
}
@ -201,6 +237,8 @@ func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
gitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil)
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
gitClient.EXPECT().LsRemote(revision).Return(revision, revisionErr)
gitClient.EXPECT().IsAnnotatedTag(revision).Return(revisionErr != nil)
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", nil)
gitClient.EXPECT().CommitSHA().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
gitClient.EXPECT().Root().Return(root)
paths.EXPECT().GetPath(mock.Anything).Return(root, nil)
@ -377,7 +415,7 @@ func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
}
func TestGenerateManifests_EmptyCache(t *testing.T) {
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base")
src := v1alpha1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
@ -525,7 +563,7 @@ func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
// ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
func TestHelmManifestFromChartRepo(t *testing.T) {
root := t.TempDir()
service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
service, gitMocks, mockCache := newServiceWithMocks(t, root)
source := &v1alpha1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
request := &apiclient.ManifestRequest{
Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
@ -680,7 +718,7 @@ func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
}
func TestGenerateManifestsUseExactRevision(t *testing.T) {
service, gitClient, _ := newServiceWithMocks(t, ".", false)
service, gitClient, _ := newServiceWithMocks(t, ".")
src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
@ -1771,9 +1809,10 @@ func TestGetHelmCharts(t *testing.T) {
}
func TestGetRevisionMetadata(t *testing.T) {
service, gitClient, _ := newServiceWithMocks(t, "../..", false)
service, gitClient, _ := newServiceWithMocks(t, "../..")
now := time.Now()
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockOk)
gitClient.EXPECT().RevisionMetadata(mock.Anything).Return(&git.RevisionMetadata{
Message: "test",
Author: "author",
@ -1796,9 +1835,9 @@ func TestGetRevisionMetadata(t *testing.T) {
}, nil)
res, err := service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
Repo: &v1alpha1.Repository{},
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
CheckSignature: true,
Repo: &v1alpha1.Repository{},
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
SourceIntegrity: sourceIntegrityReqStrict,
})
require.NoError(t, err)
@ -1806,16 +1845,16 @@ func TestGetRevisionMetadata(t *testing.T) {
assert.Equal(t, now, res.Date.Time)
assert.Equal(t, "author", res.Author)
assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
assert.NotEmpty(t, res.SignatureInfo)
assert.True(t, res.SourceIntegrityResult.IsValid())
require.Len(t, res.References, 1)
require.NotNil(t, res.References[0].Commit)
assert.Equal(t, "test-sha", res.References[0].Commit.SHA)
// Check for truncated revision value
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
Repo: &v1alpha1.Repository{},
Revision: "c0b400f",
CheckSignature: true,
Repo: &v1alpha1.Repository{},
Revision: "c0b400f",
SourceIntegrity: sourceIntegrityReqStrict,
})
require.NoError(t, err)
@ -1823,95 +1862,111 @@ func TestGetRevisionMetadata(t *testing.T) {
assert.Equal(t, now, res.Date.Time)
assert.Equal(t, "author", res.Author)
assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
assert.NotEmpty(t, res.SignatureInfo)
assert.True(t, res.SourceIntegrityResult.IsValid())
// Cache hit - signature info should not be in result
// Cache hit, but SourceIntegrity removed, will be recreated without SourceIntegrityResult
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
Repo: &v1alpha1.Repository{},
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
CheckSignature: false,
Repo: &v1alpha1.Repository{},
Revision: "c0b400fc458875d925171398f9ba9eabd5529923",
SourceIntegrity: nil,
})
require.NoError(t, err)
assert.Empty(t, res.SignatureInfo)
assert.Nil(t, res.SourceIntegrityResult)
// Enforce cache miss - signature info should not be in result
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
Repo: &v1alpha1.Repository{},
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
CheckSignature: false,
Repo: &v1alpha1.Repository{},
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
SourceIntegrity: nil,
})
require.NoError(t, err)
assert.Empty(t, res.SignatureInfo)
assert.Nil(t, res.SourceIntegrityResult)
// Cache hit on previous entry that did not have signature info
// Cache miss on the previous entry that did not have signature info - recreated
res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
Repo: &v1alpha1.Repository{},
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
CheckSignature: true,
Repo: &v1alpha1.Repository{},
Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091",
SourceIntegrity: sourceIntegrityReqStrict,
})
require.NoError(t, err)
assert.NotEmpty(t, res.SignatureInfo)
assert.True(t, res.SourceIntegrityResult.IsValid())
}
func TestGetSignatureVerificationResult(t *testing.T) {
// Commit with signature and verification requested
{
service := newServiceWithSignature(t, "../../manifests/base")
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockOk)
src := v1alpha1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
Repo: &v1alpha1.Repository{},
ApplicationSource: &src,
VerifySignature: true,
SourceIntegrity: sourceIntegrityReqStrict,
ProjectName: "something",
ProjectSourceRepos: []string{"*"},
}
res, err := service.GenerateManifest(t.Context(), &q)
require.NoError(t, err)
assert.Equal(t, testSignature, res.VerifyResult)
assert.True(t, res.SourceIntegrityResult.IsValid())
require.NoError(t, res.SourceIntegrityResult.AsError())
}
// Commit with signature and verification not requested
{
service := newServiceWithSignature(t, "../../manifests/base")
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockOk)
src := v1alpha1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something",
Repo: &v1alpha1.Repository{},
ApplicationSource: &src,
SourceIntegrity: nil,
ProjectName: "something",
ProjectSourceRepos: []string{"*"},
}
res, err := service.GenerateManifest(t.Context(), &q)
require.NoError(t, err)
assert.Empty(t, res.VerifyResult)
assert.Nil(t, res.SourceIntegrityResult)
gitClient.AssertNotCalled(t, "LsSignatures", mock.Anything, mock.Anything)
}
// Commit without signature and verification requested
{
service := newService(t, "../../manifests/base")
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(LsSignaturesMockGitError)
src := v1alpha1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
Repo: &v1alpha1.Repository{},
ApplicationSource: &src,
SourceIntegrity: sourceIntegrityReqStrict,
ProjectName: "something",
ProjectSourceRepos: []string{"*"},
}
res, err := service.GenerateManifest(t.Context(), &q)
require.NoError(t, err)
assert.Empty(t, res.VerifyResult)
assert.Equal(t, sourceIntegrityResultGitError, res.SourceIntegrityResult)
require.Error(t, res.SourceIntegrityResult.AsError())
}
// Commit without signature and verification not requested
{
service := newService(t, "../../manifests/base")
service, gitClient, _ := newServiceWithMocks(t, "../../manifests/base")
src := v1alpha1.ApplicationSource{Path: "."}
q := apiclient.ManifestRequest{
Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
Repo: &v1alpha1.Repository{},
ApplicationSource: &src,
SourceIntegrity: nil,
ProjectName: "something",
ProjectSourceRepos: []string{"*"},
}
res, err := service.GenerateManifest(t.Context(), &q)
require.NoError(t, err)
assert.Empty(t, res.VerifyResult)
assert.Nil(t, res.SourceIntegrityResult)
gitClient.AssertNotCalled(t, "LsSignatures", mock.Anything, mock.Anything)
}
}
@ -4493,11 +4548,11 @@ func TestErrorGetGitDirectories(t *testing.T) {
Revision: "sadfsadf",
},
}, want: nil, wantErr: assert.Error},
{name: "ErrorVerifyCommit", fields: fields{service: func() *Service {
{name: "ErrorListingSignatures", fields: fields{service: func() *Service {
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
gitClient.EXPECT().Checkout(mock.Anything, mock.Anything, mock.Anything).Return("", nil)
gitClient.EXPECT().LsRemote(mock.Anything).Return("", errors.New("ah error"))
gitClient.EXPECT().VerifyCommitSignature(mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf"))
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).Return([]git.RevisionSignatureInfo{}, errors.New("the thing have exploded"))
gitClient.EXPECT().Root().Return(root)
paths.EXPECT().GetPath(mock.Anything).Return(".", nil)
paths.EXPECT().GetPathIfExists(mock.Anything).Return(".")
@ -4509,7 +4564,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"},
SubmoduleEnabled: false,
Revision: "sadfsadf",
VerifyCommit: true,
SourceIntegrity: sourceIntegrityReqStrict,
},
}, want: nil, wantErr: assert.Error},
}
@ -5396,55 +5451,6 @@ func TestGetRevisionChartDetails(t *testing.T) {
})
}
func TestVerifyCommitSignature(t *testing.T) {
repo := &v1alpha1.Repository{
Repo: "https://github.com/example/repo.git",
}
t.Run("VerifyCommitSignature with valid signature", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
mockGitClient := &gitmocks.Client{}
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
Return(testSignature, nil)
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
require.NoError(t, err)
})
t.Run("VerifyCommitSignature with invalid signature", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
mockGitClient := &gitmocks.Client{}
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
Return("", nil)
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
assert.EqualError(t, err, "revision abcd1234 is not signed")
})
t.Run("VerifyCommitSignature with unknown signature", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
mockGitClient := &gitmocks.Client{}
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
Return("", errors.New("UNKNOWN signature: gpg: Unknown signature from ABCDEFGH"))
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
assert.EqualError(t, err, "UNKNOWN signature: gpg: Unknown signature from ABCDEFGH")
})
t.Run("VerifyCommitSignature with error verifying signature", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
mockGitClient := &gitmocks.Client{}
mockGitClient.EXPECT().VerifyCommitSignature(mock.Anything).
Return("", errors.New("error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature"))
err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
assert.EqualError(t, err, "error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature")
})
t.Run("VerifyCommitSignature with signature verification disabled", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "false")
mockGitClient := &gitmocks.Client{}
err := verifyCommitSignature(false, mockGitClient, "abcd1234", repo)
require.NoError(t, err)
})
}
func Test_GenerateManifests_Commands(t *testing.T) {
t.Run("helm", func(t *testing.T) {
service := newService(t, "testdata/my-chart")

View file

@ -15,6 +15,7 @@ import (
"time"
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
kubecache "github.com/argoproj/argo-cd/gitops-engine/pkg/cache"
"github.com/argoproj/argo-cd/gitops-engine/pkg/diff"
@ -1647,9 +1648,9 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe
}
defer utilio.Close(conn)
return repoClient.GetRevisionMetadata(ctx, &apiclient.RepoServerRevisionMetadataRequest{
Repo: repo,
Revision: q.GetRevision(),
CheckSignature: len(proj.Spec.SignatureKeys) > 0,
Repo: repo,
Revision: q.GetRevision(),
SourceIntegrity: proj.EffectiveSourceIntegrity(),
})
}
@ -2096,9 +2097,8 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR
return nil, status.Error(codes.FailedPrecondition, "sync with replace was disabled on the API Server level via the server configuration")
}
// We cannot use local manifests if we're only allowed to sync to signed commits
if syncReq.Manifests != nil && len(proj.Spec.SignatureKeys) > 0 {
return nil, status.Errorf(codes.FailedPrecondition, "Cannot use local sync when signature keys are required.")
if syncReq.Manifests != nil && sourceintegrity.HasCriteria(proj.EffectiveSourceIntegrity(), a.Spec.GetSources()...) {
return nil, status.Errorf(codes.FailedPrecondition, "Cannot use local manifests when source integrity is enforced")
}
resources := []v1alpha1.SyncOperationResource{}

View file

@ -9,8 +9,8 @@ import (
gpgkeypkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/gpgkey"
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/db"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v3/util/rbac"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
)
// Server provides a service of type GPGKeyService
@ -51,9 +51,9 @@ func (s *Server) Get(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*ap
return nil, err
}
keyID := gpg.KeyID(q.KeyID)
if keyID == "" {
return nil, errors.New("KeyID is malformed or empty")
keyID, err := sourceintegrity.KeyID(q.KeyID)
if err != nil {
return nil, err
}
keys, err := s.db.ListConfiguredGPGPublicKeys(ctx)

View file

@ -154,60 +154,6 @@ func TestNamespacedGetLogsAllowNS(t *testing.T) {
})
}
func TestNamespacedSyncToUnsignedCommit(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
GivenWithNamespace(t, fixture.AppNamespace()).
SetTrackingMethod("annotation").
Project("gpg").
Path(guestbookPath).
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestNamespacedSyncToSignedCommitWKK(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
SetAppNamespace(fixture.AppNamespace()).
SetTrackingMethod("annotation").
Project("gpg").
Path(guestbookPath).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestNamespacedSyncToSignedCommitKWKK(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
SetAppNamespace(fixture.AppNamespace()).
SetTrackingMethod("annotation").
Project("gpg").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
}
func TestNamespacedAppCreation(t *testing.T) {
ctx := Given(t)
ctx.

View file

@ -0,0 +1,426 @@
package e2e
import (
"testing"
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
. "github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
"github.com/stretchr/testify/assert"
. "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/test/e2e/fixture"
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app"
)
var projectWithNoKeys = AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}},
SourceIntegrity: &SourceIntegrity{
Git: &SourceIntegrityGit{
Policies: []*SourceIntegrityGitPolicy{{
Repos: []SourceIntegrityGitPolicyRepo{{URL: "*"}},
GPG: &SourceIntegrityGitPolicyGPG{
Keys: []string{}, // Verifying but permitting no keys
Mode: "head",
},
}},
},
},
}
func TestSyncToUnsignedCommit(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Path(guestbookPath).
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, " unsigned (key_id=)"))
}
func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Path(guestbookPath).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
}
func TestSyncToSignedCommitWithKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestSyncToSignedCommitWithUnallowedKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
ProjectSpec(projectWithNoKeys).
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "signed with unallowed key (key_id="+fixture.GpgGoodKeyID+")"))
}
func TestSyncToSignedBranchWithKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Path(guestbookPath).
Revision("master").
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestSyncToSignedBranchWithUnknownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Path(guestbookPath).
Revision("master").
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
}
func TestSyncToUnsignedBranch(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Revision("master").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddFile("test.yaml", "TestSyncToUnsignedBranch").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)"))
}
func TestSyncToSignedTagWithKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Revision("signed-tag").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedTag("signed-tag").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestSyncToSignedTagWithUnknownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Revision("signed-tag").
Path(guestbookPath).
Sleep(2).
When().
AddSignedTag("signed-tag").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision signed-tag by ")).
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
}
func TestSyncToUnsignedAnnotatedTag(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Revision("unsigned-tag").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
// Signed commit with an unsigned annotated tag will validate the tag signature
AddSignedFile("test.yaml", "null").
AddAnnotatedTag("unsigned-tag", "message goes here").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision unsigned-tag by ")).
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)"))
}
func TestSyncToUnsignedSimpleTag(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Revision("unsigned-simple-tag").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
// Signed commit with an unsigned not-annotated tag will validate the commit, not the tag
AddSignedFile("test.yaml", "null").
AddTag("unsigned-simple-tag").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestSyncToSignedAnnotatedTagWithUnallowedKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
ProjectSpec(projectWithNoKeys).
Revision("v1.0").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddFile("test.yaml", "null").
AddSignedTag("v1.0").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision v1.0")).
Expect(Condition(ApplicationConditionComparisonError, "signed with unallowed key (key_id="+fixture.GpgGoodKeyID+")"))
}
func TestSyncToTagBasedConstraint(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Revision("1.*").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
AddSignedTag("1.0").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestNamespacedSyncToUnsignedCommit(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
GivenWithNamespace(t, fixture.AppNamespace()).
SetTrackingMethod("annotation").
Project("gpg").
Path(guestbookPath).
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision ")).
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)"))
}
func TestNamespacedSyncToSignedCommitWithUnknownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
SetAppNamespace(fixture.AppNamespace()).
SetTrackingMethod("annotation").
Project("gpg").
Path(guestbookPath).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: Failed verifying revision ")).
Expect(Condition(ApplicationConditionComparisonError, "signed with key not in keyring (key_id="+fixture.GpgGoodKeyID+")"))
}
func TestNamespacedSyncToSignedCommit(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
SetAppNamespace(fixture.AppNamespace()).
SetTrackingMethod("annotation").
Project("gpg").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestLocalManifestRejectedWithSourceIntegrity(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
Given(t).
Project("gpg").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
CreateApp().
Sync().
Then().
And(func(app *Application) {
res, _ := fixture.RunCli("app", "manifests", app.Name)
assert.Contains(t, res, "containerPort: 80")
assert.Contains(t, res, "image: quay.io/argoprojlabs/argocd-e2e-container:0.2")
}).
Given().
LocalPath(guestbookPathLocal).
When().
IgnoreErrors().
Sync("--local-repo-root", ".").
Then().
Expect(ErrorRegex("", "Cannot use local manifests when source integrity is enforced"))
}
func TestOCISourceIgnoredWithSourceIntegrity(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
fixture.EnsureCleanState(t)
// No keys in keyring, no keys in project, OCI is not git, yet source integrity is defined.
// Expecting some of that would cause visible failure if the source integrity should be applied
Given(t).
Project("gpg").
ProjectSpec(appProjectWithSourceIntegrity()).
HTTPSInsecureRepoURLWithClientCertAdded().
PushImageToOCIRegistry("testdata/guestbook", "1.0.0").
OCIRepoAdded("my-oci-repo", "guestbook").
OCIRegistry(fixture.OCIHostURL).
OCIRegistryPath("guestbook").
RepoURLType(fixture.RepoURLTypeOCI).
Revision("1.0.0").
Path(".").
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
// Verify local manifests are permitted - source integrity criteria for git should not apply for oci
Given().
LocalPath(guestbookPathLocal).
When().
DoNotIgnoreErrors().
Sync("--local-repo-root", ".", "--force", "--prune")
}

View file

@ -159,166 +159,6 @@ func TestGetLogsAllow(t *testing.T) {
})
}
func TestSyncToUnsignedCommit(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToSignedCommitWithKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
}
func TestSyncToSignedBranchWithKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
Revision("master").
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
}
func TestSyncToSignedBranchWithUnknownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Path(guestbookPath).
Revision("master").
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToUnsignedBranch(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Revision("master").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToSignedTagWithKnownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Revision("signed-tag").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedTag("signed-tag").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
}
func TestSyncToSignedTagWithUnknownKey(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Revision("signed-tag").
Path(guestbookPath).
Sleep(2).
When().
AddSignedTag("signed-tag").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToUnsignedTag(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
Given(t).
Project("gpg").
Revision("unsigned-tag").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddTag("unsigned-tag").
IgnoreErrors().
CreateApp().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestAppCreation(t *testing.T) {
ctx := Given(t)
ctx.

View file

@ -0,0 +1,157 @@
package e2e
import (
"testing"
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/common"
"k8s.io/utils/ptr"
. "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture"
. "github.com/argoproj/argo-cd/v3/test/e2e/fixture/app"
)
var oneShotSync = func(app *Application) {
app.Spec.SyncPolicy = &SyncPolicy{
Automated: &SyncPolicyAutomated{SelfHeal: ptr.To(true)},
Retry: &RetryStrategy{Limit: 0},
}
}
func appProjectWithSourceIntegrity(keys ...string) AppProjectSpec {
if keys == nil {
keys = []string{}
}
return AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}},
SourceIntegrity: &SourceIntegrity{
Git: &SourceIntegrityGit{
Policies: []*SourceIntegrityGitPolicy{{
Repos: []SourceIntegrityGitPolicyRepo{{URL: "*"}},
GPG: &SourceIntegrityGitPolicyGPG{
Keys: keys,
Mode: SourceIntegrityGitPolicyGPGModeHead,
},
}},
},
},
}
}
func TestMultiSourceSourceIntegrityAllFailed(t *testing.T) {
SkipOnEnv(t, "GPG")
EnsureCleanState(t)
sources := []ApplicationSource{{
RepoURL: RepoURL(RepoURLTypeFile),
Path: guestbookPath,
Name: "uno",
}, {
RepoURL: RepoURL(RepoURLTypeFile),
Path: "two-nice-pods",
}}
Given(t).
Project("gpg").
ProjectSpec(appProjectWithSourceIntegrity(GpgGoodKeyID)).
GPGPublicKeyAdded().
Sleep(2).
Sources(sources).
When().
IgnoreErrors().
CreateMultiSourceAppFromFile(oneShotSync).
Sync().
Then().
Expect(OperationPhaseIs(common.OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source uno: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source 2 of 2: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "unsigned (key_id=)")).
// Should start passing after project update
Given().
When().
AddSignedFile("fake.yaml", "change"). // Needs a new commit to avoid using cached manifests
IgnoreErrors().
Sync().
Then().
Expect(OperationPhaseIs(common.OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions())
}
func TestMultiSourceSourceIntegritySomeFailed(t *testing.T) {
SkipOnEnv(t, "GPG")
EnsureCleanState(t)
sources := []ApplicationSource{{
RepoURL: RepoURL(RepoURLTypeFile),
Path: guestbookPath,
Name: "guestbook",
}, {
RepoURL: "https://github.com/argoproj/argocd-example-apps",
Path: "blue-green",
TargetRevision: "53e28ff20cc530b9ada2173fbbd64d48338583ba", // picking a precise commit so tests have a known signature
Name: "blue-green",
}}
message := "GIT/GPG: source blue-green: Failed verifying revision 53e28ff20cc530b9ada2173fbbd64d48338583ba by 'May Zhang <may_zhang@intuit.com>': signed with key not in keyring (key_id=4AEE18F83AFDEB23)"
Given(t).
Project("gpg").
ProjectSpec(appProjectWithSourceIntegrity(GpgGoodKeyID)).
Sources(sources).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("fake.yaml", "").
IgnoreErrors().
CreateMultiSourceAppFromFile(oneShotSync).
Then().
Expect(OperationPhaseIs(common.OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing)).
Expect(Condition(ApplicationConditionComparisonError, message))
}
func TestMultiSourceSourceIntegrityAllValid(t *testing.T) {
SkipOnEnv(t, "GPG")
EnsureCleanState(t)
sources := []ApplicationSource{{
RepoURL: RepoURL(RepoURLTypeFile),
Path: guestbookPath,
Name: "valid",
}, {
RepoURL: RepoURL(RepoURLTypeFile),
Path: ".",
Name: "also-valid",
}}
Given(t).
Project("gpg").
ProjectSpec(appProjectWithSourceIntegrity(GpgGoodKeyID)).
Sources(sources).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("fake.yaml", "").
IgnoreErrors().
CreateMultiSourceAppFromFile(oneShotSync).
Sync().
Then().
Expect(OperationPhaseIs(common.OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(NoConditions()).
// Should start failing after key removal
Given().
GPGPublicKeyRemoved().
When().
AddSignedFile("fake.yaml", "change"). // Needs a new commit to avoid using cached manifests
IgnoreErrors().
Sync().
Then().
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source valid: Failed verifying revision")).
Expect(Condition(ApplicationConditionComparisonError, "GIT/GPG: source also-valid: Failed verifying revision"))
}

View file

@ -25,7 +25,7 @@ func TestMultiSourceAppCreation(t *testing.T) {
ctx.
Sources(sources).
When().
CreateMultiSourceAppFromFile().
CreateMultiSourceApp().
Then().
And(func(app *Application) {
assert.Equal(t, ctx.GetName(), app.Name)
@ -80,7 +80,7 @@ func TestMultiSourceAppWithHelmExternalValueFiles(t *testing.T) {
ctx.
Sources(sources).
When().
CreateMultiSourceAppFromFile().
CreateMultiSourceApp().
Then().
And(func(app *Application) {
assert.Equal(t, ctx.GetName(), app.Name)
@ -132,7 +132,7 @@ func TestMultiSourceAppWithSourceOverride(t *testing.T) {
ctx.
Sources(sources).
When().
CreateMultiSourceAppFromFile().
CreateMultiSourceApp().
Then().
And(func(app *Application) {
assert.Equal(t, ctx.GetName(), app.Name)
@ -186,7 +186,7 @@ func TestMultiSourceAppWithSourceName(t *testing.T) {
ctx.
Sources(sources).
When().
CreateMultiSourceAppFromFile().
CreateMultiSourceApp().
Then().
And(func(app *Application) {
assert.Equal(t, ctx.GetName(), app.Name)
@ -248,7 +248,7 @@ func TestMultiSourceAppSetWithSourceName(t *testing.T) {
ctx.
Sources(sources).
When().
CreateMultiSourceAppFromFile().
CreateMultiSourceApp().
Then().
And(func(app *Application) {
assert.Equal(t, ctx.GetName(), app.Name)
@ -271,7 +271,7 @@ func TestMultiSourceAppSetWithSourceName(t *testing.T) {
})
}
func TestMultiSourceApptErrorWhenSourceNameAndSourcePosition(t *testing.T) {
func TestMultiSourceAppErrorWhenSourceNameAndSourcePosition(t *testing.T) {
sources := []ApplicationSource{{
RepoURL: RepoURL(RepoURLTypeFile),
Path: guestbookPath,
@ -285,7 +285,7 @@ func TestMultiSourceApptErrorWhenSourceNameAndSourcePosition(t *testing.T) {
ctx.
Sources(sources).
When().
CreateMultiSourceAppFromFile().
CreateMultiSourceApp().
Then().
Expect(Event(EventReasonResourceCreated, "create")).
And(func(_ *Application) {

View file

@ -3,6 +3,7 @@ package e2e
import (
"crypto/rand"
"encoding/hex"
"regexp"
"strings"
"testing"
"time"
@ -245,27 +246,10 @@ func TestSimpleGitDirectoryGeneratorGoTemplate(t *testing.T) {
func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
expectedErrorMessage := `error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = permission denied`
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
}
fixture.EnsureCleanState(t)
expectedErrorMessage := regexp.MustCompile(
`error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': unsigned \(key_id=\)`,
)
generateExpectedApp := func(name string) v1alpha1.Application {
return v1alpha1.Application{
TypeMeta: metav1.TypeMeta{
@ -299,6 +283,8 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
Given(t).
When().
// Create an unsigned local commit not to rely on whatever is in the repo's HEAD
AddFile("test.yaml", randStr(t)).
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{
Spec: v1alpha1.ApplicationSetSpec{
@ -320,7 +306,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
RepoURL: fixture.RepoURL("file://"),
Directories: []v1alpha1.GitDirectoryGeneratorItem{
{
Path: guestbookPath,
@ -333,34 +319,34 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
}).
Then().Expect(ApplicationsDoNotExist(expectedApps)).
// verify the ApplicationSet error status conditions were set correctly
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionErrorOccurred,
v1alpha1.ApplicationSetConditionStatusTrue,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionParametersGenerated,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionResourcesUpToDate,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
When().
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
}
func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
expectedErrorMessage := `error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = permission denied`
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
}
fixture.EnsureCleanState(t)
expectedErrorMessage := regexp.MustCompile(
`error generating params from git: error getting directories from repo: error retrieving Git Directories: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': signed with key not in keyring \(key_id=` + fixture.GpgGoodKeyID + `\)`,
)
generateExpectedApp := func(name string) v1alpha1.Application {
return v1alpha1.Application{
TypeMeta: metav1.TypeMeta{
@ -396,7 +382,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
Given(t).
Path(guestbookPath).
When().
AddSignedFile("test.yaml", randStr(t)).IgnoreErrors().
AddSignedFile("test.yaml", randStr(t)).
IgnoreErrors().
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{
@ -423,7 +409,7 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
RepoURL: fixture.RepoURL("file://"),
Directories: []v1alpha1.GitDirectoryGeneratorItem{
{
Path: guestbookPath,
@ -435,7 +421,24 @@ func TestSimpleGitDirectoryGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
},
}).Then().
// verify the ApplicationSet error status conditions were set correctly
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionErrorOccurred,
v1alpha1.ApplicationSetConditionStatusTrue,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionParametersGenerated,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionResourcesUpToDate,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
Expect(ApplicationsDoNotExist(expectedApps)).
When().
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
@ -549,27 +552,9 @@ func TestSimpleGitFilesGenerator(t *testing.T) {
func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
expectedErrorMessage := `error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = permission denied`
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
}
fixture.EnsureCleanState(t)
expectedErrorMessage := regexp.MustCompile(`error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': unsigned \(key_id=\)`)
project := "gpg"
generateExpectedApp := func(name string) v1alpha1.Application {
return v1alpha1.Application{
@ -597,15 +582,21 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
}
}
expectedApps := []v1alpha1.Application{
unexpectedApps := []v1alpha1.Application{
generateExpectedApp("engineering-dev-guestbook"),
generateExpectedApp("engineering-prod-guestbook"),
}
Given(t).
Path(guestbookPath).
When().
// Create an unsigned local commit not to rely on whatever is in the repo's HEAD
AddFile("test.yaml", randStr(t)).
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "simple-git-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.name}}-guestbook"},
@ -625,7 +616,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/applicationset.git",
RepoURL: fixture.RepoURL("file://"),
Files: []v1alpha1.GitFileGeneratorItem{
{
Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
@ -635,36 +626,37 @@ func TestSimpleGitFilesGeneratorGPGEnabledUnsignedCommits(t *testing.T) {
},
},
},
}).Then().Expect(ApplicationsDoNotExist(expectedApps)).
// verify the ApplicationSet error status conditions were set correctly
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
When().
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))
}).
Then().Expect(ApplicationsDoNotExist(unexpectedApps)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionErrorOccurred,
v1alpha1.ApplicationSetConditionStatusTrue,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionParametersGenerated,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionResourcesUpToDate,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
When().Delete(metav1.DeletePropagationForeground).
Then().Expect(ApplicationsDoNotExist(unexpectedApps))
}
func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
fixture.SkipOnEnv(t, "GPG")
expectedErrorMessage := `error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = permission denied`
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
}
fixture.EnsureCleanState(t)
expectedErrorMessage := regexp.MustCompile(
`error generating params from git: error retrieving Git files: rpc error: code = Unknown desc = GIT/GPG: Failed verifying revision .* by '.*': signed with key not in keyring \(key_id=` + fixture.GpgGoodKeyID + `\)`,
)
project := "gpg"
generateExpectedApp := func(name string) v1alpha1.Application {
return v1alpha1.Application{
@ -700,7 +692,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
Given(t).
Path(guestbookPath).
When().
AddSignedFile("test.yaml", randStr(t)).IgnoreErrors().
AddSignedFile("test.yaml", randStr(t)).
IgnoreErrors().
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{
@ -723,7 +715,7 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/applicationset.git",
RepoURL: fixture.RepoURL("file://"),
Files: []v1alpha1.GitFileGeneratorItem{
{
Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
@ -735,7 +727,24 @@ func TestSimpleGitFilesGeneratorGPGEnabledWithoutKnownKeys(t *testing.T) {
},
}).Then().
// verify the ApplicationSet error status conditions were set correctly
Expect(ApplicationSetHasConditions(expectedConditionsParamsError)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionErrorOccurred,
v1alpha1.ApplicationSetConditionStatusTrue,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionParametersGenerated,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
Expect(ApplicationSetHasCondition(
v1alpha1.ApplicationSetConditionResourcesUpToDate,
v1alpha1.ApplicationSetConditionStatusFalse,
expectedErrorMessage,
v1alpha1.ApplicationSetReasonErrorOccurred,
)).
Expect(ApplicationsDoNotExist(expectedApps)).
When().
Delete(metav1.DeletePropagationForeground).Then().Expect(ApplicationsDoNotExist(expectedApps))

View file

@ -183,8 +183,15 @@ func (a *Actions) CreateFromFile(handler func(app *v1alpha1.Application), flags
return a
}
func (a *Actions) CreateMultiSourceAppFromFile(flags ...string) *Actions {
func (a *Actions) CreateMultiSourceApp(flags ...string) *Actions {
a.context.T().Helper()
return a.CreateMultiSourceAppFromFile(func(_ *v1alpha1.Application) {}, flags...)
}
func (a *Actions) CreateMultiSourceAppFromFile(handler func(app *v1alpha1.Application), flags ...string) *Actions {
a.context.T().Helper()
app := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: a.context.AppName(),
@ -205,6 +212,8 @@ func (a *Actions) CreateMultiSourceAppFromFile(flags ...string) *Actions {
},
}
handler(app)
data := grpc.MustMarshal(app)
tmpFile, err := os.CreateTemp("", "")
require.NoError(a.context.T(), err)

View file

@ -233,6 +233,19 @@ func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
// AppSet name is not configurable and should always be unique, based on the context name
appSet.Name = a.context.GetName()
// Tests running in short succession using the same dummy generator URL, can cause repo-server to pull revisions
// from gitRefCache effectively using the repo from a previous test when used before the cache expiry. This prevents
// using the reference cache on appset creation to avoid that.
for _, generator := range appSet.Spec.Generators {
if generator.Git != nil {
if appSet.Annotations == nil {
appSet.Annotations = map[string]string{}
}
appSet.Annotations[common.AnnotationApplicationSetRefresh] = "true"
break
}
}
newResource, err := appSetClientSet.Create(context.Background(), utils.MustToUnstructured(&appSet), metav1.CreateOptions{})
if err == nil {
@ -562,6 +575,12 @@ func (a *Actions) runCli(args ...string) {
a.verifyAction()
}
func (a *Actions) AddFile(fileName, fileContents string) *Actions {
a.context.T().Helper()
fixture.AddFile(a.context.T(), a.context.path+"/"+fileName, fileContents)
return a
}
func (a *Actions) AddSignedFile(fileName, fileContents string) *Actions {
a.context.T().Helper()
fixture.AddSignedFile(a.context.T(), a.context.path+"/"+fileName, fileContents)

View file

@ -3,6 +3,7 @@ package applicationsets
import (
"fmt"
"reflect"
"regexp"
"slices"
"strings"
"testing"
@ -119,6 +120,23 @@ func ApplicationSetHasConditions(expectedConditions []v1alpha1.ApplicationSetCon
}
}
func ApplicationSetHasCondition(expType v1alpha1.ApplicationSetConditionType, expStatus v1alpha1.ApplicationSetConditionStatus, expMessage *regexp.Regexp, expReason string) Expectation {
return func(c *Consequences) (state, string) {
foundApplicationSet := c.applicationSet(c.context.GetName())
if foundApplicationSet == nil {
return pending, fmt.Sprintf("application set '%s' not found", c.context.GetName())
}
got := foundApplicationSet.Status.Conditions
message := fmt.Sprintf("condition {%s %s %s %s} in %v", expType, expMessage, expStatus, expReason, got)
for _, condition := range got {
if expType == condition.Type && expStatus == condition.Status && expReason == condition.Reason && expMessage.MatchString(condition.Message) {
return succeeded, message
}
}
return pending, message
}
}
// ApplicationsDoNotExist checks that each of the 'expectedApps' no longer exist in the namespace
func ApplicationsDoNotExist(expectedApps []v1alpha1.Application) Expectation {
return func(c *Consequences) (state, string) {

View file

@ -536,7 +536,6 @@ func SetAccounts(accounts map[string][]string) error {
func SetPermissions(permissions []ACL, username string, roleName string) error {
return updateRBACConfigMap(func(cm *corev1.ConfigMap) error {
var aclstr strings.Builder
for _, permission := range permissions {
_, _ = fmt.Fprintf(&aclstr, "p, role:%s, %s, %s, %s, allow \n", roleName, permission.Resource, permission.Action, permission.Scope)
}

View file

@ -12,7 +12,7 @@ import (
"github.com/argoproj/argo-cd/v3/util/errors"
)
// Add GPG public key via API and create appropriate file where the ConfigMap mount would de it as well
// AddGPGPublicKey adds public key via API and creates the appropriate file where the ConfigMap mount would do it as well
func AddGPGPublicKey(t *testing.T) {
t.Helper()
keyPath, err := filepath.Abs("../fixture/gpg/" + fixture.GpgGoodKeyID)

View file

@ -23,8 +23,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"github.com/argoproj/argo-cd/v3/util/gpg"
argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/typed/application/v1alpha1"
applicationsv1 "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
@ -862,11 +860,6 @@ func verifyGenerateManifests(
continue
}
verifySignature := false
if len(proj.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
verifySignature = true
}
repos := helmRepos
helmRepoCreds := repositoryCredentials
// If the source is OCI, there is a potential for an OCI image to be a Helm chart and that said chart in
@ -887,7 +880,6 @@ func verifyGenerateManifests(
Proxy: repoRes.Proxy,
NoProxy: repoRes.NoProxy,
},
VerifySignature: verifySignature,
Repos: repos,
Revision: source.TargetRevision,
AppName: app.Name,
@ -898,6 +890,7 @@ func verifyGenerateManifests(
KubeVersion: kubeVersion,
ApiVersions: apiVersions,
HelmOptions: helmOptions,
SourceIntegrity: proj.EffectiveSourceIntegrity(),
HelmRepoCreds: helmRepoCreds,
TrackingMethod: trackingMethod,
EnabledSourceTypes: enableGenerateManifests,

View file

@ -10,7 +10,7 @@ import (
"github.com/argoproj/argo-cd/v3/common"
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
)
// Validates a single GnuPG key and returns the key's ID
@ -32,7 +32,7 @@ func validatePGPKey(keyData string) (*appsv1.GnuPGPublicKey, error) {
}
}()
parsed, err := gpg.ValidatePGPKeys(f.Name())
parsed, err := sourceintegrity.ValidatePGPKeys(f.Name())
if err != nil {
return nil, err
}
@ -69,9 +69,9 @@ func (db *db) ListConfiguredGPGPublicKeys(_ context.Context) (map[string]*appsv1
// This is not optimal, but the executil from argo-pkg does not support writing to
// stdin of the forked process. So for now, we must live with that.
for k, p := range keysCM.Data {
expectedKeyID := gpg.KeyID(k)
if expectedKeyID == "" {
return nil, fmt.Errorf("found entry with key '%s' in ConfigMap, but this is not a valid PGP key ID", k)
expectedKeyID, err := sourceintegrity.KeyID(k)
if err != nil {
return nil, fmt.Errorf("found invalid entry with key '%s' in ConfigMap: %s", k, err.Error())
}
parsedKey, err := validatePGPKey(p)
if err != nil {
@ -91,7 +91,7 @@ func (db *db) AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*
result := make(map[string]*appsv1.GnuPGPublicKey)
skipped := make([]string, 0)
keys, err := gpg.ValidatePGPKeysFromString(keyData)
keys, err := sourceintegrity.ValidatePGPKeysFromString(keyData)
if err != nil {
return nil, nil, err
}

View file

@ -11,8 +11,8 @@ import (
"k8s.io/client-go/kubernetes/fake"
"github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v3/util/gpg/testdata"
"github.com/argoproj/argo-cd/v3/util/settings"
"github.com/argoproj/argo-cd/v3/util/sourceintegrity/testdata"
)
// GPG config map with a single key and good mapping

View file

@ -4,9 +4,11 @@ import (
"bufio"
"context"
"crypto/tls"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/mail"
@ -124,6 +126,7 @@ type gitRefCache interface {
// Client is a generic git client interface
type Client interface {
Root() string
RepoURL() string
Init() error
Fetch(revision string, depth int64) error
Submodule() error
@ -134,8 +137,15 @@ type Client interface {
LsLargeFiles() ([]string, error)
CommitSHA() (string, error)
RevisionMetadata(revision string) (*RevisionMetadata, error)
// Deprecated: To be removed in the next major version when Signature verification is replaced with Source Integrity.
VerifyCommitSignature(string) (string, error)
IsAnnotatedTag(string) bool
// IsAnnotatedTag determines if the revision is, or resolves to an annotated tag.
IsAnnotatedTag(revision string) bool
// LsSignatures gets a list of revisions including their GPG signature info.
// If revision is an annotated tag or a semantic constraint matching an annotated tag, its signature is reported as well
// If deep==true, list the commits backwards in history until a signed "seal commit" or repo init commit. The listing includes those seal commits.
// If deep==false, examines the revision only. Checking the annotated tag signature if the revision is an annotated tag, commit signature otherwise.
LsSignatures(revision string, deep bool) ([]RevisionSignatureInfo, error)
ChangedFiles(revision string, targetRevision string) ([]string, error)
IsRevisionPresent(revision string) bool
// SetAuthor sets the author name and email in the git configuration.
@ -430,6 +440,10 @@ func (m *nativeGitClient) Root() string {
return m.root
}
func (m *nativeGitClient) RepoURL() string {
return m.repoURL
}
// Init initializes a local git repository and sets the remote origin
func (m *nativeGitClient) Init() error {
_, err := git.PlainOpen(m.root)
@ -967,8 +981,11 @@ func updateCommitMetadata(logCtx *log.Entry, relatedCommit *CommitMetadata, line
}
// VerifyCommitSignature Runs verify-commit on a given revision and returns the output
//
// Deprecated: To be removed in the next major version when Signature verification is replaced with Source Integrity.
func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error) {
out, err := m.runGnuPGWrapper(context.Background(), "git-verify-wrapper.sh", revision)
cmd := m.cmdWithGPG(context.Background(), "git-verify-wrapper.sh", revision)
out, err := m.runCmdOutput(cmd, runOpts{})
if err != nil {
log.Errorf("error verifying commit signature: %v", err)
return "", errors.New("permission denied")
@ -976,11 +993,314 @@ func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error)
return out, nil
}
// IsAnnotatedTag returns true if the revision points to an annotated tag
type (
GPGVerificationResult string
RevisionSignatureInfo struct {
Revision string
VerificationResult GPGVerificationResult
SignatureKeyID string
Date string
AuthorIdentity string
}
)
const (
GPGVerificationResultGood GPGVerificationResult = "signed" // All good
GPGVerificationResultBad GPGVerificationResult = "bad signature" // Not able to cryptographically verify signature
GPGVerificationResultUntrusted GPGVerificationResult = "signed with untrusted key" // The trust level of the key in the gpg keyring is not sufficient
GPGVerificationResultExpiredSignature GPGVerificationResult = "expired signature" // Signature have expired
GPGVerificationResultExpiredKey GPGVerificationResult = "signed with expired key" // Signed with a key expired at the time of the signing
GPGVerificationResultRevokedKey GPGVerificationResult = "signed with revoked key" // Signed with a key that is revoked
GPGVerificationResultMissingKey GPGVerificationResult = "signed with key not in keyring" // The key used to sign was not added to the gpg keyring
GPGVerificationResultUnsigned GPGVerificationResult = "unsigned" // Commit it not signed at all
)
func gpgVerificationFromGpgCode(gpgCode string) GPGVerificationResult {
// GPG code presented by `git verify-tag --raw`
// https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes
switch gpgCode {
case "GOODSIG":
return GPGVerificationResultGood
case "BADSIG":
return GPGVerificationResultBad
case "EXPSIG":
return GPGVerificationResultExpiredSignature
case "EXPKEYSIG":
return GPGVerificationResultExpiredKey
case "REVKEYSIG":
return GPGVerificationResultRevokedKey
case "ERRSIG":
return GPGVerificationResultMissingKey
default:
panic(fmt.Sprintf("Unable to parse VerificationResult from '%s'", gpgCode))
}
}
func gpgVerificationFromGitRevParse(oneLetter string) GPGVerificationResult {
// The letters each represent a given verification result, as output by git rev-parse pretty format.
// See PRETTY FORMAT in git-rev-list(1) for more information.
// https://github.com/git/git/blob/5e6e4854e086ba0025bc7dc11e6b475c92a2f556/gpg-interface.c#L188
switch oneLetter {
case "G":
return GPGVerificationResultGood
case "B":
return GPGVerificationResultBad
case "U":
return GPGVerificationResultUntrusted
case "X":
return GPGVerificationResultExpiredSignature
case "Y":
return GPGVerificationResultExpiredKey
case "R":
return GPGVerificationResultRevokedKey
case "E":
return GPGVerificationResultMissingKey
case "N":
return GPGVerificationResultUnsigned
default:
panic(fmt.Sprintf("Unable to parse VerificationResult from '%s'", oneLetter))
}
}
var gpgKeyIdRegexp = regexp.MustCompile("[0-9a-zA-Z]{16}")
func (m *nativeGitClient) tagSignature(tagRevision string) (*RevisionSignatureInfo, error) {
ctx := context.Background()
// Unlike for commits, there is no elegant way to slurp all signature info for tag. So this extracts details needed
// for RevisionSignatureInfo from 2 different git invocations.
cmd := m.cmdWithGPG(ctx, "git", "for-each-ref", "refs/tags/"+tagRevision, `--format=%(taggerdate),%(taggername) "%(taggeremail)"`)
tagOut, err := m.runCmdOutput(cmd, runOpts{})
if err != nil {
return nil, err
}
if tagOut == "" {
return nil, fmt.Errorf("no tag found: %q", tagRevision)
}
tagInfo := strings.Split(tagOut, ",")
if len(tagInfo) != 2 {
return nil, fmt.Errorf("failed to parse tag %q for revisions %q", tagOut, tagRevision)
}
cmd = m.cmdWithGPG(ctx, "git", "verify-tag", tagRevision, "--raw")
tagGpgOut, err := m.runCmdOutput(cmd, runOpts{
CaptureStderr: true, // The structured --raw output is printed to stderr only
SkipErrorLogging: true, // Unsigned returns rc=1
})
status, keyId, err := evaluateGpgSignStatus(err, tagGpgOut)
if err != nil {
return nil, fmt.Errorf("gpg failed verifying git tag %q: %s", tagRevision, err.Error())
}
info, err := newRevisionSignatureInfo(tagRevision, status, keyId, tagInfo[0], tagInfo[1])
if err != nil {
return nil, fmt.Errorf("failed building revision gpg signature info for tag %q: %s", tagRevision, err.Error())
}
return info, err
}
func evaluateGpgSignStatus(cmdErr error, tagGpgOut string) (result GPGVerificationResult, keyId string, err error) {
if cmdErr != nil {
// Commit is not signed
if tagGpgOut == "error: no signature found" {
return GPGVerificationResultUnsigned, "", nil
}
// Parse the output to extract info, ERRSIG causes `rc!=0`
if !strings.Contains(tagGpgOut, "[GNUPG:] ERRSIG ") {
return "", "", cmdErr
}
}
// https://github.com/gpg/gnupg/blob/master/doc/DETAILS#general-status-codes
re := regexp.MustCompile(`\[GNUPG:] (GOODSIG|BADSIG|EXPSIG|EXPKEYSIG|REVKEYSIG|ERRSIG) ([0-9A-F]+) `)
for line := range strings.Lines(tagGpgOut) {
match := re.FindAllStringSubmatch(line, -1)
switch len(match) {
case 0:
continue
case 1:
return gpgVerificationFromGpgCode(match[0][1]), match[0][2], nil
default:
return "", "", fmt.Errorf("too many matches parsing line %q", line)
}
}
return "", "", fmt.Errorf("unexpected `git verify-tag --raw` output: %q", tagGpgOut)
}
func (m *nativeGitClient) LsSignatures(unresolvedRevision string, deep bool) ([]RevisionSignatureInfo, error) {
// Resolve eventual semantic tag constraint before annotated tag detection
if versions.IsConstraint(unresolvedRevision) {
refs, err := m.getRefs()
if err != nil {
return nil, err
}
unresolvedRevision, err = versions.MaxVersion(unresolvedRevision, getGitTags(refs))
if err != nil {
return nil, err
}
}
var signatures []RevisionSignatureInfo
if m.IsAnnotatedTag(unresolvedRevision) {
signature, err := m.tagSignature(unresolvedRevision)
if err != nil {
return nil, err
}
signatures = append(signatures, *signature)
// Check just the annotated tag
if !deep {
return signatures, nil
}
}
commitSignaturesRawOut, err := m.listRawSignatures(deep)
if err != nil {
return nil, err
}
// Final LF will be cut by executil
csvR := csv.NewReader(strings.NewReader(commitSignaturesRawOut))
for {
r, err := csvR.Read()
// EOF means parsing had ended
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if len(r) < 5 {
return nil, fmt.Errorf("invalid rev-list output for %q (fields=%d)", unresolvedRevision, len(r))
}
revision := r[0]
signatureInfo, err := newRevisionSignatureInfo(revision, gpgVerificationFromGitRevParse(r[1]), r[2], r[3], r[4])
if err != nil {
return nil, fmt.Errorf("failed building revision gpg signature info for %q at %q: %s", unresolvedRevision, revision, err.Error())
}
signatures = append(signatures, *signatureInfo)
}
return signatures, nil
}
// newRevisionSignatureInfo builds valid RevisionSignatureInfo
func newRevisionSignatureInfo(revision string, verificationResult GPGVerificationResult, signatureKeyID string, date string, authorIdentity string) (*RevisionSignatureInfo, error) {
if revision == "" {
return nil, errors.New("no revision specified")
}
if date == "" {
return nil, errors.New("no date specified")
}
if authorIdentity == "" {
return nil, errors.New("no author specified")
}
// Unsigned have no key ID, other states must have key ID
if verificationResult == GPGVerificationResultUnsigned {
if signatureKeyID != "" {
return nil, fmt.Errorf("a gpg signing key id %q specified for unsigned commit", signatureKeyID)
}
} else {
if !gpgKeyIdRegexp.MatchString(signatureKeyID) {
return nil, fmt.Errorf("invalid gpg signing key %q", signatureKeyID)
}
}
return &RevisionSignatureInfo{
Revision: revision,
VerificationResult: verificationResult,
SignatureKeyID: signatureKeyID,
Date: date,
AuthorIdentity: authorIdentity,
}, nil
}
func (m *nativeGitClient) listRawSignatures(deep bool) (string, error) {
revisionSha, err := m.CommitSHA()
if err != nil {
return "", err
}
ctx := context.Background()
// This is using a two-step approach to solve the following problem: find all ancestors of a given revision in git history DAG,
// stopping on a signed seal commit, or an init commit. Note there might be multiple seal commits that separate the revision
// form init commit in case the history merged past the most recent seal commit.
//
// 1) Find all seal commits based on the trailer in their message. This searches the entire git history, which is unnecessary,
// but there does not seem to be a decent way to stop on the most recent seal commits in each branch with a single git invocation.
// Found commits are later eliminated to the correctly signed and trusted ones - this is to make sure that unsigned
// or untrusted commits with a seal trailer do not stop the history verification.
// 2) Find all the ancestor commits from the given revision stopping on any of the identified seal commits.
// See git-rev-list(1) for description of the format string
var commitFilterArgs []string
if deep {
// Find all seal commits with their signing indicator
cmd := m.cmdWithGPG(ctx, "git", "rev-list", `--pretty=format:%G?,%H`, "--no-commit-header", "--grep=Argocd-gpg-seal:", "--regexp-ignore-case", revisionSha)
sealCommitsRawOut, err := m.runCmdOutput(cmd, runOpts{})
if err != nil {
return "", err
}
commitFilterArgs, err = m.getSealRevListFilter(revisionSha, sealCommitsRawOut)
if err != nil {
return "", err
}
} else {
// List only the one revision - no seal commit search done
commitFilterArgs = []string{revisionSha, "-1", "--"}
}
// Find all commits until the criteria, including
lsArgs := append([]string{"rev-list", `--pretty=format:%H,%G?,%GK,"%aD","%an <%ae>"`, "--no-commit-header"}, commitFilterArgs...)
commitSignaturesRawOut, err := m.runCmdOutput(m.cmdWithGPG(ctx, "git", lsArgs...), runOpts{})
if err != nil {
return "", err
}
return commitSignaturesRawOut, nil
}
// getSealRevListFilter create arguments for `git rev-list` to search the history all the way until the seal commits found.
func (m *nativeGitClient) getSealRevListFilter(revision string, sealCommitsRawOut string) ([]string, error) {
// Keep only seal commits with a valid signature
var sealCommits []string
for line := range strings.SplitSeq(sealCommitsRawOut, "\n") {
if strings.HasPrefix(line, "G,") {
sealCommits = append(sealCommits, line[2:])
}
}
sealCommitsLen := len(sealCommits)
log.Debugf("Found %d seal commits for %s", sealCommitsLen, revision)
// No (correctly signed) seal commits found - verify all ancestry
if sealCommitsLen == 0 {
return []string{revision, "--"}, nil
}
// Resolve, in case revision is not a commit number
sha, err := m.CommitSHA()
if err != nil {
return nil, err
}
if sha == sealCommits[0] {
// Currently on seal commit - verify just this one
return []string{revision, "-1", "--"}, nil
}
// Some seal commits in history - filter until those
return append([]string{"--boundary", revision, "--not"}, sealCommits...), nil
}
// IsAnnotatedTag returns true if the revision is an annotated tag existing in the repository, and false for everything else.
func (m *nativeGitClient) IsAnnotatedTag(revision string) bool {
cmd := exec.CommandContext(context.Background(), "git", "describe", "--exact-match", revision)
cmd := exec.CommandContext(context.Background(), "git", "cat-file", "-t", revision)
out, err := m.runCmdOutput(cmd, runOpts{SkipErrorLogging: true})
if out != "" && err == nil {
// a lightweight tag returns "commit" - makes sense in the git world
if err == nil && out == "tag" {
return true
}
return false
@ -1247,11 +1567,11 @@ func (m *nativeGitClient) HasFileChanged(filePath string) (bool, error) {
return false, fmt.Errorf("git diff failed: %w", err)
}
// runWrapper runs a custom command with all the semantics of running the Git client
func (m *nativeGitClient) runGnuPGWrapper(ctx context.Context, wrapper string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, wrapper, args...)
// cmdWithGPG creates git Cmd with a GPG-enabled environment
func (m *nativeGitClient) cmdWithGPG(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Env = append(cmd.Env, "GNUPGHOME="+common.GetGnuPGHomePath(), "LANG=C")
return m.runCmdOutput(cmd, runOpts{})
return cmd
}
// runCmd is a convenience function to run a command in a given directory and return its output

View file

@ -121,25 +121,30 @@ func Test_IsAnnotatedTag(t *testing.T) {
err = runCmd(ctx, client.Root(), "git", "commit", "-m", "Initial commit", "-a")
require.NoError(t, err)
atag := client.IsAnnotatedTag("master")
err = runCmd(ctx, client.Root(), "git", "tag", "annot-tag", "-a", "-m", "Create annotated tag")
require.NoError(t, err)
err = runCmd(ctx, client.Root(), "git", "tag", "light-tag")
require.NoError(t, err)
atag := client.IsAnnotatedTag("HEAD")
assert.False(t, atag)
err = runCmd(ctx, client.Root(), "git", "tag", "some-tag", "-a", "-m", "Create annotated tag")
atag = client.IsAnnotatedTag("master")
assert.False(t, atag)
atag = client.IsAnnotatedTag("blorp")
assert.False(t, atag)
sha, err := client.CommitSHA()
require.NoError(t, err)
atag = client.IsAnnotatedTag("some-tag")
atag = client.IsAnnotatedTag(sha)
assert.False(t, atag)
atag = client.IsAnnotatedTag("annot-tag")
assert.True(t, atag)
// Tag effectually points to HEAD, so it's considered the same
atag = client.IsAnnotatedTag("HEAD")
assert.True(t, atag)
err = runCmd(ctx, client.Root(), "git", "rm", "README")
require.NoError(t, err)
err = runCmd(ctx, client.Root(), "git", "commit", "-m", "remove README", "-a")
require.NoError(t, err)
// We moved on, so tag doesn't point to HEAD anymore
atag = client.IsAnnotatedTag("HEAD")
atag = client.IsAnnotatedTag("light-tag")
assert.False(t, atag)
}
@ -1463,3 +1468,40 @@ func Test_nativeGitClient_HasFileChanged(t *testing.T) {
require.NoError(t, err)
require.True(t, changed, "expected modified file to be reported as changed")
}
func Test_LsSignatures_Error(t *testing.T) {
ctx := t.Context()
tempDir, err := _createEmptyGitRepo(ctx)
require.NoError(t, err)
client, err := NewClient("file://"+tempDir, NopCreds{}, true, false, "", "")
require.NoError(t, err)
require.NoError(t, client.Init())
out, err := client.SetAuthor("test", "test@example.com")
require.NoError(t, err, "error output: %s", out)
err = runCmd(ctx, tempDir, "git", "log")
require.NoError(t, err)
tests := []struct {
revision string
deep bool
expectedMsg string
}{
{
revision: "5555.*",
deep: false,
expectedMsg: "version matching constraint not found",
},
{
revision: "5555.*",
deep: true,
expectedMsg: "version matching constraint not found",
},
}
for _, tt := range tests {
signatures, err := client.LsSignatures(tt.revision, tt.deep)
require.ErrorContains(t, err, tt.expectedMsg)
assert.Nil(t, signatures)
}
}

View file

@ -358,7 +358,7 @@ func TestLFSClient(t *testing.T) {
func TestVerifyCommitSignature(t *testing.T) {
p := t.TempDir()
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", p, NopCreds{}, false, false, "", "")
client, err := NewClientExt("https://github.com/argoproj/argocd-example-apps.git", p, NopCreds{}, false, false, "", "")
require.NoError(t, err)
err = client.Init()
@ -375,8 +375,8 @@ func TestVerifyCommitSignature(t *testing.T) {
require.NoError(t, err)
// Fetch the specific commits needed for signature verification
signedCommit := "28027897aad1262662096745f2ce2d4c74d02b7f"
unsignedCommit := "85d660f0b967960becce3d49bd51c678ba2a5d24"
signedCommit := "723b86e01bea11dcf72316cb172868fcbf05d69e"
unsignedCommit := "1ccdee0a611224ccc6b9ff7919fe7002f905436e"
err = client.Fetch(signedCommit, 1)
require.NoError(t, err)
err = client.Fetch(unsignedCommit, 1)

View file

@ -0,0 +1,320 @@
package git
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/argoproj/argo-cd/v3/common"
)
type gpgReadyRepo struct {
t *testing.T
git Client
gpgHome string
}
func newGPGReadyRepo(t *testing.T) *gpgReadyRepo {
t.Helper()
repo := &gpgReadyRepo{t, nil, t.TempDir()}
t.Setenv(common.EnvGnuPGHome, repo.gpgHome)
err := os.Chmod(repo.gpgHome, 0o700)
require.NoError(t, err)
repo.git, err = NewClient("https://fake.url/org/repo.git", NopCreds{}, true, false, "", "")
require.NoError(t, err)
_ = os.RemoveAll(repo.git.Root())
t.Cleanup(func() {
_ = os.RemoveAll(repo.git.Root())
})
err = repo.git.Init()
require.NoError(t, err)
require.NoError(t, repo.cmd("checkout", "-b", "main"))
repo.setUser("Test User", "test@example.com")
return repo
}
func (g *gpgReadyRepo) setUser(name string, email string) {
require.NoError(g.t, g.cmd("config", "--local", "user.name", name))
require.NoError(g.t, g.cmd("config", "--local", "user.email", email))
}
func (g *gpgReadyRepo) generateGPGKey(name string) (keyID string) {
g.t.Helper()
keyInput := fmt.Sprintf(`%%echo Generating test key
Key-Type: RSA
Key-Length: 2048
Name-Real: %s User
Name-Email: %s@example.com
Expire-Date: 0
%%no-protection
%%commit
%%echo Done`, name, name)
cmd := exec.CommandContext(g.t.Context(), "gpg", "--batch", "--generate-key", "--homedir", g.gpgHome)
cmd.Stdin = strings.NewReader(keyInput)
out, err := cmd.CombinedOutput()
require.NoError(g.t, err, "gpg key generation failed: %s", out)
cmd = exec.CommandContext(g.t.Context(), "gpg", "--list-keys", "--with-colons", "--homedir", g.gpgHome)
out, err = cmd.Output()
require.NoError(g.t, err)
// Parse output to get key ID
lines := strings.SplitSeq(string(out), "\n")
for line := range lines {
if strings.HasPrefix(line, "pub:") {
fields := strings.Split(line, ":")
if len(fields) > 4 {
keyID = fields[4]
// Loop even after found intentionally, expected the newest key will be the last one
}
}
}
require.NotEmpty(g.t, keyID, "failed to get GPG key ID")
return keyID
}
func (g *gpgReadyRepo) revokeGPGKey(keyID string) {
cmd := exec.CommandContext(
g.t.Context(),
"gpg", "--batch",
"--command-fd=0", "--status-fd=1",
"--homedir", g.gpgHome,
"--edit-key", keyID,
)
// gpg is so not meant to be used from automation. This is why `--command-fd=0 --status-fd=1` is needed
cmd.Stdin = strings.NewReader(`revkey
y
2
Automated revocation
y
save
`)
out, err := cmd.CombinedOutput()
require.NoError(g.t, err, "gpg key revocation generation failed: %s", out)
}
func (g *gpgReadyRepo) cmd(args ...string) error {
cmd := exec.CommandContext(g.t.Context(), "git", args...)
cmd.Dir = g.git.Root()
cmd.Env = append(cmd.Env, "GNUPGHOME="+g.gpgHome)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func (g *gpgReadyRepo) commitSHA() string {
sha, err := g.git.CommitSHA()
require.NoError(g.t, err)
return sha
}
func (g *gpgReadyRepo) assertSignedAs(revision string, expectedSign ...string) {
info, err := g.git.LsSignatures(revision, true)
require.NoError(g.t, err)
var actualSign []string
for _, record := range info {
actualSign = append(actualSign, string(record.VerificationResult)+"="+record.SignatureKeyID)
}
assert.Equal(g.t, expectedSign, actualSign)
}
func Test_LsSignatures_SignedAndMerged(t *testing.T) {
repo := newGPGReadyRepo(t)
mainKeyID := repo.generateGPGKey("main")
otherKeyID := repo.generateGPGKey("other")
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=root", "--gpg-sign="+mainKeyID))
require.NoError(t, repo.cmd("checkout", "-b", "left", "main"))
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=right", "--gpg-sign="+otherKeyID))
require.NoError(t, repo.cmd("checkout", "main"))
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=left", "--gpg-sign="+mainKeyID))
require.NoError(t, repo.cmd("merge", "left", "--no-edit", "--message=merge", "--gpg-sign="+mainKeyID))
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=main", "--gpg-sign="+mainKeyID))
tip := repo.commitSHA()
repo.assertSignedAs(
tip,
"signed="+mainKeyID, // main
"signed="+mainKeyID, // merge
"signed="+mainKeyID, "signed="+otherKeyID, // left + right
"signed="+mainKeyID, // root
)
repo.revokeGPGKey(mainKeyID)
repo.assertSignedAs(
tip,
"signed with revoked key="+mainKeyID, // main
"signed with revoked key="+mainKeyID, // merge
"signed with revoked key="+mainKeyID, "signed="+otherKeyID, // left + right
"signed with revoked key="+mainKeyID, // root
)
}
func Test_LsSignatures_Sealed_linear(t *testing.T) {
repo := newGPGReadyRepo(t)
trustedKeyID := repo.generateGPGKey("trusted")
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID)
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned"))
repo.assertSignedAs(repo.commitSHA(), "unsigned=", "signed="+trustedKeyID)
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID, "unsigned=", "signed="+trustedKeyID)
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=seal", "--gpg-sign="+trustedKeyID, "--trailer=ArgoCD-gpg-seal: XXX"))
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID)
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID, "signed="+trustedKeyID)
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned"))
repo.assertSignedAs(repo.commitSHA(), "unsigned=", "signed="+trustedKeyID, "signed="+trustedKeyID)
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=seal", "--gpg-sign="+trustedKeyID, "--trailer=ArgoCD-gpg-seal: XXX"))
repo.assertSignedAs(repo.commitSHA(), "signed="+trustedKeyID)
}
func Test_LsSignatures_UnsignedSealedCommitDoesNotStopHistorySearch(t *testing.T) {
// The seal commit must be signed and trusted. When it is not, it is not considered a seal commit and the history is searched further.
repo := newGPGReadyRepo(t)
trustedKeyID := repo.generateGPGKey("trusted")
// Will not be listed
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned init"))
// The seal commit we stop on
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed seal", "--trailer=ArgoCD-gpg-seal: XXX", "--gpg-sign="+trustedKeyID))
signedSealSha := repo.commitSHA()
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned past"))
unsignedPastSha := repo.commitSHA()
// The wannabe seal commit we ignore - unsigned
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=unsigned seal", "--trailer=ArgoCD-gpg-seal: XXX"))
unsignedSealSha := repo.commitSHA()
require.NoError(t, repo.cmd("commit", "--allow-empty", "--no-edit", "--message=signed", "--gpg-sign="+trustedKeyID))
signedSha := repo.commitSHA()
info, err := repo.git.LsSignatures(signedSha, true)
require.NoError(t, err)
assert.Len(t, info, 4)
assert.Equal(t, GPGVerificationResultGood, info[0].VerificationResult)
assert.Equal(t, signedSha, info[0].Revision)
assert.Equal(t, GPGVerificationResultUnsigned, info[1].VerificationResult)
assert.Equal(t, unsignedSealSha, info[1].Revision)
assert.Equal(t, GPGVerificationResultUnsigned, info[2].VerificationResult)
assert.Equal(t, unsignedPastSha, info[2].Revision)
assert.Equal(t, GPGVerificationResultGood, info[3].VerificationResult)
assert.Equal(t, signedSealSha, info[3].Revision)
}
func Test_SignedTag(t *testing.T) {
repo := newGPGReadyRepo(t)
commitKeyId := repo.generateGPGKey("commit gpg")
tagKeyId := repo.generateGPGKey("tag gpg")
require.NoError(t, repo.cmd("commit", "--allow-empty", "--message=unsigned"))
require.NoError(t, repo.cmd("commit", "--allow-empty", "--message=signed", "--gpg-sign="+commitKeyId))
// Tags are made by different user and key
repo.setUser("Tagging user", "tagger@argo.io")
require.NoError(t, repo.cmd("tag", "--message=signed tag", "--local-user="+tagKeyId, "1.0", "HEAD~1"))
require.NoError(t, repo.cmd("tag", "--message=signed tag", "--local-user="+tagKeyId, "2.0", "HEAD"))
require.NoError(t, repo.cmd("tag", "--message=unsigned tag", "dev", "HEAD"))
info, err := repo.git.LsSignatures("1.0", false)
require.NoError(t, err)
require.Len(t, info, 1)
assert.Equal(t, "1.0", info[0].Revision)
assert.Equal(t, GPGVerificationResultGood, info[0].VerificationResult)
assert.Equal(t, tagKeyId, info[0].SignatureKeyID)
assert.Equal(t, `Tagging user "<tagger@argo.io>"`, info[0].AuthorIdentity)
info, err = repo.git.LsSignatures("2.0", false)
require.NoError(t, err)
require.Len(t, info, 1)
assert.Equal(t, "2.0", info[0].Revision)
assert.Equal(t, GPGVerificationResultGood, info[0].VerificationResult)
assert.Equal(t, tagKeyId, info[0].SignatureKeyID)
assert.Equal(t, `Tagging user "<tagger@argo.io>"`, info[0].AuthorIdentity)
info, err = repo.git.LsSignatures("dev", false)
require.NoError(t, err)
require.Len(t, info, 1)
assert.Equal(t, "dev", info[0].Revision)
assert.Equal(t, GPGVerificationResultUnsigned, info[0].VerificationResult)
assert.Empty(t, info[0].SignatureKeyID)
assert.Equal(t, `Tagging user "<tagger@argo.io>"`, info[0].AuthorIdentity)
}
func Test_parseGpgSignStatus(t *testing.T) {
testCases := []struct {
cmdErr error
tagGpgOut string
expError string
expResult GPGVerificationResult
expKeyID string
}{
{
errors.New("fake"),
"error: no signature found",
"", GPGVerificationResultUnsigned, "",
},
{
errors.New("fake"),
"the unexpected have happened",
"fake", "", "",
},
{
nil,
"Buahahaha!",
"unexpected `git verify-tag --raw` output: \"Buahahaha!\"", "", "",
},
{
nil,
`[GNUPG:] NEWSIG
[GNUPG:] ERRSIG D56C4FCA57A46444 1 10 00 1763632400 9 EA459B49595CBE3FD1FBA303D56C4FCA57A46444
[GNUPG:] NO_PUBKEY D56C4FCA57A46444
[GNUPG:] FAILURE gpg-exit 33554433`,
"", GPGVerificationResultMissingKey, "D56C4FCA57A46444",
},
{
nil,
`[GNUPG:] NEWSIG user17@argo.io
[GNUPG:] KEY_CONSIDERED D7E87AF6B99E64079FFECC029515ACB41E14E7F9 0
[GNUPG:] SIG_ID ES7wSYaAnVXVsRjW15LzE4TMp+U 2025-11-19 3671527729
[GNUPG:] GOODSIG 9515ACB41E14E7F9 User N17 <user17@argo.io>
[GNUPG:] VALIDSIG D7E87AF6B99E64079FFECC029515ACB41E14E7F9 2025-11-19 3671527729 0 4 0 1 10 00 D7E87AF6B99E64079FFECC029515ACB41E14E7F9
[GNUPG:] TRUST_ULTIMATE 0 pgp user17@argo.io`,
"", GPGVerificationResultGood, "9515ACB41E14E7F9",
},
}
for _, tt := range testCases {
result, keyId, err := evaluateGpgSignStatus(tt.cmdErr, tt.tagGpgOut)
if tt.expError != "" {
require.Error(t, err)
assert.Equal(t, tt.expError, err.Error())
}
assert.Equal(t, tt.expResult, result)
assert.Equal(t, tt.expKeyID, keyId)
}
}

128
util/git/mocks/Client.go generated
View file

@ -724,8 +724,8 @@ func (_c *Client_Init_Call) RunAndReturn(run func() error) *Client_Init_Call {
}
// IsAnnotatedTag provides a mock function for the type Client
func (_mock *Client) IsAnnotatedTag(s string) bool {
ret := _mock.Called(s)
func (_mock *Client) IsAnnotatedTag(revision string) bool {
ret := _mock.Called(revision)
if len(ret) == 0 {
panic("no return value specified for IsAnnotatedTag")
@ -733,7 +733,7 @@ func (_mock *Client) IsAnnotatedTag(s string) bool {
var r0 bool
if returnFunc, ok := ret.Get(0).(func(string) bool); ok {
r0 = returnFunc(s)
r0 = returnFunc(revision)
} else {
r0 = ret.Get(0).(bool)
}
@ -746,12 +746,12 @@ type Client_IsAnnotatedTag_Call struct {
}
// IsAnnotatedTag is a helper method to define mock.On call
// - s string
func (_e *Client_Expecter) IsAnnotatedTag(s interface{}) *Client_IsAnnotatedTag_Call {
return &Client_IsAnnotatedTag_Call{Call: _e.mock.On("IsAnnotatedTag", s)}
// - revision string
func (_e *Client_Expecter) IsAnnotatedTag(revision interface{}) *Client_IsAnnotatedTag_Call {
return &Client_IsAnnotatedTag_Call{Call: _e.mock.On("IsAnnotatedTag", revision)}
}
func (_c *Client_IsAnnotatedTag_Call) Run(run func(s string)) *Client_IsAnnotatedTag_Call {
func (_c *Client_IsAnnotatedTag_Call) Run(run func(revision string)) *Client_IsAnnotatedTag_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
@ -769,7 +769,7 @@ func (_c *Client_IsAnnotatedTag_Call) Return(b bool) *Client_IsAnnotatedTag_Call
return _c
}
func (_c *Client_IsAnnotatedTag_Call) RunAndReturn(run func(s string) bool) *Client_IsAnnotatedTag_Call {
func (_c *Client_IsAnnotatedTag_Call) RunAndReturn(run func(revision string) bool) *Client_IsAnnotatedTag_Call {
_c.Call.Return(run)
return _c
}
@ -1063,6 +1063,74 @@ func (_c *Client_LsRemote_Call) RunAndReturn(run func(revision string) (string,
return _c
}
// LsSignatures provides a mock function for the type Client
func (_mock *Client) LsSignatures(revision string, deep bool) ([]git.RevisionSignatureInfo, error) {
ret := _mock.Called(revision, deep)
if len(ret) == 0 {
panic("no return value specified for LsSignatures")
}
var r0 []git.RevisionSignatureInfo
var r1 error
if returnFunc, ok := ret.Get(0).(func(string, bool) ([]git.RevisionSignatureInfo, error)); ok {
return returnFunc(revision, deep)
}
if returnFunc, ok := ret.Get(0).(func(string, bool) []git.RevisionSignatureInfo); ok {
r0 = returnFunc(revision, deep)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]git.RevisionSignatureInfo)
}
}
if returnFunc, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = returnFunc(revision, deep)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_LsSignatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LsSignatures'
type Client_LsSignatures_Call struct {
*mock.Call
}
// LsSignatures is a helper method to define mock.On call
// - revision string
// - deep bool
func (_e *Client_Expecter) LsSignatures(revision interface{}, deep interface{}) *Client_LsSignatures_Call {
return &Client_LsSignatures_Call{Call: _e.mock.On("LsSignatures", revision, deep)}
}
func (_c *Client_LsSignatures_Call) Run(run func(revision string, deep bool)) *Client_LsSignatures_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 bool
if args[1] != nil {
arg1 = args[1].(bool)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Client_LsSignatures_Call) Return(revisionSignatureInfos []git.RevisionSignatureInfo, err error) *Client_LsSignatures_Call {
_c.Call.Return(revisionSignatureInfos, err)
return _c
}
func (_c *Client_LsSignatures_Call) RunAndReturn(run func(revision string, deep bool) ([]git.RevisionSignatureInfo, error)) *Client_LsSignatures_Call {
_c.Call.Return(run)
return _c
}
// RemoveContents provides a mock function for the type Client
func (_mock *Client) RemoveContents(paths []string) (string, error) {
ret := _mock.Called(paths)
@ -1123,6 +1191,50 @@ func (_c *Client_RemoveContents_Call) RunAndReturn(run func(paths []string) (str
return _c
}
// RepoURL provides a mock function for the type Client
func (_mock *Client) RepoURL() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for RepoURL")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Client_RepoURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RepoURL'
type Client_RepoURL_Call struct {
*mock.Call
}
// RepoURL is a helper method to define mock.On call
func (_e *Client_Expecter) RepoURL() *Client_RepoURL_Call {
return &Client_RepoURL_Call{Call: _e.mock.On("RepoURL")}
}
func (_c *Client_RepoURL_Call) Run(run func()) *Client_RepoURL_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_RepoURL_Call) Return(s string) *Client_RepoURL_Call {
_c.Call.Return(s)
return _c
}
func (_c *Client_RepoURL_Call) RunAndReturn(run func() string) *Client_RepoURL_Call {
_c.Call.Return(run)
return _c
}
// RevisionMetadata provides a mock function for the type Client
func (_mock *Client) RevisionMetadata(revision string) (*git.RevisionMetadata, error) {
ret := _mock.Called(revision)

View file

@ -1,3 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 4AEE18F83AFDEB23
gpg: BAD signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]

View file

@ -1,3 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 5F4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]

View file

@ -1,3 +0,0 @@
gpg: Signature was made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]

View file

@ -1,3 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key noreply@github.com
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]

View file

@ -1,3 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing)" <noreply@github.com>" [ultimate]

View file

@ -1,6 +0,0 @@
gpg: CRC error; AF65FD - 3ABB26
gpg: [don't know]: invalid packet (ctb=78)
gpg: no signature found
gpg: the signature could not be verified.
Please remember that the signature file (.sig or .asc)
should be the first file given on the command line.

View file

@ -1,3 +0,0 @@
Lorem ipsum
Lorem ipsum
Lorem ipsum

View file

@ -1,2 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 4AEE18F83AFDEB23

View file

@ -1 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET

View file

@ -1,59 +0,0 @@
package testdata
import _ "embed"
var (
//go:embed bad_signature_bad.txt
Bad_signature_bad_txt string
//go:embed bad_signature_badkeyid.txt
Bad_signature_badkeyid_txt string
//go:embed bad_signature_malformed1.txt
Bad_signature_malformed1_txt string
//go:embed bad_signature_malformed2.txt
Bad_signature_malformed2_txt string
//go:embed bad_signature_malformed3.txt
Bad_signature_malformed3_txt string
//go:embed bad_signature_manipulated.txt
Bad_signature_manipulated_txt string
//go:embed bad_signature_nodata.txt
Bad_signature_nodata_txt string
//go:embed bad_signature_preeof1.txt
Bad_signature_preeof1_txt string
//go:embed bad_signature_preeof2.txt
Bad_signature_preeof2_txt string
//go:embed garbage.asc
Garbage_asc string
//go:embed github.asc
Github_asc string
//go:embed good_signature.txt
Good_signature_txt string
//go:embed janedoe.asc
Janedoe_asc string
//go:embed johndoe.asc
Johndoe_asc string
//go:embed multi.asc
Multi_asc string
//go:embed multi2.asc
Multi2_asc string
//go:embed unknown_signature1.txt
Unknown_signature1_txt string
//go:embed unknown_signature2.txt
Unknown_signature2_txt string
)

View file

@ -1,3 +0,0 @@
gpg: Signature made Wed Feb 26 23:22:34 2020 CET
gpg: using RSA key 4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]

View file

@ -1,3 +0,0 @@
gpg: Signature made Mon Aug 26 20:59:48 2019 CEST
gpg: using RSA key 4AEE18F83AFDEB23
gpg: Can't check signature: No public key

View file

@ -1,4 +0,0 @@
gpg: Signature made Mon Aug 26 20:59:48 2019 CEST
gpg: using RSA key 4AEE18F83AFDEB23
gpg: issuer "j.doe@example.com"
gpg: Can't check signature: No public key

View file

@ -285,9 +285,8 @@ g, depB, role:depB
`
hook := test.LogHook{}
log.AddHook(&hook)
t.Cleanup(func() {
log.StandardLogger().ReplaceHooks(log.LevelHooks{})
})
t.Cleanup(hook.CleanupHook)
require.NoError(t, ValidatePolicy(policy))
assert.Empty(t, hook.GetRegexMatchesInEntries("user defined roles not found in policies"))

View file

@ -1,4 +1,4 @@
package gpg
package sourceintegrity
import (
"bufio"
@ -32,18 +32,6 @@ var uidMatch = regexp.MustCompile(`^uid\s*\[\s*([a-z]+)\s*\]\s+(.*)$`)
// Regular expression to match import status
var importMatch = regexp.MustCompile(`^gpg: key ([A-Z0-9]+): public key "([^"]+)" imported$`)
// Regular expression to match the start of a commit signature verification
var verificationStartMatch = regexp.MustCompile(`^gpg: Signature made ([a-zA-Z0-9\ :]+)$`)
// Regular expression to match the key ID of a commit signature verification
var verificationKeyIDMatch = regexp.MustCompile(`^gpg:\s+using\s([A-Za-z]+)\skey\s([a-zA-Z0-9]+)$`)
// Regular expression to match possible additional fields of a commit signature verification
var verificationAdditionalFields = regexp.MustCompile(`^gpg:\s+issuer\s.+$`)
// Regular expression to match the signature status of a commit signature verification
var verificationStatusMatch = regexp.MustCompile(`^gpg: ([a-zA-Z]+) signature from "([^"]+)" \[([a-zA-Z]+)\]$`)
// This is the recipe for automatic key generation, passed to gpg --batch --gen-key
// for initializing our keyring with a trustdb. A new private key will be generated each
// time argocd-server starts, so it's transient and is not used for anything except for
@ -70,15 +58,15 @@ func isHexString(s string) bool {
return err == nil
}
// KeyID get the actual correct (short) key ID from either a fingerprint or the key ID. Returns the empty string if k seems not to be a PGP key ID.
func KeyID(k string) string {
// KeyID get the actual correct (short) key ID from either a fingerprint or the key ID. Errors if it is not a valid GnuPG key ID.
func KeyID(k string) (string, error) {
if IsLongKeyID(k) {
return k[24:]
return k[24:], nil
} else if IsShortKeyID(k) {
return k
return k, nil
}
// Invalid key
return ""
return "", fmt.Errorf("'%s' is not a valid GnuPG key ID", k)
}
// IsLongKeyID returns true if the string represents a long key ID (aka fingerprint)
@ -97,32 +85,6 @@ func IsShortKeyID(k string) bool {
return false
}
// Result of a git commit verification
type PGPVerifyResult struct {
// Date the signature was made
Date string
// KeyID the signature was made with
KeyID string
// Identity
Identity string
// Trust level of the key
Trust string
// Cipher of the key the signature was made with
Cipher string
// Result of verification - "unknown", "good" or "bad"
Result string
// Additional informational message
Message string
}
// Signature verification results
const (
VerifyResultGood = "Good"
VerifyResultBad = "Bad"
VerifyResultInvalid = "Invalid"
VerifyResultUnknown = "Unknown"
)
// Key trust values
const (
TrustUnknown = "unknown"
@ -141,9 +103,6 @@ var pgpTrustLevels = map[string]int{
TrustUltimate: 6,
}
// Maximum number of lines to parse for a gpg verify-commit output
const MaxVerificationLinesToParse = 40
// Helper function to append GNUPGHOME for a command execution environment
func getGPGEnviron() []string {
return append(os.Environ(), "GNUPGHOME="+common.GetGnuPGHomePath(), "LANG=C")
@ -205,14 +164,6 @@ func removeKeyRing(path string) error {
return nil
}
// IsGPGEnabled returns true if GPG feature is enabled
func IsGPGEnabled() bool {
if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.EqualFold(en, "false") || strings.EqualFold(en, "no") {
return false
}
return true
}
// InitializeGnuPG will initialize a GnuPG working directory and also create a
// transient private key so that the trust DB will work correctly.
func InitializeGnuPG() error {
@ -481,7 +432,7 @@ func IsSecretKey(keyID string) (bool, error) {
return true, nil
}
// GetInstalledPGPKeys() runs gpg to retrieve public keys from our keyring. If kids is non-empty, limit result to those key IDs
// GetInstalledPGPKeys runs gpg to retrieve public keys from our keyring. If kids is non-empty, limit result to those key IDs
func GetInstalledPGPKeys(kids []string) ([]*appsv1.GnuPGPublicKey, error) {
keys := make([]*appsv1.GnuPGPublicKey, 0)
ctx := context.Background()
@ -580,109 +531,6 @@ func GetInstalledPGPKeys(kids []string) ([]*appsv1.GnuPGPublicKey, error) {
return keys, nil
}
// ParseGitCommitVerification parses the output of "git verify-commit" and returns the result
func ParseGitCommitVerification(signature string) PGPVerifyResult {
result := PGPVerifyResult{Result: VerifyResultUnknown}
parseOk := false
linesParsed := 0
// Shortcut for returning an unknown verification result with a reason
unknownResult := func(reason string) PGPVerifyResult {
return PGPVerifyResult{
Result: VerifyResultUnknown,
Message: reason,
}
}
scanner := bufio.NewScanner(strings.NewReader(signature))
for scanner.Scan() && linesParsed < MaxVerificationLinesToParse {
linesParsed++
// Indicating the beginning of a signature
start := verificationStartMatch.FindStringSubmatch(scanner.Text())
if len(start) == 2 {
result.Date = start[1]
if !scanner.Scan() {
return unknownResult("Unexpected end-of-file while parsing commit verification output.")
}
linesParsed++
// What key has made the signature?
keyID := verificationKeyIDMatch.FindStringSubmatch(scanner.Text())
if len(keyID) != 3 {
return unknownResult("Could not parse key ID of commit verification output.")
}
result.Cipher = keyID[1]
result.KeyID = KeyID(keyID[2])
if result.KeyID == "" {
return unknownResult("Invalid PGP key ID found in verification result: " + result.KeyID)
}
// What was the result of signature verification?
if !scanner.Scan() {
return unknownResult("Unexpected end-of-file while parsing commit verification output.")
}
linesParsed++
// Skip additional fields
for verificationAdditionalFields.MatchString(scanner.Text()) {
if !scanner.Scan() {
return unknownResult("Unexpected end-of-file while parsing commit verification output.")
}
linesParsed++
}
if strings.HasPrefix(scanner.Text(), "gpg: Can't check signature: ") {
result.Result = VerifyResultInvalid
result.Identity = "unknown"
result.Trust = TrustUnknown
result.Message = scanner.Text()
} else {
sigState := verificationStatusMatch.FindStringSubmatch(scanner.Text())
if len(sigState) != 4 {
return unknownResult("Could not parse result of verify operation, check logs for more information.")
}
switch strings.ToLower(sigState[1]) {
case "good":
result.Result = VerifyResultGood
case "bad":
result.Result = VerifyResultBad
default:
result.Result = VerifyResultInvalid
}
result.Identity = sigState[2]
// Did we catch a valid trust?
if _, ok := pgpTrustLevels[sigState[3]]; ok {
result.Trust = sigState[3]
} else {
result.Trust = TrustUnknown
}
result.Message = "Success verifying the commit signature."
}
// No more data to parse here
parseOk = true
break
}
}
if parseOk && linesParsed < MaxVerificationLinesToParse {
// Operation successful - return result
return result
} else if linesParsed >= MaxVerificationLinesToParse {
// Too many output lines, return error
return unknownResult("Too many lines of gpg verify-commit output, abort.")
}
// No data found, return error
return unknownResult("Could not parse output of verify-commit, no verification data found.")
}
// SyncKeyRingFromDirectory will sync the GPG keyring with files in a directory. This is a one-way sync,
// with the configuration being the leading information.
// Files must have a file name matching their Key ID. Keys that are found in the directory but are not

View file

@ -1,4 +1,4 @@
package gpg
package sourceintegrity
import (
"fmt"
@ -46,23 +46,6 @@ func initTempDir(t *testing.T) string {
return p
}
func Test_IsGPGEnabled(t *testing.T) {
t.Run("true", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
assert.True(t, IsGPGEnabled())
})
t.Run("false", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "false")
assert.False(t, IsGPGEnabled())
})
t.Run("empty", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "")
assert.True(t, IsGPGEnabled())
})
}
func Test_GPG_InitializeGnuPG(t *testing.T) {
p := initTempDir(t)
@ -296,161 +279,6 @@ func Test_ValidateGPGKeys(t *testing.T) {
}
}
func Test_GPG_ParseGitCommitVerification(t *testing.T) {
initTempDir(t)
err := InitializeGnuPG()
require.NoError(t, err)
keys, err := ImportPGPKeys("testdata/github.asc")
require.NoError(t, err)
assert.Len(t, keys, 1)
// Good case
{
c, err := os.ReadFile("testdata/good_signature.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
assert.Equal(t, "RSA", res.Cipher)
assert.Equal(t, "ultimate", res.Trust)
assert.Equal(t, "Wed Feb 26 23:22:34 2020 CET", res.Date)
assert.Equal(t, VerifyResultGood, res.Result)
}
// Signature with unknown key - considered invalid
{
c, err := os.ReadFile("testdata/unknown_signature1.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
assert.Equal(t, "RSA", res.Cipher)
assert.Equal(t, TrustUnknown, res.Trust)
assert.Equal(t, "Mon Aug 26 20:59:48 2019 CEST", res.Date)
assert.Equal(t, VerifyResultInvalid, res.Result)
}
// Signature with unknown key and additional fields - considered invalid
{
c, err := os.ReadFile("testdata/unknown_signature2.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
assert.Equal(t, "RSA", res.Cipher)
assert.Equal(t, TrustUnknown, res.Trust)
assert.Equal(t, "Mon Aug 26 20:59:48 2019 CEST", res.Date)
assert.Equal(t, VerifyResultInvalid, res.Result)
}
// Bad signature with known key
{
c, err := os.ReadFile("testdata/bad_signature_bad.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
assert.Equal(t, "RSA", res.Cipher)
assert.Equal(t, "ultimate", res.Trust)
assert.Equal(t, "Wed Feb 26 23:22:34 2020 CET", res.Date)
assert.Equal(t, VerifyResultBad, res.Result)
}
// Bad case: Manipulated/invalid clear text signature
{
c, err := os.ReadFile("testdata/bad_signature_manipulated.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "Could not parse output")
}
// Bad case: Incomplete signature data #1
{
c, err := os.ReadFile("testdata/bad_signature_preeof1.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "end-of-file")
}
// Bad case: Incomplete signature data #2
{
c, err := os.ReadFile("testdata/bad_signature_preeof2.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "end-of-file")
}
// Bad case: No signature data #1
{
c, err := os.ReadFile("testdata/bad_signature_nodata.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "no verification data found")
}
// Bad case: Malformed signature data #1
{
c, err := os.ReadFile("testdata/bad_signature_malformed1.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "no verification data found")
}
// Bad case: Malformed signature data #2
{
c, err := os.ReadFile("testdata/bad_signature_malformed2.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "Could not parse key ID")
}
// Bad case: Malformed signature data #3
{
c, err := os.ReadFile("testdata/bad_signature_malformed3.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "Could not parse result of verify")
}
// Bad case: Invalid key ID in signature
{
c, err := os.ReadFile("testdata/bad_signature_badkeyid.txt")
if err != nil {
panic(err.Error())
}
res := ParseGitCommitVerification(string(c))
assert.Equal(t, VerifyResultUnknown, res.Result)
assert.Contains(t, res.Message, "Invalid PGP key ID")
}
}
func Test_GetGnuPGHomePath(t *testing.T) {
t.Run("empty", func(t *testing.T) {
t.Setenv(common.EnvGnuPGHome, "")
@ -468,30 +296,35 @@ func Test_GetGnuPGHomePath(t *testing.T) {
func Test_KeyID(t *testing.T) {
// Good case - long key ID (aka fingerprint) to short key ID
{
res := KeyID(longKeyID)
res, err := KeyID(longKeyID)
require.NoError(t, err)
assert.Equal(t, shortKeyID, res)
}
// Good case - short key ID remains same
{
res := KeyID(shortKeyID)
res, err := KeyID(shortKeyID)
require.NoError(t, err)
assert.Equal(t, shortKeyID, res)
}
// Bad case - key ID too short
{
keyID := "AEE18F83AFDEB23"
res := KeyID(keyID)
res, err := KeyID(keyID)
require.Error(t, err)
assert.Empty(t, res)
}
// Bad case - key ID too long
{
keyID := "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB2323"
res := KeyID(keyID)
res, err := KeyID(keyID)
require.Error(t, err)
assert.Empty(t, res)
}
// Bad case - right length, but not hex string
{
keyID := "abcdefghijklmn"
res := KeyID(keyID)
res, err := KeyID(keyID)
require.Error(t, err)
assert.Empty(t, res)
}
}

View file

@ -0,0 +1,235 @@
package sourceintegrity
import (
"errors"
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/git"
"github.com/argoproj/argo-cd/v3/util/glob"
)
type gitFunc func(gitClient git.Client, verifiedRevision string) (result *v1alpha1.SourceIntegrityCheckResult, legacyDescription string, err error)
var _gpgDisabledLoggedAlready bool
// HasCriteria determines if any of the sources have some criteria declared
func HasCriteria(si *v1alpha1.SourceIntegrity, sources ...v1alpha1.ApplicationSource) bool {
if si == nil || si.Git == nil {
return false
}
for _, source := range sources {
if !source.IsZero() && !source.IsOCI() && !source.IsHelm() {
if lookupGit(si, source.RepoURL) != nil {
return true
}
}
}
return false
}
// VerifyGit makes sure the git repository satisfies the criteria declared.
// It returns nil in case there were no relevant criteria, a check result if there were.
// The verifiedRevision is expected to be either an annotated tag to a resolved commit sha - the revision, its signature is being verified.
func VerifyGit(si *v1alpha1.SourceIntegrity, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
if si == nil || si.Git == nil {
return nil, "", nil
}
check := lookupGit(si, gitClient.RepoURL())
if check != nil {
return check(gitClient, verifiedRevision)
}
return nil, "", nil
}
func lookupGit(si *v1alpha1.SourceIntegrity, repoURL string) gitFunc {
policies := findMatchingGitPolicies(si.Git, repoURL)
nPolicies := len(policies)
if nPolicies == 0 {
log.Infof("No git source integrity policies found for repo URL: %s", repoURL)
return nil
}
if nPolicies > 1 {
// Multiple matching policies is an error. BUT, it has to return a check that fails for every repo.
// This is to make sure that a mistake in argo cd configuration does not disable verification until fixed.
msg := fmt.Sprintf("multiple (%d) git source integrity policies found for repo URL: %s", nPolicies, repoURL)
log.Warn(msg)
return func(_ git.Client, _ string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
return nil, "", errors.New(msg)
}
}
policy := policies[0]
if policy.GPG != nil {
if policy.GPG.Mode == v1alpha1.SourceIntegrityGitPolicyGPGModeNone {
// Declare missing check because there is no verification performed
return nil
}
if !_gpgDisabledLoggedAlready && !IsGPGEnabled() {
log.Warnf("SourceIntegrity criteria for git+gpg declared, but it is turned off by ARGOCD_GPG_ENABLED")
_gpgDisabledLoggedAlready = true
return nil
}
return func(gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
return verify(policy.GPG, gitClient, verifiedRevision)
}
}
log.Warnf("No verification configured for SourceIntegrity policy for %+v", policy.Repos)
return nil
}
func findMatchingGitPolicies(si *v1alpha1.SourceIntegrityGit, repoURL string) (policies []*v1alpha1.SourceIntegrityGitPolicy) {
for _, p := range si.Policies {
include := false
for _, r := range p.Repos {
m := repoMatches(r.URL, repoURL)
if m == -1 {
include = false
break
} else if m == 1 {
include = true
}
}
if include {
policies = append(policies, p)
}
}
return policies
}
func repoMatches(urlGlob string, repoURL string) int {
if strings.HasPrefix(urlGlob, "!") {
if glob.Match(urlGlob[1:], repoURL) {
return -1
}
} else {
if glob.Match(urlGlob, repoURL) {
return 1
}
}
return 0
}
func verify(g *v1alpha1.SourceIntegrityGitPolicyGPG, gitClient git.Client, verifiedRevision string) (*v1alpha1.SourceIntegrityCheckResult, string, error) {
const checkName = "GIT/GPG"
var deep bool
switch g.Mode {
// verify tag if on tag, latest revision otherwise
case v1alpha1.SourceIntegrityGitPolicyGPGModeHead:
deep = false
// verify history from the current commit
case v1alpha1.SourceIntegrityGitPolicyGPGModeStrict:
deep = true
default:
return nil, "", fmt.Errorf("unknown GPG mode %q configured for GIT source integrity", g.Mode)
}
signatures, err := gitClient.LsSignatures(verifiedRevision, deep)
if err != nil {
return nil, "", err
}
if len(signatures) == 0 {
panic("no signatures found for " + verifiedRevision)
}
problems, legacyDescription := describeProblems(g, signatures)
result := &v1alpha1.SourceIntegrityCheckResult{Checks: []v1alpha1.SourceIntegrityCheckResultItem{{
Name: checkName,
Problems: problems,
}}}
return result, legacyDescription, nil
}
// describeProblems reports 10 most recent problematic signatures or unsigned commits.
// The number is limited not to flood the UI and logs with too many problems. Problems related to the same signing key are squashed.
func describeProblems(g *v1alpha1.SourceIntegrityGitPolicyGPG, signatureInfos []git.RevisionSignatureInfo) (problems []string, legacyDescription string) {
reportedKeys := make(map[string]any)
for _, signatureInfo := range signatureInfos {
// TODO: Delete with next major version. Backward compatibility only
if legacyDescription == "" {
if signatureInfo.VerificationResult == git.GPGVerificationResultUnsigned {
legacyDescription = "Revision is not signed."
} else {
legacyResult := map[git.GPGVerificationResult]string{
git.GPGVerificationResultGood: "Good",
git.GPGVerificationResultBad: "Bad",
git.GPGVerificationResultUntrusted: "Invalid",
git.GPGVerificationResultExpiredSignature: "Invalid",
git.GPGVerificationResultExpiredKey: "Invalid",
git.GPGVerificationResultRevokedKey: "Invalid",
git.GPGVerificationResultMissingKey: "Invalid",
}[signatureInfo.VerificationResult]
legacyDescription = fmt.Sprintf("%s signature from %s key %s", legacyResult, signatureInfo.AuthorIdentity, signatureInfo.SignatureKeyID)
}
}
// Do not report the same key twice unless:
// - the revision is unsigned (unsigned commits can have different authors, so they are all worth reporting)
// - the revision is a tag (tags are signed separately from commits)
if signatureInfo.SignatureKeyID != "" && git.IsCommitSHA(signatureInfo.Revision) {
if _, exists := reportedKeys[signatureInfo.SignatureKeyID]; exists {
continue
}
reportedKeys[signatureInfo.SignatureKeyID] = nil
}
problem := gpgProblemMessage(g, signatureInfo)
if problem != "" {
problems = append(problems, problem)
// Report at most 10 problems
if len(problems) >= 10 {
break
}
}
}
return problems, legacyDescription
}
// gpgProblemMessage generates a message describing GPG verification issues for a specific revision signature and the configured policy.
// When an empty string is returned, it means there is no problem - the validation has passed.
func gpgProblemMessage(g *v1alpha1.SourceIntegrityGitPolicyGPG, signatureInfo git.RevisionSignatureInfo) string {
if signatureInfo.VerificationResult != git.GPGVerificationResultGood {
return fmt.Sprintf(
"Failed verifying revision %s by '%s': %s (key_id=%s)",
signatureInfo.Revision, signatureInfo.AuthorIdentity, signatureInfo.VerificationResult, signatureInfo.SignatureKeyID,
)
}
for _, allowedKey := range g.Keys {
allowedKey, err := KeyID(allowedKey)
if err != nil {
log.Error(err.Error())
continue
}
if allowedKey == signatureInfo.SignatureKeyID {
return ""
}
}
return fmt.Sprintf(
"Failed verifying revision %s by '%s': signed with unallowed key (key_id=%s)",
signatureInfo.Revision, signatureInfo.AuthorIdentity, signatureInfo.SignatureKeyID,
)
}
// IsGPGEnabled returns true if the GPG feature is enabled
func IsGPGEnabled() bool {
if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.EqualFold(en, "false") || strings.EqualFold(en, "no") {
return false
}
return true
}

View file

@ -0,0 +1,535 @@
package sourceintegrity
import (
"fmt"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/git"
gitmocks "github.com/argoproj/argo-cd/v3/util/git/mocks"
utilTest "github.com/argoproj/argo-cd/v3/util/test"
)
func Test_IsGPGEnabled(t *testing.T) {
t.Run("true", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "true")
assert.True(t, IsGPGEnabled())
})
t.Run("false", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "false")
assert.False(t, IsGPGEnabled())
})
t.Run("empty", func(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "")
assert.True(t, IsGPGEnabled())
})
}
func Test_GPGDisabledLogging(t *testing.T) {
t.Setenv("ARGOCD_GPG_ENABLED", "false")
si := &v1alpha1.SourceIntegrity{Git: &v1alpha1.SourceIntegrityGit{Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{{URL: "*"}},
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
Keys: []string{"SOME_KEY_ID"},
},
}}}}
logger := utilTest.LogHook{}
logrus.AddHook(&logger)
t.Cleanup(logger.CleanupHook)
fun := lookupGit(si, "https://github.com/argoproj/argo-cd.git")
assert.Equal(t, []string{"SourceIntegrity criteria for git+gpg declared, but it is turned off by ARGOCD_GPG_ENABLED"}, logger.GetEntries())
assert.Nil(t, fun)
// No logs on the second call
logger.Entries = []logrus.Entry{}
lookupGit(si, "https://github.com/argoproj/argo-cd-ext.git")
assert.Equal(t, []string{}, logger.GetEntries())
assert.Nil(t, fun)
}
func TestGPGUnknownMode(t *testing.T) {
gitClient := &gitmocks.Client{}
gitClient.EXPECT().IsAnnotatedTag(mock.Anything).Return(false)
gitClient.EXPECT().CommitSHA().Return("DEADBEEF", nil)
s := &v1alpha1.SourceIntegrityGitPolicyGPG{Mode: "foobar", Keys: []string{}}
result, _, err := verify(s, gitClient, "https://github.com/argoproj/argo-cd.git")
require.ErrorContains(t, err, `unknown GPG mode "foobar" configured for GIT source integrity`)
assert.Nil(t, result)
}
func TestNullOrEmptyDoesNothing(t *testing.T) {
repoURL := "https://github.com/argoproj/argo-cd"
applicationSource := v1alpha1.ApplicationSource{RepoURL: repoURL}
gitClient := &gitmocks.Client{}
gitClient.EXPECT().RepoURL().Return(repoURL)
tests := []struct {
name string
si *v1alpha1.SourceIntegrity
logged []string
}{
{
name: "nil",
si: nil,
logged: []string{},
},
{
name: "No GIT",
si: &v1alpha1.SourceIntegrity{}, // No Git or alternative specified
logged: []string{},
},
{
name: "No matching policy",
si: &v1alpha1.SourceIntegrity{Git: &v1alpha1.SourceIntegrityGit{}}, // No policies configured here
logged: []string{},
},
{
name: "Matching policy does nothing",
si: &v1alpha1.SourceIntegrity{Git: &v1alpha1.SourceIntegrityGit{Policies: []*v1alpha1.SourceIntegrityGitPolicy{{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{{URL: "*"}},
// No GPG or alternative specified
}}}},
logged: []string{"No verification configured for SourceIntegrity policy for [{URL:*}]"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := utilTest.LogHook{}
logrus.AddHook(&logger)
t.Cleanup(logger.CleanupHook)
assert.False(t, HasCriteria(tt.si, applicationSource))
assert.Equal(t, tt.logged, logger.GetEntries())
})
}
}
func TestPolicyMatching(t *testing.T) {
group := &v1alpha1.SourceIntegrityGitPolicy{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
{URL: "https://github.com/group/*"},
{URL: "!https://github.com/group/*-legacy.git"},
{URL: "!https://github.com/group/*-critical.git"},
},
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead,
},
}
legacy := &v1alpha1.SourceIntegrityGitPolicy{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
{URL: "https://github.com/group/*-legacy.git"},
},
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeNone,
},
}
critical := &v1alpha1.SourceIntegrityGitPolicy{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
{URL: "https://github.com/group/*-critical.git"},
},
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
},
}
// collides with group
duplicated := &v1alpha1.SourceIntegrityGitPolicy{
Repos: []v1alpha1.SourceIntegrityGitPolicyRepo{
{URL: "https://github.com/group/duplicated.git"},
},
GPG: &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead,
},
}
sig := &v1alpha1.SourceIntegrityGit{
Policies: []*v1alpha1.SourceIntegrityGitPolicy{group, legacy, critical, duplicated},
}
p := func(ps ...*v1alpha1.SourceIntegrityGitPolicy) []*v1alpha1.SourceIntegrityGitPolicy { return ps }
testCases := []struct {
repo string
expectedPolicies []*v1alpha1.SourceIntegrityGitPolicy
expectedLogs []string
expectedNoFunc bool
}{
{
repo: "https://github.com/group/head.git",
expectedPolicies: p(group),
expectedLogs: []string{},
},
{
repo: "https://github.com/group/foo-legacy.git",
expectedPolicies: p(legacy),
expectedLogs: []string{},
expectedNoFunc: true, // The mode is "none"
},
{
repo: "https://github.com/group/bar-critical.git",
expectedPolicies: p(critical),
expectedLogs: []string{},
},
{
repo: "https://github.com/group/duplicated.git",
expectedPolicies: p(group, duplicated),
expectedLogs: []string{"multiple (2) git source integrity policies found for repo URL: https://github.com/group/duplicated.git"},
},
{
repo: "https://gitlab.com/foo/bar.git",
expectedPolicies: p(),
expectedLogs: []string{"No git source integrity policies found for repo URL: https://gitlab.com/foo/bar.git"},
expectedNoFunc: true,
},
}
for _, tt := range testCases {
t.Run(tt.repo, func(t *testing.T) {
actual := findMatchingGitPolicies(sig, tt.repo)
assert.Equal(t, tt.expectedPolicies, actual)
hook := utilTest.NewLogHook(logrus.InfoLevel)
logrus.AddHook(hook)
defer hook.CleanupHook()
si := &v1alpha1.SourceIntegrity{Git: sig}
forGitFunc := lookupGit(si, tt.repo)
if tt.expectedNoFunc {
assert.Nil(t, forGitFunc)
} else {
assert.NotNil(t, forGitFunc)
}
assert.Equal(t, tt.expectedLogs, hook.GetEntries())
})
}
}
// Verify that when a user has configured the full fingerprint, it is still accepted
func TestComparingWithGPGFingerprint(t *testing.T) {
const shortKey = "D56C4FCA57A46444"
const fingerprint = "01234567890123456789abcd" + shortKey
require.True(t, IsShortKeyID(shortKey))
require.True(t, IsLongKeyID(fingerprint))
gitClient := &gitmocks.Client{}
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).Return([]git.RevisionSignatureInfo{{
Revision: "1.0", VerificationResult: git.GPGVerificationResultGood, SignatureKeyID: shortKey, Date: "ignored", AuthorIdentity: "ignored",
}}, nil)
gpgWithTag := &v1alpha1.SourceIntegrityGitPolicyGPG{Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead, Keys: []string{fingerprint}}
// And verifying a given revision
result, legacy, err := verify(gpgWithTag, gitClient, "1.0")
require.NoError(t, err)
assert.True(t, result.IsValid())
require.NoError(t, result.AsError())
assert.Equal(t, "Good signature from ignored key D56C4FCA57A46444", legacy)
}
func TestGPGHeadValid(t *testing.T) {
const sha = "0c7a9c3f939c1f19b518bcdd11e2fce9703c4901"
const tag = "tag"
const keyId = "4cfe068f80b1681b"
testCases := []struct {
revision string
check func(gitClient *gitmocks.Client, logger utilTest.LogHook)
}{
{
revision: sha,
check: func(gitClient *gitmocks.Client, logger utilTest.LogHook) {
gitClient.AssertCalled(t, "LsSignatures", sha, false)
assert.Empty(t, logger.GetEntries())
},
},
{
revision: tag,
check: func(gitClient *gitmocks.Client, logger utilTest.LogHook) {
gitClient.AssertCalled(t, "LsSignatures", tag, false)
assert.Empty(t, logger.GetEntries())
},
},
}
for _, test := range testCases {
t.Run("verify "+test.revision, func(t *testing.T) {
// Given repo with a tagged commit
gitClient := &gitmocks.Client{}
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(func(revision string, _ bool) ([]git.RevisionSignatureInfo, error) {
return []git.RevisionSignatureInfo{{
Revision: revision, VerificationResult: git.GPGVerificationResultGood, SignatureKeyID: keyId, Date: "ignored", AuthorIdentity: "ignored",
}}, nil
})
logger := utilTest.LogHook{}
logrus.AddHook(&logger)
t.Cleanup(logger.CleanupHook)
// When using head mode
gpgWithTag := &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeHead,
Keys: []string{keyId, "0000000000000000"},
}
// And verifying a given revision
result, legacy, err := verify(gpgWithTag, gitClient, test.revision)
require.NoError(t, err)
// Then it is checked and valid
assert.True(t, result.IsValid())
assert.Equal(t, []string{"GIT/GPG"}, result.PassedChecks())
test.check(gitClient, logger)
require.NoError(t, result.AsError())
assert.Equal(t, "Good signature from ignored key 4cfe068f80b1681b", legacy)
})
}
}
func TestDescribeProblems(t *testing.T) {
const r = "aafc9e88599f24802b113b6278e42eaadda32cd6"
const a = "Commit Author <nereply@acme.com>"
const kGood = "AAAAAAAAAAAAAAAA"
const kOk = "BBBBBBBBBBBBBBB"
policy := v1alpha1.SourceIntegrityGitPolicyGPG{Keys: []string{kGood, kOk}}
sig := func(key string, result git.GPGVerificationResult) git.RevisionSignatureInfo {
return git.RevisionSignatureInfo{
Revision: r,
VerificationResult: result,
SignatureKeyID: key,
AuthorIdentity: a,
}
}
tests := []struct {
name string
gpg *v1alpha1.SourceIntegrityGitPolicyGPG
sigs []git.RevisionSignatureInfo
expected []string
legacy string
}{
{
name: "report only problems",
gpg: &policy,
sigs: []git.RevisionSignatureInfo{
sig("bad", git.GPGVerificationResultRevokedKey),
sig(kGood, git.GPGVerificationResultGood),
sig("also_bad", git.GPGVerificationResultUntrusted),
},
expected: []string{
"Failed verifying revision " + r + " by '" + a + "': signed with revoked key (key_id=bad)",
"Failed verifying revision " + r + " by '" + a + "': signed with untrusted key (key_id=also_bad)",
},
legacy: "Invalid signature from Commit Author <nereply@acme.com> key bad",
},
{
name: "collapse problems of the same key",
gpg: &policy,
sigs: []git.RevisionSignatureInfo{
sig("bad", git.GPGVerificationResultRevokedKey),
sig(kGood, git.GPGVerificationResultGood),
sig("also_bad", git.GPGVerificationResultUntrusted),
sig("bad", git.GPGVerificationResultRevokedKey),
},
expected: []string{
"Failed verifying revision " + r + " by '" + a + "': signed with revoked key (key_id=bad)",
"Failed verifying revision " + r + " by '" + a + "': signed with untrusted key (key_id=also_bad)",
},
legacy: "Invalid signature from Commit Author <nereply@acme.com> key bad",
},
{
name: "do not collapse unsigned commits, as they can differ by author",
gpg: &policy,
sigs: []git.RevisionSignatureInfo{
sig("", git.GPGVerificationResultUnsigned),
sig("", git.GPGVerificationResultUnsigned),
sig("", git.GPGVerificationResultUnsigned),
},
expected: []string{
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
},
legacy: "Revision is not signed.",
},
{
name: "Report first ten problems only",
gpg: &policy,
sigs: []git.RevisionSignatureInfo{
sig("revoked", git.GPGVerificationResultRevokedKey),
sig("", git.GPGVerificationResultUnsigned),
sig("untrusted", git.GPGVerificationResultUntrusted),
sig("missing", git.GPGVerificationResultMissingKey),
sig("expired_key", git.GPGVerificationResultExpiredKey),
sig("expired_sig", git.GPGVerificationResultExpiredSignature),
sig("bad", git.GPGVerificationResultBad),
sig("also_bad", git.GPGVerificationResultBad),
sig("more_bad", git.GPGVerificationResultBad),
sig("outright_terrible", git.GPGVerificationResultBad),
// the rest is cut off
sig("OMG", git.GPGVerificationResultBad),
sig("nope", git.GPGVerificationResultBad),
sig("you_gotta_be_kidding_me", git.GPGVerificationResultBad),
},
expected: []string{
"Failed verifying revision " + r + " by '" + a + "': signed with revoked key (key_id=revoked)",
"Failed verifying revision " + r + " by '" + a + "': unsigned (key_id=)",
"Failed verifying revision " + r + " by '" + a + "': signed with untrusted key (key_id=untrusted)",
"Failed verifying revision " + r + " by '" + a + "': signed with key not in keyring (key_id=missing)",
"Failed verifying revision " + r + " by '" + a + "': signed with expired key (key_id=expired_key)",
"Failed verifying revision " + r + " by '" + a + "': expired signature (key_id=expired_sig)",
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=bad)",
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=also_bad)",
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=more_bad)",
"Failed verifying revision " + r + " by '" + a + "': bad signature (key_id=outright_terrible)",
},
legacy: "Invalid signature from Commit Author <nereply@acme.com> key revoked",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
problems, legacy := describeProblems(tt.gpg, tt.sigs)
assert.Equal(t, tt.expected, problems)
assert.Equal(t, tt.legacy, legacy)
})
}
}
func TestGPGStrictValid(t *testing.T) {
const shaFirst = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const shaSecond = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
const shaThird = "cccccccccccccccccccccccccccccccccccccccc"
const tagFirst = "tag-first"
const tagSecond = "tag-second"
const tagThird = "tag-third"
const keyOfThird = "9c698b961c1088db"
const keyOfSecond = "f4b9db205449e1d9"
const keyOfFirst = "92bfcec2e8161558"
rsi := func(rev string, key string) git.RevisionSignatureInfo {
return git.RevisionSignatureInfo{
Revision: rev,
VerificationResult: git.GPGVerificationResultGood,
SignatureKeyID: key,
Date: "ignored",
AuthorIdentity: "ignored",
}
}
// To be resolved as lsSignatures[deep][revision]
lsSignatures := map[bool]map[string][]git.RevisionSignatureInfo{
// Return info for all preceding revisions. If revision is a tag, start with a tag.
true: {
shaFirst: []git.RevisionSignatureInfo{rsi(shaFirst, keyOfFirst)},
tagFirst: []git.RevisionSignatureInfo{rsi(tagFirst, keyOfFirst), rsi(tagFirst, keyOfFirst)},
shaSecond: []git.RevisionSignatureInfo{rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
tagSecond: []git.RevisionSignatureInfo{rsi(tagSecond, keyOfSecond), rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
shaThird: []git.RevisionSignatureInfo{rsi(shaThird, keyOfThird), rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
tagThird: []git.RevisionSignatureInfo{rsi(tagThird, keyOfThird), rsi(shaThird, keyOfThird), rsi(shaSecond, keyOfSecond), rsi(shaFirst, keyOfFirst)},
},
// Return info for just the tag or revision
false: {
shaFirst: []git.RevisionSignatureInfo{rsi(shaFirst, keyOfFirst)},
tagFirst: []git.RevisionSignatureInfo{rsi(tagFirst, keyOfFirst)},
shaSecond: []git.RevisionSignatureInfo{rsi(shaSecond, keyOfSecond)},
tagSecond: []git.RevisionSignatureInfo{rsi(tagSecond, keyOfSecond)},
shaThird: []git.RevisionSignatureInfo{rsi(shaThird, keyOfThird)},
tagThird: []git.RevisionSignatureInfo{rsi(tagThird, keyOfThird)},
},
}
tests := []struct {
revision string
expectedErr string
expectedPassed []string
expectedLsArgs []any
}{
{
revision: shaFirst,
expectedPassed: []string{"GIT/GPG"},
expectedLsArgs: []any{shaFirst, true},
},
{
revision: shaSecond,
expectedPassed: []string{"GIT/GPG"},
expectedLsArgs: []any{shaSecond, true},
},
{
revision: shaThird,
expectedPassed: []string{},
expectedErr: fmt.Sprintf("GIT/GPG: Failed verifying revision %s by 'ignored': signed with unallowed key (key_id=%s)", shaThird, keyOfThird),
expectedLsArgs: []any{shaThird, true},
},
{
revision: tagFirst,
expectedPassed: []string{"GIT/GPG"},
expectedLsArgs: []any{shaFirst, true},
},
{
revision: tagSecond,
expectedPassed: []string{"GIT/GPG"},
expectedLsArgs: []any{shaSecond, true},
},
{
revision: tagThird,
expectedPassed: []string{},
expectedErr: fmt.Sprintf(`GIT/GPG: Failed verifying revision %s by 'ignored': signed with unallowed key (key_id=%s)
GIT/GPG: Failed verifying revision %s by 'ignored': signed with unallowed key (key_id=%s)`, tagThird, keyOfThird, shaThird, keyOfThird),
expectedLsArgs: []any{shaThird, true},
},
}
for _, test := range tests {
t.Run("verify "+test.revision, func(t *testing.T) {
// Given repo with a tagged commit
gitClient := &gitmocks.Client{}
gitClient.EXPECT().LsSignatures(mock.Anything, mock.Anything).RunAndReturn(
func(revision string, deep bool) (info []git.RevisionSignatureInfo, err error) {
if ret, ok := lsSignatures[deep][revision]; ok {
return ret, nil
}
panic("unknown revision " + revision)
},
)
logger := utilTest.LogHook{}
logrus.AddHook(&logger)
t.Cleanup(logger.CleanupHook)
// When using strict mode
gpgWithTag := &v1alpha1.SourceIntegrityGitPolicyGPG{
Mode: v1alpha1.SourceIntegrityGitPolicyGPGModeStrict,
Keys: []string{keyOfFirst, keyOfSecond},
}
// And verifying a given revision
result, legacy, err := verify(gpgWithTag, gitClient, test.revision)
require.NoError(t, err)
// Then it is checked and valid
err = result.AsError()
if test.expectedErr == "" {
require.NoError(t, err)
assert.True(t, result.IsValid())
assert.Contains(t, legacy, "Good signature from ")
} else {
require.Error(t, err)
assert.Equal(t, test.expectedErr, err.Error())
assert.False(t, result.IsValid())
// Confusing but correct. Signature is good, but not allowed in project so this should be rejected.
assert.Contains(t, legacy, "Good signature from ")
}
assert.Equal(t, test.expectedPassed, result.PassedChecks())
assert.Empty(t, logger.GetEntries())
})
}
}

23
util/sourceintegrity/testdata/data.go vendored Normal file
View file

@ -0,0 +1,23 @@
package testdata
import _ "embed"
var (
//go:embed garbage.asc
Garbage_asc string
//go:embed github.asc
Github_asc string
//go:embed janedoe.asc
Janedoe_asc string
//go:embed johndoe.asc
Johndoe_asc string
//go:embed multi.asc
Multi_asc string
//go:embed multi2.asc
Multi2_asc string
)

View file

@ -292,11 +292,24 @@ func generateJWTToken(issuer string) (string, error) {
}
type LogHook struct {
Entries []log.Entry
Entries []log.Entry
minLevel log.Level
}
func NewLogHook(minLevel log.Level) *LogHook {
return &LogHook{minLevel: minLevel}
}
func (h *LogHook) Levels() []log.Level {
return []log.Level{log.WarnLevel}
if h.minLevel == 0 {
h.minLevel = log.WarnLevel
}
var levels []log.Level
for i := log.PanicLevel; i <= h.minLevel; i++ {
levels = append(levels, i)
}
return levels
}
func (h *LogHook) Fire(entry *log.Entry) error {
@ -304,6 +317,10 @@ func (h *LogHook) Fire(entry *log.Entry) error {
return nil
}
func (h *LogHook) CleanupHook() {
log.StandardLogger().ReplaceHooks(log.LevelHooks{})
}
func (h *LogHook) GetRegexMatchesInEntries(match string) []string {
re := regexp.MustCompile(match)
matches := make([]string, 0)
@ -314,3 +331,11 @@ func (h *LogHook) GetRegexMatchesInEntries(match string) []string {
}
return matches
}
func (h *LogHook) GetEntries() []string {
matches := make([]string, 0)
for _, entry := range h.Entries {
matches = append(matches, entry.Message)
}
return matches
}