argo-cd/util/db/gpgkeys_test.go
jannfis be718e2b61
feat: GPG commit signature verification (#2492) (#3242)
* Add initial primitives and tests for GPG related operations

* More tests and test documentation

* Move gpg primitives to own module

* Add initial primitives for running git verify-commit and tests

* Improve and better comment test

* Implement VerifyCommitSignature() primitive for metrics wrapper

* More commentary

* Make reposerver verify gpg signatures when generating manifests

* Make signature validation optional

* Forbid use of local manifests when signature verification is enabled

* Introduce new signatureKeys field in project CRD

* Initial support for only syncing against signed revisions

* Updates to GnuPG primitives and more test cases

* Move signature verification to correct place and add tests

* Add signature verification result to revision metadata and display it in UI

* Add more primitives and move out some stuff to common module

* Add more testdata

* Add key management primitives to ArgoDB

* Move type GnuPGPublicKey to appsv1 package

* Add const ArgoCDGPGKeysConfigMapName

* Handle key operations with appsv1.GnuPGPublicKey

* Add initial API for managing GPG keys

* Remove deprecated code

* Add primitives for adding public keys to configuration

* Change semantics of ValidateGPGKeys to return more key information

* Add key import functionality to public key API

* Fix code quirks reported by linter

* More code quirks fixes

* Fix test

* Add primitives for deleting keys from configuration

* Add delete key operation to API and CLI

* Cosmetics

* Implement logic to sync configuration to keyring in repo-server

* Add IsGPGEnabled() primitive and also update trustdb on ownertrust changes

* Use gpg.IsGPGEnabled() instead of custom test

* Remove all keyring manipulating methods from DB

* Cosmetics/comments

* Require grpc methods from argoproj pkg

* Enable setting config path via ARGOCD_GPG_DATA_PATH

* Allow "no" and any cases in ARGOCD_GPG_ENABLED

* Enable GPG feature on start and start-e2e and set required environment

* Cosmetics/comments

* Cosmetics and commentary

* Update API documentation

* Fix comment

* Only run GPG related operations if GPG is enabled

* Allow setting ARGOCD_GPG_ENABLE from the environment

* Create GPG ConfigMap resource during installation

* Use function instead of constant to get the watcher path

* Re-watch source path in case it gets recreated. Also, error on finish

* Add End-to-End tests for GPG commit verification

* Introduce SignatureKey type for AppProject CRD

* Fix merge error from previous commit

* Adapt test for additional manifest (argocd-gpg-keys-cm.yaml)

* Fix linter issues

* Adapt CircleCI configuration to enable running tests

* Add wrapper scripts for git and gpg

* Sigh.

* Display gpg version in CircleCI

* Install gnupg2 and link it to gpg in CI

* Try to install gnupg2 in CircleCI image

* More CircleCI tweaks

* # This is a combination of 10 commits.
# This is the 1st commit message:

Containerize tests - test cycle

# This is the commit message #2:

adapt working directory

# This is the commit message #3:

Build before running tests (so we might have a cache)

# This is the commit message #4:

Test limiting parallelism

# This is the commit message #5:

Remove unbound variable

# This is the commit message #6:

Decrease parallelism to find out limit

# This is the commit message #7:

Use correct flag

# This is the commit message #8:

Update Docker image

# This is the commit message #9:

Remove build phase and increase parallelism

# This is the commit message #10:

Further increase parallelism

* Dockerize toolchain

* Add new targets to Makefile

* Codegen

* Properly handle permissions for E2E tests

* Remove gnupg2 installation from CircleCI configuration

* Limit parallelism of build

* Fix Yarn lint

* Retrigger CI for possible flaky test

* Codegen

* Remove duplicate target in Makefile

* Pull in pager from dep ensure -v

* Adapt to gitops-engine changes and codegen

* Use new health package for health status constants

* Add GPG methods to ArgoDB mock module

* Fix possible nil pointer dereference

* Fix linter issue in imports

* Introduce RBAC resource type 'gpgkeys' and adapt policies

* Use ARGOCD_GNUPGHOME instead of GNUPGHOME for subsystem configuration

Also remove some deprecated unit tests.

* Also register GPG keys API with gRPC-GW

* Update from codegen

* Update GPG key API

* Add web UI to manage GPG keys

* Lint updates

* Change wording

* Add some plausibility checks for supplied data on key creation

* Update from codegen

* Re-allow binary keys and move check for ASCII armoured to UI

* Make yarn lint happy

* Add editing signature keys for projects in UI

* Add ability to configure signature keys for project in CLI

* Change default value to use for GNUPGHOME

* Do not include data section in default gpg keys CM

* Adapt Docker image for GnuPG feature

* Add required configuration to installation manifests

* Add add-signature-key and remove-signature-key commands to project CLI

* Fix typo

* Add initial user documentation for GnuPG verification

* Fix role name - oops

* Mention required RBAC roles in docs

* Support GPG verification of git annotated tags as well

* Ensure CLI can build succesfully

* Better support verification on tags

* Print key type in upper case

* Update user documentation

* Correctly disable GnuPG verification if ARGOCD_GPG_ENABLE=false

* Clarify that this feature is only available with Git repositories

* codegen

* Move verification code to own function

* Remove deprecated check

* Make things more developer friendly when running locally

* Enable GPG feature by default, and don't require ARGOCD_GNUPGHOME to be set

* Revert changes to manifests to reflect default enable state

* Codegen
2020-06-22 18:21:53 +02:00

304 lines
9.3 KiB
Go

package db
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/settings"
)
// GPG config map with a single key and good mapping
var gpgCMEmpty = v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDGPGKeysConfigMapName,
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
}
// GPG config map with a single key and good mapping
var gpgCMSingleGoodPubkey = v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDGPGKeysConfigMapName,
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"4AEE18F83AFDEB23": test.MustLoadFileToString("../gpg/testdata/github.asc"),
},
}
// GPG config map with two keys and good mapping
var gpgCMMultiGoodPubkey = v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDGPGKeysConfigMapName,
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"FDC79815400D88A9": test.MustLoadFileToString("../gpg/testdata/johndoe.asc"),
"F7842A5CEAA9C0B1": test.MustLoadFileToString("../gpg/testdata/janedoe.asc"),
},
}
// GPG config map with a single key and bad mapping
var gpgCMSingleKeyWrongId = v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDGPGKeysConfigMapName,
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"5AEE18F83AFDEB23": test.MustLoadFileToString("../gpg/testdata/github.asc"),
},
}
// GPG config map with a garbage pub key
var gpgCMGarbagePubkey = v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDGPGKeysConfigMapName,
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"4AEE18F83AFDEB23": test.MustLoadFileToString("../gpg/testdata/garbage.asc"),
},
}
// GPG config map with a wrong key
var gpgCMGarbageCMKey = v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDGPGKeysConfigMapName,
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"wullerosekaufe": test.MustLoadFileToString("../gpg/testdata/github.asc"),
},
}
// Returns a fake client set for use in tests
func getGPGKeysClientset(gpgCM v1.ConfigMap) *fake.Clientset {
cm := v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: testNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: nil,
}
return fake.NewSimpleClientset([]runtime.Object{&cm, &gpgCM}...)
}
func Test_ValidatePGPKey(t *testing.T) {
// Good case - single PGP key
{
key, err := validatePGPKey(test.MustLoadFileToString("../gpg/testdata/github.asc"))
assert.NoError(t, err)
assert.NotNil(t, key)
assert.Equal(t, "4AEE18F83AFDEB23", key.KeyID)
assert.NotEmpty(t, key.Owner)
assert.NotEmpty(t, key.KeyData)
assert.NotEmpty(t, key.SubType)
}
// Bad case - Garbage
{
key, err := validatePGPKey(test.MustLoadFileToString("../gpg/testdata/garbage.asc"))
assert.Error(t, err)
assert.Nil(t, key)
}
// Bad case - more than one key
{
key, err := validatePGPKey(test.MustLoadFileToString("../gpg/testdata/multi.asc"))
assert.Error(t, err)
assert.Nil(t, key)
}
}
func Test_ListConfiguredGPGPublicKeys(t *testing.T) {
// Good case. Single key in input, right mapping to Key ID in CM
{
clientset := getGPGKeysClientset(gpgCMSingleGoodPubkey)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
if db == nil {
panic("could not get database")
}
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
assert.NoError(t, err)
assert.Len(t, keys, 1)
}
// Good case. No certificates in ConfigMap
{
clientset := getGPGKeysClientset(gpgCMEmpty)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
if db == nil {
panic("could not get database")
}
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
assert.NoError(t, err)
assert.Len(t, keys, 0)
}
// Bad case. Single key in input, wrong mapping to Key ID in CM
{
clientset := getGPGKeysClientset(gpgCMSingleKeyWrongId)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
if db == nil {
panic("could not get database")
}
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
assert.Error(t, err)
assert.Len(t, keys, 0)
}
// Bad case. Garbage public key
{
clientset := getGPGKeysClientset(gpgCMGarbagePubkey)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
if db == nil {
panic("could not get database")
}
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
assert.Error(t, err)
assert.Len(t, keys, 0)
}
// Bad case. Garbage ConfigMap key in data
{
clientset := getGPGKeysClientset(gpgCMGarbageCMKey)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
if db == nil {
panic("could not get database")
}
keys, err := db.ListConfiguredGPGPublicKeys(context.Background())
assert.Error(t, err)
assert.Len(t, keys, 0)
}
}
func Test_AddGPGPublicKey(t *testing.T) {
// Good case
{
clientset := getGPGKeysClientset(gpgCMEmpty)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
// Key should be added
new, skipped, err := db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/github.asc"))
assert.NoError(t, err)
assert.Len(t, new, 1)
assert.Len(t, skipped, 0)
cm, err := settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
assert.NoError(t, err)
assert.Len(t, cm.Data, 1)
// Same key should not be added, but skipped
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/github.asc"))
assert.NoError(t, err)
assert.Len(t, new, 0)
assert.Len(t, skipped, 1)
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
assert.NoError(t, err)
assert.Len(t, cm.Data, 1)
// New keys should be added
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/multi.asc"))
assert.NoError(t, err)
assert.Len(t, new, 2)
assert.Len(t, skipped, 0)
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
assert.NoError(t, err)
assert.Len(t, cm.Data, 3)
// Same new keys should be skipped
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/multi.asc"))
assert.NoError(t, err)
assert.Len(t, new, 0)
assert.Len(t, skipped, 2)
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
assert.NoError(t, err)
assert.Len(t, cm.Data, 3)
// Garbage input should result in error
new, skipped, err = db.AddGPGPublicKey(context.Background(), test.MustLoadFileToString("../gpg/testdata/garbage.asc"))
assert.Error(t, err)
assert.Nil(t, new)
assert.Nil(t, skipped)
cm, err = settings.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
assert.NoError(t, err)
assert.Len(t, cm.Data, 3)
}
}
func Test_DeleteGPGPublicKey(t *testing.T) {
defer os.Setenv("GNUPGHOME", "")
// Good case
{
clientset := getGPGKeysClientset(gpgCMMultiGoodPubkey)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
// Key should be removed
err := db.DeleteGPGPublicKey(context.Background(), "FDC79815400D88A9")
assert.NoError(t, err)
// Key should not exist anymore, therefore can't be deleted again
err = db.DeleteGPGPublicKey(context.Background(), "FDC79815400D88A9")
assert.Error(t, err)
// One key left in configuration
n, err := db.ListConfiguredGPGPublicKeys(context.Background())
assert.NoError(t, err)
assert.Len(t, n, 1)
// Key should be removed
err = db.DeleteGPGPublicKey(context.Background(), "F7842A5CEAA9C0B1")
assert.NoError(t, err)
// Key should not exist anymore, therefore can't be deleted again
err = db.DeleteGPGPublicKey(context.Background(), "F7842A5CEAA9C0B1")
assert.Error(t, err)
// No key left in configuration
n, err = db.ListConfiguredGPGPublicKeys(context.Background())
assert.NoError(t, err)
assert.Len(t, n, 0)
}
// Bad case - empty ConfigMap
{
clientset := getGPGKeysClientset(gpgCMEmpty)
settings := settings.NewSettingsManager(context.Background(), clientset, testNamespace)
db := NewDB(testNamespace, settings, clientset)
// Key should be removed
err := db.DeleteGPGPublicKey(context.Background(), "F7842A5CEAA9C0B1")
assert.Error(t, err)
}
}