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
This commit is contained in:
jannfis 2020-06-22 18:21:53 +02:00 committed by GitHub
parent a886241ef2
commit be718e2b61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 7669 additions and 521 deletions

View file

@ -50,12 +50,14 @@ RUN groupadd -g 999 argocd && \
chmod g=u /home/argocd && \
chmod g=u /etc/passwd && \
apt-get update && \
apt-get install -y git git-lfs python3-pip tini && \
apt-get install -y git git-lfs python3-pip tini gpg && \
apt-get clean && \
pip3 install awscli==1.18.80 && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
@ -71,6 +73,10 @@ RUN mkdir -p /app/config/ssh && \
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
RUN mkdir -p /app/config/tls
RUN mkdir -p /app/config/gpg/source && \
mkdir -p /app/config/gpg/keys && \
chown argocd /app/config/gpg/keys && \
chmod 0700 /app/config/gpg/keys
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
ENV USER=argocd

View file

@ -98,6 +98,8 @@ IMAGE_NAMESPACE?=
STATIC_BUILD?=true
# build development images
DEV_IMAGE?=false
ARGOCD_GPG_ENABLED?=true
ARGOCD_E2E_APISERVER_PORT?=8080
override LDFLAGS += \
-X ${PACKAGE}.version=${VERSION} \
@ -159,6 +161,8 @@ codegen:
.PHONY: cli
cli: clean-debug
rm -f ${DIST_DIR}/${CLI_NAME}
mkdir -p ${DIST_DIR}
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
.PHONY: cli-docker
@ -331,7 +335,7 @@ test-e2e:
test-e2e-local: cli
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
export GO111MODULE=off
NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
# Spawns a shell in the test server container for debugging purposes
debug-test-server:
@ -354,9 +358,17 @@ start-e2e-local:
kubectl create ns argocd-e2e || true
kubectl config set-context --current --namespace=argocd-e2e
kustomize build test/manifests/base | kubectl apply -f -
# Create GPG keys and source directories
if test -d /tmp/argo-e2e/app/config/gpg; then rm -rf /tmp/argo-e2e/app/config/gpg/*; fi
mkdir -p /tmp/argo-e2e/app/config/gpg/keys && chmod 0700 /tmp/argo-e2e/app/config/gpg/keys
mkdir -p /tmp/argo-e2e/app/config/gpg/source && chmod 0700 /tmp/argo-e2e/app/config/gpg/source
if test "$(USER_ID)" != ""; then chown -R "$(USER_ID)" /tmp/argo-e2e; fi
# set paths for locally managed ssh known hosts and tls certs data
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
ARGOCD_GPG_DATA_PATH=/tmp/argo-e2e/app/config/gpg/source \
ARGOCD_GNUPGHOME=/tmp/argo-e2e/app/config/gpg/keys \
ARGOCD_GPG_ENABLED=true \
ARGOCD_E2E_DISABLE_AUTH=false \
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
@ -383,8 +395,13 @@ start-local: mod-vendor-local
# check we can connect to Docker to start Redis
killall goreman || true
kubectl create ns argocd || true
rm -rf /tmp/argocd-local
mkdir -p /tmp/argocd-local
mkdir -p /tmp/argocd-local/gpg/keys && chmod 0700 /tmp/argocd-local/gpg/keys
mkdir -p /tmp/argocd-local/gpg/source
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=false \
ARGOCD_GPG_ENABLED=true \
ARGOCD_E2E_TEST=false \
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}

View file

@ -1,8 +1,8 @@
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:=/tmp/argocd/ssh} go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:=/tmp/argocd/ssh} go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.22.0 serve /dex.yaml"
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.8-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:=/tmp/argocd/ssh} go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
git-server: test/fixture/testrepos/start-git.sh
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:=/tmp/argocd/tls}
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}

View file

@ -12,6 +12,7 @@ p, role:readonly, clusters, get, *, allow
p, role:readonly, repositories, get, *, allow
p, role:readonly, projects, get, *, allow
p, role:readonly, accounts, get, *, allow
p, role:readonly, gpgkeys, get, *, allow
p, role:admin, applications, create, */*, allow
p, role:admin, applications, update, */*, allow
@ -32,6 +33,8 @@ p, role:admin, projects, create, *, allow
p, role:admin, projects, update, *, allow
p, role:admin, projects, delete, *, allow
p, role:admin, accounts, update, *, allow
p, role:admin, gpgkeys, create, *, allow
p, role:admin, gpgkeys, delete, *, allow
g, role:admin, role:readonly
g, admin, role:admin

1 # Built-in policy which defines two roles: role:readonly and role:admin,
12 p, role:readonly, projects, get, *, allow
13 p, role:readonly, accounts, get, *, allow
14 p, role:admin, applications, create, */*, allow p, role:readonly, gpgkeys, get, *, allow
15 p, role:admin, applications, create, */*, allow
16 p, role:admin, applications, update, */*, allow
17 p, role:admin, applications, delete, */*, allow
18 p, role:admin, applications, sync, */*, allow
33 p, role:admin, accounts, update, *, allow
34 g, role:admin, role:readonly p, role:admin, gpgkeys, create, *, allow
35 g, admin, role:admin p, role:admin, gpgkeys, delete, *, allow
36 g, role:admin, role:readonly
37 g, admin, role:admin
38
39
40

View file

@ -124,7 +124,7 @@
"tags": [
"AccountService"
],
"operationId": "CreateTokenMixin9",
"operationId": "CreateTokenMixin10",
"parameters": [
{
"type": "string",
@ -156,7 +156,7 @@
"tags": [
"AccountService"
],
"operationId": "DeleteTokenMixin9",
"operationId": "DeleteTokenMixin10",
"parameters": [
{
"type": "string",
@ -187,7 +187,7 @@
"ApplicationService"
],
"summary": "List returns list of applications",
"operationId": "ListMixin8",
"operationId": "List",
"parameters": [
{
"type": "string",
@ -237,7 +237,7 @@
"ApplicationService"
],
"summary": "Create creates an application",
"operationId": "CreateMixin8",
"operationId": "Create",
"parameters": [
{
"name": "body",
@ -264,7 +264,7 @@
"ApplicationService"
],
"summary": "Update updates an application",
"operationId": "UpdateMixin8",
"operationId": "Update",
"parameters": [
{
"type": "string",
@ -395,7 +395,7 @@
"ApplicationService"
],
"summary": "Get returns an application by name",
"operationId": "GetMixin8",
"operationId": "GetMixin1",
"parameters": [
{
"type": "string",
@ -445,7 +445,7 @@
"ApplicationService"
],
"summary": "Delete deletes an application",
"operationId": "DeleteMixin8",
"operationId": "Delete",
"parameters": [
{
"type": "string",
@ -1084,7 +1084,7 @@
"ClusterService"
],
"summary": "List returns list of clusters",
"operationId": "List",
"operationId": "ListMixin5",
"parameters": [
{
"type": "string",
@ -1111,7 +1111,7 @@
"ClusterService"
],
"summary": "Create creates a cluster",
"operationId": "Create",
"operationId": "CreateMixin5",
"parameters": [
{
"name": "body",
@ -1138,7 +1138,7 @@
"ClusterService"
],
"summary": "Update updates a cluster",
"operationId": "Update",
"operationId": "UpdateMixin5",
"parameters": [
{
"type": "string",
@ -1171,7 +1171,7 @@
"ClusterService"
],
"summary": "Get returns a cluster by server address",
"operationId": "GetMixin2",
"operationId": "GetMixin5",
"parameters": [
{
"type": "string",
@ -1199,7 +1199,7 @@
"ClusterService"
],
"summary": "Delete deletes a cluster",
"operationId": "Delete",
"operationId": "DeleteMixin5",
"parameters": [
{
"type": "string",
@ -1243,6 +1243,96 @@
}
}
},
"/api/v1/gpgkeys": {
"get": {
"tags": [
"GPGKeyService"
],
"summary": "List all available repository certificates",
"operationId": "ListMixin2",
"parameters": [
{
"type": "string",
"description": "The GPG key ID to query for.",
"name": "keyID",
"in": "query"
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/v1alpha1GnuPGPublicKeyList"
}
}
}
},
"post": {
"tags": [
"GPGKeyService"
],
"summary": "Create one or more GPG public keys in the server's configuration",
"operationId": "CreateMixin2",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1alpha1GnuPGPublicKey"
}
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/gpgkeyGnuPGPublicKeyCreateResponse"
}
}
}
},
"delete": {
"tags": [
"GPGKeyService"
],
"summary": "Delete specified GPG public key from the server's configuration",
"operationId": "DeleteMixin2",
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/gpgkeyGnuPGPublicKeyResponse"
}
}
}
}
},
"/api/v1/gpgkeys/{keyID}": {
"get": {
"tags": [
"GPGKeyService"
],
"summary": "Get information about specified GPG public key from the server",
"operationId": "GetMixin2",
"parameters": [
{
"type": "string",
"name": "keyID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/v1alpha1GnuPGPublicKey"
}
}
}
}
},
"/api/v1/projects": {
"get": {
"tags": [
@ -1704,7 +1794,7 @@
"RepositoryService"
],
"summary": "Get returns a repository or its credentials",
"operationId": "GetMixin3",
"operationId": "Get",
"parameters": [
{
"type": "string",
@ -1886,7 +1976,7 @@
"SessionService"
],
"summary": "Create a new JWT for authentication and set a cookie if using HTTP.",
"operationId": "CreateMixin10",
"operationId": "CreateMixin11",
"parameters": [
{
"name": "body",
@ -1911,7 +2001,7 @@
"SessionService"
],
"summary": "Delete an existing JWT cookie if using HTTP.",
"operationId": "DeleteMixin10",
"operationId": "DeleteMixin11",
"responses": {
"200": {
"description": "(empty)",
@ -1945,7 +2035,7 @@
"SettingsService"
],
"summary": "Get returns Argo CD settings",
"operationId": "Get",
"operationId": "GetMixin8",
"responses": {
"200": {
"description": "(empty)",
@ -2440,6 +2530,26 @@
}
}
},
"gpgkeyGnuPGPublicKeyCreateResponse": {
"type": "object",
"title": "Response to a public key creation request",
"properties": {
"created": {
"$ref": "#/definitions/v1alpha1GnuPGPublicKeyList"
},
"skipped": {
"type": "array",
"title": "List of key IDs that haven been skipped because they already exist on the server",
"items": {
"type": "string"
}
}
}
},
"gpgkeyGnuPGPublicKeyResponse": {
"type": "object",
"title": "Generic (empty) response for GPG public key CRUD requests"
},
"oidcClaim": {
"type": "object",
"properties": {
@ -2687,6 +2797,10 @@
},
"sourceType": {
"type": "string"
},
"verifyResult": {
"type": "string",
"title": "Raw response of git verify-commit operation (always the empty string for Helm)"
}
}
},
@ -3247,6 +3361,13 @@
"$ref": "#/definitions/v1alpha1ProjectRole"
}
},
"signatureKeys": {
"type": "array",
"title": "List of PGP key IDs that commits to be synced to must be signed with",
"items": {
"$ref": "#/definitions/v1alpha1SignatureKey"
}
},
"sourceRepos": {
"type": "array",
"title": "SourceRepos contains list of repository URLs which can be used for deployment",
@ -3775,6 +3896,51 @@
}
}
},
"v1alpha1GnuPGPublicKey": {
"type": "object",
"title": "GnuPGPublicKey is a representation of a GnuPG public key",
"properties": {
"fingerprint": {
"type": "string",
"title": "Fingerprint of the key"
},
"keyData": {
"type": "string",
"title": "Key data"
},
"keyID": {
"type": "string",
"title": "KeyID in hexadecimal string format"
},
"owner": {
"type": "string",
"title": "Owner identification"
},
"subType": {
"type": "string",
"title": "Key sub type (e.g. rsa4096)"
},
"trust": {
"type": "string",
"title": "Trust level"
}
}
},
"v1alpha1GnuPGPublicKeyList": {
"type": "object",
"title": "GnuPGPublicKeyList is a collection of GnuPGPublicKey objects",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1GnuPGPublicKey"
}
},
"metadata": {
"$ref": "#/definitions/v1ListMeta"
}
}
},
"v1alpha1HealthStatus": {
"type": "object",
"properties": {
@ -4521,6 +4687,10 @@
"type": "string",
"title": "the message associated with the revision,\nprobably the commit message,\nthis is truncated to the first newline or 64 characters (which ever comes first)"
},
"signatureInfo": {
"type": "string",
"title": "If revision was signed with GPG, and signature verification is enabled,\nthis contains a hint on the signer"
},
"tags": {
"type": "array",
"title": "tags on the revision,\nnote - tags can move from one revision to another",
@ -4530,6 +4700,16 @@
}
}
},
"v1alpha1SignatureKey": {
"type": "object",
"title": "SignatureKey is the specification of a key required to verify commit signatures with",
"properties": {
"keyID": {
"type": "string",
"title": "The ID of the key in hexadecimal notation"
}
}
},
"v1alpha1SyncOperation": {
"description": "SyncOperation contains sync operation details.",
"type": "object",

View file

@ -19,14 +19,24 @@ import (
"github.com/argoproj/argo-cd/reposerver/metrics"
cacheutil "github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/gpg"
"github.com/argoproj/argo-cd/util/tls"
)
const (
// CLIName is the name of the CLI
cliName = "argocd-repo-server"
cliName = "argocd-repo-server"
gnuPGSourcePath = "/app/config/gpg/source"
)
func getGnuPGSourcePath() string {
if path := os.Getenv("ARGOCD_GPG_DATA_PATH"); path != "" {
return path
} else {
return gnuPGSourcePath
}
}
func newCommand() *cobra.Command {
var (
logFormat string
@ -63,6 +73,19 @@ func newCommand() *cobra.Command {
http.Handle("/metrics", metricsServer.GetHandler())
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
if gpg.IsGPGEnabled() {
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
err = gpg.InitializeGnuPG()
errors.CheckError(err)
log.Infof("Populating GnuPG keyring with keys from %s", getGnuPGSourcePath())
added, removed, err := gpg.SyncKeyRingFromDirectory(getGnuPGSourcePath())
errors.CheckError(err)
log.Infof("Loaded %d (and removed %d) keys from keyring", len(added), len(removed))
go func() { errors.CheckError(reposerver.StartGPGWatcher(getGnuPGSourcePath())) }()
}
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
stats.RegisterStackDumper()
stats.StartStatsTicker(10 * time.Minute)

162
cmd/argocd/commands/gpg.go Normal file
View file

@ -0,0 +1,162 @@
package commands
import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/spf13/cobra"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
// NewGPGCommand returns a new instance of an `argocd repo` command
func NewGPGCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "gpg",
Short: "Manage GPG keys used for signature verification",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
Example: ``,
}
command.AddCommand(NewGPGListCommand(clientOpts))
command.AddCommand(NewGPGGetCommand(clientOpts))
command.AddCommand(NewGPGAddCommand(clientOpts))
command.AddCommand(NewGPGDeleteCommand(clientOpts))
return command
}
// NewGPGListCommand lists all configured public keys from the server
func NewGPGListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List configured GPG public keys",
Run: func(c *cobra.Command, args []string) {
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
keys, err := gpgIf.List(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(keys.Items, output, false)
errors.CheckError(err)
case "wide", "":
printKeyTable(keys.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
// NewGPGGetCommand retrieves a single public key from the server
func NewGPGGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "get KEYID",
Short: "Get the GPG public key with ID <KEYID> from the server",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
}
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
key, err := gpgIf.Get(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(key, output, false)
errors.CheckError(err)
case "wide", "":
fmt.Printf("Key ID: %s\n", key.KeyID)
fmt.Printf("Key fingerprint: %s\n", key.Fingerprint)
fmt.Printf("Key subtype: %s\n", strings.ToUpper(key.SubType))
fmt.Printf("Key owner: %s\n", key.Owner)
fmt.Printf("Key data follows until EOF:\n%s\n", key.KeyData)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
// NewGPGAddCommand adds a public key to the server's configuration
func NewGPGAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
fromFile string
)
var command = &cobra.Command{
Use: "add",
Short: "Adds a GPG public key to the server's keyring",
Run: func(c *cobra.Command, args []string) {
if fromFile == "" {
errors.CheckError(fmt.Errorf("--from is mandatory"))
}
keyData, err := ioutil.ReadFile(fromFile)
if err != nil {
errors.CheckError(err)
}
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
resp, err := gpgIf.Create(context.Background(), &gpgkeypkg.GnuPGPublicKeyCreateRequest{Publickey: &appsv1.GnuPGPublicKey{KeyData: string(keyData)}})
errors.CheckError(err)
fmt.Printf("Created %d key(s) from input file", len(resp.Created.Items))
if len(resp.Skipped) > 0 {
fmt.Printf(", and %d key(s) were skipped because they exist already", len(resp.Skipped))
}
fmt.Printf(".\n")
},
}
command.Flags().StringVarP(&fromFile, "from", "f", "", "Path to the file that contains the GPG public key to import")
return command
}
// NewGPGDeleteCommand removes a key from the server's keyring
func NewGPGDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rm KEYID",
Short: "Removes a GPG public key from the server's keyring",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
}
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
_, err := gpgIf.Delete(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
errors.CheckError(err)
fmt.Printf("Deleted key with key ID %s\n", args[0])
},
}
return command
}
// Print table of certificate info
func printKeyTable(keys []appsv1.GnuPGPublicKey) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "KEYID\tTYPE\tIDENTITY\n")
for _, k := range keys {
fmt.Fprintf(w, "%s\t%s\t%s\n", k.KeyID, strings.ToUpper(k.SubType), k.Owner)
}
_ = w.Flush()
}

View file

@ -28,12 +28,14 @@ import (
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/gpg"
)
type projectOpts struct {
description string
destinations []string
sources []string
signatureKeys []string
orphanedResourcesEnabled bool
orphanedResourcesWarn bool
}
@ -60,6 +62,18 @@ func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
return destinations
}
// TODO: Get configured keys and emit warning when a key is specified that is not configured
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)
}
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
}
return signatureKeys
}
// NewProjectCommand returns a new instance of an `argocd proj` command
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
@ -77,6 +91,8 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.AddCommand(NewProjectListCommand(clientOpts))
command.AddCommand(NewProjectSetCommand(clientOpts))
command.AddCommand(NewProjectEditCommand(clientOpts))
command.AddCommand(NewProjectAddSignatureKeyCommand(clientOpts))
command.AddCommand(NewProjectRemoveSignatureKeyCommand(clientOpts))
command.AddCommand(NewProjectAddDestinationCommand(clientOpts))
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
@ -94,6 +110,7 @@ func addProjFlags(command *cobra.Command, opts *projectOpts) {
command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted source repository URL")
command.Flags().StringSliceVar(&opts.signatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should be a warning condition when orphaned resources detected")
}
@ -133,6 +150,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
Short: "Create a project",
Run: func(c *cobra.Command, args []string) {
var proj v1alpha1.AppProject
fmt.Printf("EE: %d/%v\n", len(opts.signatureKeys), opts.signatureKeys)
if fileURL == "-" {
// read stdin
reader := bufio.NewReader(os.Stdin)
@ -165,6 +183,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
Description: opts.description,
Destinations: opts.GetDestinations(),
SourceRepos: opts.sources,
SignatureKeys: opts.GetSignatureKeys(),
OrphanedResources: getOrphanedResourcesSettings(c, opts),
},
}
@ -215,6 +234,8 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
proj.Spec.Destinations = opts.GetDestinations()
case "src":
proj.Spec.SourceRepos = opts.sources
case "signature-keys":
proj.Spec.SignatureKeys = opts.GetSignatureKeys()
case "orphaned-resources", "orphaned-resources-warn":
proj.Spec.OrphanedResources = getOrphanedResourcesSettings(c, opts)
}
@ -233,6 +254,81 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
return command
}
// NewProjectAddSignatureKeyCommand returns a new instance of an `argocd proj add-destination` command
func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "add-signature-key PROJECT KEY-ID",
Short: "Add GnuPG signature key to project",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
signatureKey := args[1]
if !gpg.IsShortKeyID(signatureKey) && !gpg.IsLongKeyID(signatureKey) {
log.Fatalf("%s is not a valid GnuPG key ID", signatureKey)
}
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for _, key := range proj.Spec.SignatureKeys {
if key.KeyID == signatureKey {
log.Fatal("Specified signature key is already defined in project")
}
}
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys, v1alpha1.SignatureKey{KeyID: signatureKey})
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
// NewProjectRemoveDestinationCommand returns a new instance of an `argocd proj remove-destination` command
func NewProjectRemoveSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "remove-signature-key PROJECT KEY-ID",
Short: "Remove GnuPG signature key from project",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
signatureKey := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
index := -1
for i, key := range proj.Spec.SignatureKeys {
if key.KeyID == signatureKey {
index = i
break
}
}
if index == -1 {
log.Fatal("Specified signature key is not configured for project")
} else {
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys[:index], proj.Spec.SignatureKeys[index+1:]...)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
}
return command
}
// NewProjectAddDestinationCommand returns a new instance of an `argocd proj add-destination` command
func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
@ -571,7 +667,7 @@ func printProjectNames(projects []v1alpha1.AppProject) {
// Print table of project info
func printProjectTable(projects []v1alpha1.AppProject) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tORPHANED-RESOURCES\n")
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tSIGNATURE-KEYS\tORPHANED-RESOURCES\n")
for _, p := range projects {
printProjectLine(w, &p)
}
@ -616,7 +712,7 @@ func formatOrphanedResources(p *v1alpha1.AppProject) string {
}
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys string
switch len(p.Spec.Destinations) {
case 0:
destinations = "<none>"
@ -647,7 +743,13 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
default:
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
}
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, formatOrphanedResources(p))
switch len(p.Spec.SignatureKeys) {
case 0:
signatureKeys = "<none>"
default:
signatureKeys = fmt.Sprintf("%d key(s)", len(p.Spec.SignatureKeys))
}
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys, formatOrphanedResources(p))
}
func printProject(p *v1alpha1.AppProject) {
@ -695,6 +797,18 @@ func printProject(p *v1alpha1.AppProject) {
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
}
// Print required signature keys
signatureKeysStr := "<none>"
if len(p.Spec.SignatureKeys) > 0 {
kids := make([]string, 0)
for _, key := range p.Spec.SignatureKeys {
kids = append(kids, key.KeyID)
}
signatureKeysStr = strings.Join(kids, ", ")
}
fmt.Printf(printProjFmtStr, "Signature keys:", signatureKeysStr)
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
}

View file

@ -53,6 +53,7 @@ func NewCommand() *cobra.Command {
command.AddCommand(NewAccountCommand(&clientOpts))
command.AddCommand(NewLogoutCommand(&clientOpts))
command.AddCommand(NewCertCommand(&clientOpts))
command.AddCommand(NewGPGCommand(&clientOpts))
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
errors.CheckError(err)

View file

@ -25,6 +25,7 @@ const (
ArgoCDKnownHostsConfigMapName = "argocd-ssh-known-hosts-cm"
// Contains TLS certificate data for connecting repositories. Will get mounted as volume to pods
ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
ArgoCDGPGKeysConfigMapName = "argocd-gpg-keys-cm"
)
// Some default configurables
@ -50,6 +51,8 @@ const (
DefaultPathSSHConfig = "/app/config/ssh"
// Default name for the SSH known hosts file
DefaultSSHKnownHostsName = "ssh_known_hosts"
// Default path to GnuPG home directory
DefaultGnuPgHomePath = "/app/config/gpg/keys"
)
// Argo CD application related constants
@ -148,6 +151,8 @@ const (
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
// EnvGnuPGHome is the path to ArgoCD's GnuPG keyring for signature verification
EnvGnuPGHome = "ARGOCD_GNUPGHOME"
)
const (
@ -160,6 +165,15 @@ const (
CacheVersion = "1.0.0"
)
// GetGnuPGHomePath retrieves the path to use for GnuPG home directory, which is either taken from GNUPGHOME environment or a default value
func GetGnuPGHomePath() string {
if gnuPgHome := os.Getenv(EnvGnuPGHome); gnuPgHome == "" {
return DefaultGnuPgHomePath
} else {
return gnuPgHome
}
}
var (
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
K8sClientConfigQPS float32 = 50

View file

@ -30,6 +30,7 @@ import (
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/util/argo"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/gpg"
argohealth "github.com/argoproj/argo-cd/util/health"
"github.com/argoproj/argo-cd/util/settings"
"github.com/argoproj/argo-cd/util/stats"
@ -93,7 +94,7 @@ type appStateManager struct {
namespace string
}
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache, verifySignature bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
ts := stats.NewTimingStats()
helmRepos, err := m.db.ListHelmRepositories(context.Background())
if err != nil {
@ -152,6 +153,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
KustomizeOptions: kustomizeOptions,
KubeVersion: serverVersion,
ApiVersions: argo.APIGroupsToVersions(apiGroups),
VerifySignature: verifySignature,
})
if err != nil {
return nil, nil, err
@ -242,6 +244,50 @@ func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string,
return appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
}
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
// revision.
func verifyGnuPGSignature(revision string, project *appv1.AppProject, manifestInfo *apiclient.ManifestResponse) []appv1.ApplicationCondition {
now := metav1.Now()
conditions := make([]appv1.ApplicationCondition, 0)
// We need to have some data in the verificatin result to parse, otherwise there was no signature
if manifestInfo.VerifyResult != "" {
verifyResult, err := gpg.ParseGitCommitVerification(manifestInfo.VerifyResult)
if err != nil {
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
log.Errorf("Error while verifying git commit for revision %s: %s", revision, err.Error())
} else {
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
}
// CompareAppState compares application git state to the live app state, using the specified
// revision and supplied source. If revision or overrides are empty, then compares against
// revision and overrides in the app spec.
@ -261,6 +307,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
}
}
// When signature keys are defined in the project spec, we need to verify the signature on the Git revision
verifySignature := false
if project.Spec.SignatureKeys != nil && len(project.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
verifySignature = true
}
// 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)
@ -273,18 +325,27 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
now := metav1.Now()
if len(localManifests) == 0 {
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache, verifySignature)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
failedToLoadObjs = true
}
} else {
targetObjs, err = unmarshalManifests(localManifests)
if err != nil {
// Prevent applying local manifests for now when signature verification is enabled
// 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"
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
failedToLoadObjs = true
} else {
targetObjs, err = unmarshalManifests(localManifests)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
failedToLoadObjs = true
}
}
manifestInfo = nil
}
@ -454,6 +515,13 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: 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 oppinion about the result
// and stop processing if we do not agree about the outcome.
if gpg.IsGPGEnabled() && verifySignature && manifestInfo != nil {
conditions = append(conditions, verifyGnuPGSignature(revision, project, manifestInfo)...)
}
compRes := comparisonResult{
syncStatus: &syncStatus,
healthStatus: healthStatus,

View file

@ -2,6 +2,8 @@ package controller
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"time"
@ -440,3 +442,280 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime)
}
// helper function to read contents of a file to string
// panics on error
func mustReadFile(path string) string {
b, err := ioutil.ReadFile(path)
if err != nil {
panic(err.Error())
}
return string(b)
}
var signedProj = argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: test.FakeArgoCDNamespace,
},
Spec: argoappv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "*",
},
},
SignatureKeys: []argoappv1.SignatureKey{
{
KeyID: "4AEE18F83AFDEB23",
},
},
},
}
func TestSignedResponseNoSignatureRequired(t *testing.T) {
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
os.Setenv("ARGOCD_GPG_ENABLED", "true")
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
// 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(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
// 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"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
}
func TestSignedResponseSignatureRequired(t *testing.T) {
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
os.Setenv("ARGOCD_GPG_ENABLED", "true")
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
// We have a good signature response, valid key, and signing is required - 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"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
// We have a bad 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_bad.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
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(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
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(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
}
// We have a good signature and signing is required, but key is not allowed - 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"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
testProj := signedProj
testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
compRes := ctrl.appStateManager.CompareAppState(app, &testProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
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(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
}
os.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(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
// Signature required and local manifests supplied and GPG subystem is disabled - 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{""}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
}

View file

@ -0,0 +1,305 @@
# GnuPG signature verifcation
## 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` and `argocd-application-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"
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.
## 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.
## 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
Similarily, 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: *
Whitelisted Cluster Resources: */*
Blacklisted 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` and
`argocd-application-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.

1
go.mod
View file

@ -19,6 +19,7 @@ require (
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
github.com/dustin/go-humanize v1.0.0
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/fsnotify/fsnotify v1.4.7
github.com/ghodss/yaml v1.0.0
github.com/go-openapi/loads v0.19.2
github.com/go-openapi/runtime v0.19.0

View file

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"time"
@ -61,10 +62,33 @@ func newCommand() *cobra.Command {
log.Warnf("Failed to create directory: %v", err)
return
}
for name, data := range cm.Data {
err := ioutil.WriteFile(path.Join(destPath, name), []byte(data), 0644)
// Remove files that do not exist in ConfigMap anymore
err = filepath.Walk(destPath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if err != nil {
log.Warnf("Failed to create file: %v", err)
log.Warnf("Error walking path %s: %v", path, err)
}
p := filepath.Base(path)
if _, ok := cm.Data[p]; !ok {
log.Infof("Removing file '%s'", path)
err := os.Remove(path)
if err != nil {
log.Warnf("Failed to remove file %s: %v", path, err)
}
}
return nil
})
if err != nil {
log.Fatalf("Error: %v", err)
}
// Create or update files that are specified in ConfigMap
for name, data := range cm.Data {
p := path.Join(destPath, name)
err := ioutil.WriteFile(p, []byte(data), 0644)
if err != nil {
log.Warnf("Failed to create file %s: %v", p, err)
}
}
}

View file

@ -121,7 +121,7 @@ clean_swagger() {
}
echo "If additional types are added, the number of expected collisions may need to be increased"
EXPECTED_COLLISION_COUNT=32
EXPECTED_COLLISION_COUNT=33
collect_swagger server ${EXPECTED_COLLISION_COUNT}
clean_swagger server
clean_swagger reposerver

46
hack/git-verify-wrapper.sh Executable file
View file

@ -0,0 +1,46 @@
#!/bin/sh
# Wrapper script to perform GPG signature validation on git commit SHAs and
# annotated tags.
#
# We capture stderr to stdout, so we can have the output in the logs. Also,
# we ignore error codes that are emitted if signature verification failed.
#
if test "$1" = ""; then
echo "Wrong usage of git-verify-wrapper.sh" >&2
exit 1
fi
REVISION="$1"
TYPE=
# Figure out we have an annotated tag or a commit SHA
if git describe --exact-match "${REVISION}" >/dev/null 2>&1; then
IFS=''
TYPE=tag
OUTPUT=$(git verify-tag "$REVISION" 2>&1)
RET=$?
else
IFS=''
TYPE=commit
OUTPUT=$(git verify-commit "$REVISION" 2>&1)
RET=$?
fi
case "$RET" in
0)
echo "$OUTPUT"
;;
1)
# git verify-tag emits error messages if no signature is found on tag,
# which we don't want in the output.
if test "$TYPE" = "tag" -a "${OUTPUT%%:*}" = "error"; then
OUTPUT=""
fi
echo "$OUTPUT"
RET=0
;;
*)
echo "$OUTPUT" >&2
;;
esac
exit $RET

19
hack/gpg-wrapper.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
# Simple wrapper around gpg to prevent exit code != 0
ARGS=$*
OUTPUT=$(gpg $ARGS 2>&1)
IFS=''
RET=$?
case "$RET" in
0)
echo $OUTPUT
;;
1)
echo $OUTPUT
RET=0
;;
*)
echo $OUTPUT >&2
;;
esac
exit $RET

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-gpg-keys-cm
app.kubernetes.io/part-of: argocd
name: argocd-gpg-keys-cm

View file

@ -7,3 +7,4 @@ resources:
- argocd-rbac-cm.yaml
- argocd-ssh-known-hosts-cm.yaml
- argocd-tls-certs-cm.yaml
- argocd-gpg-keys-cm.yaml

View file

@ -43,6 +43,8 @@ spec:
mountPath: /app/config/ssh
- name: tls-certs
mountPath: /app/config/tls
- name: gpg-keys
mountPath: /app/config/gpg/source
volumes:
- name: ssh-known-hosts
configMap:
@ -50,3 +52,6 @@ spec:
- name: tls-certs
configMap:
name: argocd-tls-certs-cm
- name: gpg-keys
configMap:
name: argocd-gpg-keys-cm

View file

@ -170,6 +170,20 @@ spec:
- name
type: object
type: array
signatureKeys:
description: List of PGP key IDs that commits to be synced to must be
signed with
items:
description: SignatureKey is the specification of a key required to
verify commit signatures with
properties:
keyID:
description: The ID of the key in hexadecimal notation
type: string
required:
- keyID
type: object
type: array
sourceRepos:
description: SourceRepos contains list of repository URLs which can
be used for deployment

View file

@ -1904,6 +1904,20 @@ spec:
- name
type: object
type: array
signatureKeys:
description: List of PGP key IDs that commits to be synced to must be
signed with
items:
description: SignatureKey is the specification of a key required to
verify commit signatures with
properties:
keyID:
description: The ID of the key in hexadecimal notation
type: string
required:
- keyID
type: object
type: array
sourceRepos:
description: SourceRepos contains list of repository URLs which can
be used for deployment
@ -2539,6 +2553,14 @@ metadata:
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-gpg-keys-cm
app.kubernetes.io/part-of: argocd
name: argocd-gpg-keys-cm
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-rbac-cm
@ -3045,6 +3067,8 @@ spec:
name: ssh-known-hosts
- mountPath: /app/config/tls
name: tls-certs
- mountPath: /app/config/gpg/source
name: gpg-keys
volumes:
- configMap:
name: argocd-ssh-known-hosts-cm
@ -3052,6 +3076,9 @@ spec:
- configMap:
name: argocd-tls-certs-cm
name: tls-certs
- configMap:
name: argocd-gpg-keys-cm
name: gpg-keys
---
apiVersion: apps/v1
kind: Deployment

View file

@ -1904,6 +1904,20 @@ spec:
- name
type: object
type: array
signatureKeys:
description: List of PGP key IDs that commits to be synced to must be
signed with
items:
description: SignatureKey is the specification of a key required to
verify commit signatures with
properties:
keyID:
description: The ID of the key in hexadecimal notation
type: string
required:
- keyID
type: object
type: array
sourceRepos:
description: SourceRepos contains list of repository URLs which can
be used for deployment
@ -2454,6 +2468,14 @@ metadata:
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-gpg-keys-cm
app.kubernetes.io/part-of: argocd
name: argocd-gpg-keys-cm
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-rbac-cm
@ -2960,6 +2982,8 @@ spec:
name: ssh-known-hosts
- mountPath: /app/config/tls
name: tls-certs
- mountPath: /app/config/gpg/source
name: gpg-keys
volumes:
- configMap:
name: argocd-ssh-known-hosts-cm
@ -2967,6 +2991,9 @@ spec:
- configMap:
name: argocd-tls-certs-cm
name: tls-certs
- configMap:
name: argocd-gpg-keys-cm
name: gpg-keys
---
apiVersion: apps/v1
kind: Deployment

View file

@ -1904,6 +1904,20 @@ spec:
- name
type: object
type: array
signatureKeys:
description: List of PGP key IDs that commits to be synced to must be
signed with
items:
description: SignatureKey is the specification of a key required to
verify commit signatures with
properties:
keyID:
description: The ID of the key in hexadecimal notation
type: string
required:
- keyID
type: object
type: array
sourceRepos:
description: SourceRepos contains list of repository URLs which can
be used for deployment
@ -2234,6 +2248,14 @@ metadata:
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-gpg-keys-cm
app.kubernetes.io/part-of: argocd
name: argocd-gpg-keys-cm
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-rbac-cm
@ -2559,6 +2581,8 @@ spec:
name: ssh-known-hosts
- mountPath: /app/config/tls
name: tls-certs
- mountPath: /app/config/gpg/source
name: gpg-keys
volumes:
- configMap:
name: argocd-ssh-known-hosts-cm
@ -2566,6 +2590,9 @@ spec:
- configMap:
name: argocd-tls-certs-cm
name: tls-certs
- configMap:
name: argocd-gpg-keys-cm
name: gpg-keys
---
apiVersion: apps/v1
kind: Deployment

View file

@ -1904,6 +1904,20 @@ spec:
- name
type: object
type: array
signatureKeys:
description: List of PGP key IDs that commits to be synced to must be
signed with
items:
description: SignatureKey is the specification of a key required to
verify commit signatures with
properties:
keyID:
description: The ID of the key in hexadecimal notation
type: string
required:
- keyID
type: object
type: array
sourceRepos:
description: SourceRepos contains list of repository URLs which can
be used for deployment
@ -2149,6 +2163,14 @@ metadata:
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-gpg-keys-cm
app.kubernetes.io/part-of: argocd
name: argocd-gpg-keys-cm
---
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/name: argocd-rbac-cm
@ -2474,6 +2496,8 @@ spec:
name: ssh-known-hosts
- mountPath: /app/config/tls
name: tls-certs
- mountPath: /app/config/gpg/source
name: gpg-keys
volumes:
- configMap:
name: argocd-ssh-known-hosts-cm
@ -2481,6 +2505,9 @@ spec:
- configMap:
name: argocd-tls-certs-cm
name: tls-certs
- configMap:
name: argocd-gpg-keys-cm
name: gpg-keys
---
apiVersion: apps/v1
kind: Deployment

View file

@ -65,6 +65,7 @@ nav:
- user-guide/tool_detection.md
- user-guide/projects.md
- user-guide/private-repositories.md
- GnuPG verification: user-guide/gpg-verification.md
- user-guide/auto_sync.md
- user-guide/diffing.md
- user-guide/orphaned-resources.md

View file

@ -31,6 +31,7 @@ import (
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
@ -68,6 +69,8 @@ type Client interface {
NewCertClientOrDie() (io.Closer, certificatepkg.CertificateServiceClient)
NewClusterClient() (io.Closer, clusterpkg.ClusterServiceClient, error)
NewClusterClientOrDie() (io.Closer, clusterpkg.ClusterServiceClient)
NewGPGKeyClient() (io.Closer, gpgkeypkg.GPGKeyServiceClient, error)
NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient)
NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error)
NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient)
NewSessionClient() (io.Closer, sessionpkg.SessionServiceClient, error)
@ -559,6 +562,23 @@ func (c *client) NewClusterClientOrDie() (io.Closer, clusterpkg.ClusterServiceCl
return conn, clusterIf
}
func (c *client) NewGPGKeyClient() (io.Closer, gpgkeypkg.GPGKeyServiceClient, error) {
conn, closer, err := c.newConn()
if err != nil {
return nil, nil, err
}
gpgkeyIf := gpgkeypkg.NewGPGKeyServiceClient(conn)
return closer, gpgkeyIf, nil
}
func (c *client) NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient) {
conn, gpgkeyIf, err := c.NewGPGKeyClient()
if err != nil {
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
}
return conn, gpgkeyIf
}
func (c *client) NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error) {
conn, closer, err := c.newConn()
if err != nil {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,288 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: server/gpgkey/gpgkey.proto
/*
Package gpgkey is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package gpgkey
import (
"io"
"net/http"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var (
filter_GPGKeyService_List_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_GPGKeyService_List_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GnuPGPublicKeyQuery
var metadata runtime.ServerMetadata
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GPGKeyService_List_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func request_GPGKeyService_Get_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GnuPGPublicKeyQuery
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["keyID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "keyID")
}
protoReq.KeyID, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "keyID", err)
}
msg, err := client.Get(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
var (
filter_GPGKeyService_Create_0 = &utilities.DoubleArray{Encoding: map[string]int{"publickey": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
)
func request_GPGKeyService_Create_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GnuPGPublicKeyCreateRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Publickey); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GPGKeyService_Create_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Create(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
var (
filter_GPGKeyService_Delete_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_GPGKeyService_Delete_0(ctx context.Context, marshaler runtime.Marshaler, client GPGKeyServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GnuPGPublicKeyQuery
var metadata runtime.ServerMetadata
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_GPGKeyService_Delete_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Delete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
// RegisterGPGKeyServiceHandlerFromEndpoint is same as RegisterGPGKeyServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterGPGKeyServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterGPGKeyServiceHandler(ctx, mux, conn)
}
// RegisterGPGKeyServiceHandler registers the http handlers for service GPGKeyService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterGPGKeyServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterGPGKeyServiceHandlerClient(ctx, mux, NewGPGKeyServiceClient(conn))
}
// RegisterGPGKeyServiceHandler registers the http handlers for service GPGKeyService to "mux".
// The handlers forward requests to the grpc endpoint over the given implementation of "GPGKeyServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GPGKeyServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "GPGKeyServiceClient" to call the correct interceptors.
func RegisterGPGKeyServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GPGKeyServiceClient) error {
mux.Handle("GET", pattern_GPGKeyService_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GPGKeyService_List_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GPGKeyService_List_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_GPGKeyService_Get_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GPGKeyService_Get_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GPGKeyService_Get_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_GPGKeyService_Create_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GPGKeyService_Create_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GPGKeyService_Create_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_GPGKeyService_Delete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GPGKeyService_Delete_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GPGKeyService_Delete_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_GPGKeyService_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "gpgkeys"}, ""))
pattern_GPGKeyService_Get_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "gpgkeys", "keyID"}, ""))
pattern_GPGKeyService_Create_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "gpgkeys"}, ""))
pattern_GPGKeyService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "gpgkeys"}, ""))
)
var (
forward_GPGKeyService_List_0 = runtime.ForwardResponseMessage
forward_GPGKeyService_Get_0 = runtime.ForwardResponseMessage
forward_GPGKeyService_Create_0 = runtime.ForwardResponseMessage
forward_GPGKeyService_Delete_0 = runtime.ForwardResponseMessage
)

View file

@ -4,6 +4,7 @@ API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/appli
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,NamespaceResourceBlacklist
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,NamespaceResourceWhitelist
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,Roles
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,SignatureKeys
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,AppProjectSpec,SourceRepos
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ApplicationList,Items
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ApplicationSourceHelm,FileParameters
@ -24,6 +25,7 @@ API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/appli
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ClusterList,Items
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,Command,Args
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,Command,Command
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,GnuPGPublicKeyList,Items
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,Operation,Info
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ProjectRole,Groups
API rule violation: list_type_missing,github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1,ProjectRole,JWTTokens

File diff suppressed because it is too large Load diff

View file

@ -74,6 +74,9 @@ message AppProjectSpec {
// NamespaceResourceWhitelist contains list of whitelisted namespace level resources
repeated k8s.io.apimachinery.pkg.apis.meta.v1.GroupKind namespaceResourceWhitelist = 9;
// List of PGP key IDs that commits to be synced to must be signed with
repeated SignatureKey signatureKeys = 10;
}
// Application is a definition of Application resource.
@ -400,6 +403,34 @@ message EnvEntry {
optional string value = 2;
}
// GnuPGPublicKey is a representation of a GnuPG public key
message GnuPGPublicKey {
// KeyID in hexadecimal string format
optional string keyID = 1;
// Fingerprint of the key
optional string fingerprint = 2;
// Owner identification
optional string owner = 3;
// Trust level
optional string trust = 4;
// Key sub type (e.g. rsa4096)
optional string subType = 5;
// Key data
optional string keyData = 6;
}
// GnuPGPublicKeyList is a collection of GnuPGPublicKey objects
message GnuPGPublicKeyList {
optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
repeated GnuPGPublicKey items = 2;
}
message HealthStatus {
optional string status = 1;
@ -867,6 +898,16 @@ message RevisionMetadata {
// probably the commit message,
// this is truncated to the first newline or 64 characters (which ever comes first)
optional string message = 4;
// If revision was signed with GPG, and signature verification is enabled,
// this contains a hint on the signer
optional string signatureInfo = 5;
}
// SignatureKey is the specification of a key required to verify commit signatures with
message SignatureKey {
// The ID of the key in hexadecimal notation
optional string keyID = 1;
}
// SyncOperation contains sync operation details.

View file

@ -42,6 +42,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ConfigManagementPlugin": schema_pkg_apis_application_v1alpha1_ConfigManagementPlugin(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ConnectionState": schema_pkg_apis_application_v1alpha1_ConnectionState(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.EnvEntry": schema_pkg_apis_application_v1alpha1_EnvEntry(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKey": schema_pkg_apis_application_v1alpha1_GnuPGPublicKey(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKeyList": schema_pkg_apis_application_v1alpha1_GnuPGPublicKeyList(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.HealthStatus": schema_pkg_apis_application_v1alpha1_HealthStatus(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.HelmFileParameter": schema_pkg_apis_application_v1alpha1_HelmFileParameter(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.HelmParameter": schema_pkg_apis_application_v1alpha1_HelmParameter(ref),
@ -77,6 +79,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ResourceStatus": schema_pkg_apis_application_v1alpha1_ResourceStatus(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.RevisionHistory": schema_pkg_apis_application_v1alpha1_RevisionHistory(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.RevisionMetadata": schema_pkg_apis_application_v1alpha1_RevisionMetadata(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SignatureKey": schema_pkg_apis_application_v1alpha1_SignatureKey(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncOperation": schema_pkg_apis_application_v1alpha1_SyncOperation(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncOperationResource": schema_pkg_apis_application_v1alpha1_SyncOperationResource(ref),
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncOperationResult": schema_pkg_apis_application_v1alpha1_SyncOperationResult(ref),
@ -318,11 +321,24 @@ func schema_pkg_apis_application_v1alpha1_AppProjectSpec(ref common.ReferenceCal
},
},
},
"signatureKeys": {
SchemaProps: spec.SchemaProps{
Description: "List of PGP key IDs that commits to be synced to must be signed with",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SignatureKey"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ApplicationDestination", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.OrphanedResourcesMonitorSettings", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ProjectRole", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncWindow", "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind"},
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ApplicationDestination", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.OrphanedResourcesMonitorSettings", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.ProjectRole", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SignatureKey", "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.SyncWindow", "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind"},
}
}
@ -1438,6 +1454,95 @@ func schema_pkg_apis_application_v1alpha1_EnvEntry(ref common.ReferenceCallback)
}
}
func schema_pkg_apis_application_v1alpha1_GnuPGPublicKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "GnuPGPublicKey is a representation of a GnuPG public key",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"keyID": {
SchemaProps: spec.SchemaProps{
Description: "KeyID in hexadecimal string format",
Type: []string{"string"},
Format: "",
},
},
"fingerprint": {
SchemaProps: spec.SchemaProps{
Description: "Fingerprint of the key",
Type: []string{"string"},
Format: "",
},
},
"owner": {
SchemaProps: spec.SchemaProps{
Description: "Owner identification",
Type: []string{"string"},
Format: "",
},
},
"trust": {
SchemaProps: spec.SchemaProps{
Description: "Trust level",
Type: []string{"string"},
Format: "",
},
},
"subType": {
SchemaProps: spec.SchemaProps{
Description: "Key sub type (e.g. rsa4096)",
Type: []string{"string"},
Format: "",
},
},
"keyData": {
SchemaProps: spec.SchemaProps{
Description: "Key data",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"keyID"},
},
},
}
}
func schema_pkg_apis_application_v1alpha1_GnuPGPublicKeyList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "GnuPGPublicKeyList is a collection of GnuPGPublicKey objects",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"metadata": {
SchemaProps: spec.SchemaProps{
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKey"),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1.GnuPGPublicKey", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_application_v1alpha1_HealthStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -3018,6 +3123,13 @@ func schema_pkg_apis_application_v1alpha1_RevisionMetadata(ref common.ReferenceC
Format: "",
},
},
"signatureInfo": {
SchemaProps: spec.SchemaProps{
Description: "If revision was signed with GPG, and signature verification is enabled, this contains a hint on the signer",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"date"},
},
@ -3027,6 +3139,27 @@ func schema_pkg_apis_application_v1alpha1_RevisionMetadata(ref common.ReferenceC
}
}
func schema_pkg_apis_application_v1alpha1_SignatureKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "SignatureKey is the specification of a key required to verify commit signatures with",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"keyID": {
SchemaProps: spec.SchemaProps{
Description: "The ID of the key in hexadecimal notation",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"keyID"},
},
},
}
}
func schema_pkg_apis_application_v1alpha1_SyncOperation(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View file

@ -618,6 +618,9 @@ type RevisionMetadata struct {
// probably the commit message,
// this is truncated to the first newline or 64 characters (which ever comes first)
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`
// If revision was signed with GPG, and signature verification is enabled,
// this contains a hint on the signer
SignatureInfo string `json:"signatureInfo,omitempty" protobuf:"bytes,5,opt,name=signatureInfo"`
}
// SyncOperationResult represent result of sync operation
@ -1247,6 +1250,28 @@ type RepositoryCertificateList struct {
Items []RepositoryCertificate `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// GnuPGPublicKey is a representation of a GnuPG public key
type GnuPGPublicKey struct {
// KeyID in hexadecimal string format
KeyID string `json:"keyID" protobuf:"bytes,1,opt,name=keyID"`
// Fingerprint of the key
Fingerprint string `json:"fingerprint,omitempty" protobuf:"bytes,2,opt,name=fingerprint"`
// Owner identification
Owner string `json:"owner,omitempty" protobuf:"bytes,3,opt,name=owner"`
// Trust level
Trust string `json:"trust,omitempty" protobuf:"bytes,4,opt,name=trust"`
// Key sub type (e.g. rsa4096)
SubType string `json:"subType,omitempty" protobuf:"bytes,5,opt,name=subType"`
// Key data
KeyData string `json:"keyData,omitempty" protobuf:"bytes,6,opt,name=keyData"`
}
// GnuPGPublicKeyList is a collection of GnuPGPublicKey objects
type GnuPGPublicKeyList struct {
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []GnuPGPublicKey `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// AppProjectList is list of AppProject resources
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type AppProjectList struct {
@ -1540,6 +1565,12 @@ func (s *OrphanedResourcesMonitorSettings) IsWarn() bool {
return s.Warn == nil || *s.Warn
}
// SignatureKey is the specification of a key required to verify commit signatures with
type SignatureKey struct {
// The ID of the key in hexadecimal notation
KeyID string `json:"keyID" protobuf:"bytes,1,name=keyID"`
}
// AppProjectSpec is the specification of an AppProject
type AppProjectSpec struct {
// SourceRepos contains list of repository URLs which can be used for deployment
@ -1560,6 +1591,8 @@ type AppProjectSpec struct {
SyncWindows SyncWindows `json:"syncWindows,omitempty" protobuf:"bytes,8,opt,name=syncWindows"`
// NamespaceResourceWhitelist contains list of whitelisted namespace level resources
NamespaceResourceWhitelist []metav1.GroupKind `json:"namespaceResourceWhitelist,omitempty" protobuf:"bytes,9,opt,name=namespaceResourceWhitelist"`
// List of PGP key IDs that commits to be synced to must be signed with
SignatureKeys []SignatureKey `json:"signatureKeys,omitempty" protobuf:"bytes,10,opt,name=signatureKeys"`
}
// SyncWindows is a collection of sync windows in this project

View file

@ -137,6 +137,11 @@ func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
*out = make([]v1.GroupKind, len(*in))
copy(*out, *in)
}
if in.SignatureKeys != nil {
in, out := &in.SignatureKeys, &out.SignatureKeys
*out = make([]SignatureKey, len(*in))
copy(*out, *in)
}
return
}
@ -822,6 +827,44 @@ func (in *EnvEntry) DeepCopy() *EnvEntry {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GnuPGPublicKey) DeepCopyInto(out *GnuPGPublicKey) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GnuPGPublicKey.
func (in *GnuPGPublicKey) DeepCopy() *GnuPGPublicKey {
if in == nil {
return nil
}
out := new(GnuPGPublicKey)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GnuPGPublicKeyList) DeepCopyInto(out *GnuPGPublicKeyList) {
*out = *in
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]GnuPGPublicKey, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GnuPGPublicKeyList.
func (in *GnuPGPublicKeyList) DeepCopy() *GnuPGPublicKeyList {
if in == nil {
return nil
}
out := new(GnuPGPublicKeyList)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthStatus) DeepCopyInto(out *HealthStatus) {
*out = *in
@ -1648,6 +1691,22 @@ func (in *RevisionMetadata) DeepCopy() *RevisionMetadata {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SignatureKey) DeepCopyInto(out *SignatureKey) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SignatureKey.
func (in *SignatureKey) DeepCopy() *SignatureKey {
if in == nil {
return nil
}
out := new(SignatureKey)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SyncOperation) DeepCopyInto(out *SyncOperation) {
*out = *in

View file

@ -34,20 +34,22 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type ManifestRequest struct {
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
// revision, potentially un-resolved
Revision string `protobuf:"bytes,2,opt,name=revision,proto3" json:"revision,omitempty"`
NoCache bool `protobuf:"varint,3,opt,name=noCache,proto3" json:"noCache,omitempty"`
AppLabelKey string `protobuf:"bytes,4,opt,name=appLabelKey,proto3" json:"appLabelKey,omitempty"`
AppLabelValue string `protobuf:"bytes,5,opt,name=appLabelValue,proto3" json:"appLabelValue,omitempty"`
Namespace string `protobuf:"bytes,8,opt,name=namespace,proto3" json:"namespace,omitempty"`
ApplicationSource *v1alpha1.ApplicationSource `protobuf:"bytes,10,opt,name=applicationSource,proto3" json:"applicationSource,omitempty"`
Repos []*v1alpha1.Repository `protobuf:"bytes,11,rep,name=repos,proto3" json:"repos,omitempty"`
Plugins []*v1alpha1.ConfigManagementPlugin `protobuf:"bytes,12,rep,name=plugins,proto3" json:"plugins,omitempty"`
KustomizeOptions *v1alpha1.KustomizeOptions `protobuf:"bytes,13,opt,name=kustomizeOptions,proto3" json:"kustomizeOptions,omitempty"`
KubeVersion string `protobuf:"bytes,14,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"`
ApiVersions []string `protobuf:"bytes,15,rep,name=apiVersions,proto3" json:"apiVersions,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"`
NoCache bool `protobuf:"varint,3,opt,name=noCache,proto3" json:"noCache,omitempty"`
AppLabelKey string `protobuf:"bytes,4,opt,name=appLabelKey,proto3" json:"appLabelKey,omitempty"`
AppLabelValue string `protobuf:"bytes,5,opt,name=appLabelValue,proto3" json:"appLabelValue,omitempty"`
Namespace string `protobuf:"bytes,8,opt,name=namespace,proto3" json:"namespace,omitempty"`
ApplicationSource *v1alpha1.ApplicationSource `protobuf:"bytes,10,opt,name=applicationSource,proto3" json:"applicationSource,omitempty"`
Repos []*v1alpha1.Repository `protobuf:"bytes,11,rep,name=repos,proto3" json:"repos,omitempty"`
Plugins []*v1alpha1.ConfigManagementPlugin `protobuf:"bytes,12,rep,name=plugins,proto3" json:"plugins,omitempty"`
KustomizeOptions *v1alpha1.KustomizeOptions `protobuf:"bytes,13,opt,name=kustomizeOptions,proto3" json:"kustomizeOptions,omitempty"`
KubeVersion string `protobuf:"bytes,14,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"`
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"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ManifestRequest) Reset() { *m = ManifestRequest{} }
@ -167,13 +169,22 @@ func (m *ManifestRequest) GetApiVersions() []string {
return nil
}
func (m *ManifestRequest) GetVerifySignature() bool {
if m != nil {
return m.VerifySignature
}
return false
}
type ManifestResponse struct {
Manifests []string `protobuf:"bytes,1,rep,name=manifests,proto3" json:"manifests,omitempty"`
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
Server string `protobuf:"bytes,3,opt,name=server,proto3" json:"server,omitempty"`
// resolved revision
Revision string `protobuf:"bytes,4,opt,name=revision,proto3" json:"revision,omitempty"`
SourceType string `protobuf:"bytes,6,opt,name=sourceType,proto3" json:"sourceType,omitempty"`
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)
VerifyResult string `protobuf:"bytes,7,opt,name=verifyResult,proto3" json:"verifyResult,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -247,6 +258,13 @@ func (m *ManifestResponse) GetSourceType() string {
return ""
}
func (m *ManifestResponse) GetVerifyResult() string {
if m != nil {
return m.VerifyResult
}
return ""
}
// ListAppsRequest requests a repository directory structure
type ListAppsRequest struct {
Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"`
@ -1095,82 +1113,84 @@ func init() {
}
var fileDescriptor_dd8723cfcc820480 = []byte{
// 1194 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x57, 0x5b, 0x6f, 0x1b, 0xc5,
0x17, 0xcf, 0xda, 0x8e, 0x13, 0x1f, 0xf7, 0xe2, 0x4c, 0xfb, 0xef, 0x7f, 0x31, 0xa9, 0x65, 0x56,
0x80, 0x02, 0xa5, 0x6b, 0x12, 0x2a, 0x11, 0x15, 0xa9, 0x92, 0x49, 0xd2, 0x14, 0x39, 0x51, 0xd3,
0x0d, 0x54, 0xe2, 0x22, 0x55, 0x93, 0xf5, 0x64, 0x3d, 0x78, 0xbd, 0x3b, 0xec, 0x8c, 0x8d, 0xd2,
0x2f, 0x00, 0x12, 0x8f, 0x88, 0x17, 0x1e, 0xf9, 0x08, 0xbc, 0xf1, 0xce, 0x03, 0x8f, 0x7c, 0x04,
0x94, 0x47, 0x3e, 0x05, 0x9a, 0xd9, 0xdb, 0x78, 0xed, 0xe4, 0xc5, 0xbd, 0xbc, 0xd8, 0x33, 0x67,
0xce, 0x6d, 0xce, 0xf9, 0x9d, 0x33, 0x67, 0xe1, 0xdd, 0x88, 0xb0, 0x90, 0x93, 0x68, 0x42, 0xa2,
0x8e, 0x5a, 0x52, 0x11, 0x46, 0x67, 0xda, 0xd2, 0x66, 0x51, 0x28, 0x42, 0x04, 0x39, 0xa5, 0x79,
0xd3, 0x0b, 0xbd, 0x50, 0x91, 0x3b, 0x72, 0x15, 0x73, 0x34, 0xd7, 0xbd, 0x30, 0xf4, 0x7c, 0xd2,
0xc1, 0x8c, 0x76, 0x70, 0x10, 0x84, 0x02, 0x0b, 0x1a, 0x06, 0x3c, 0x39, 0xb5, 0x86, 0xdb, 0xdc,
0xa6, 0xa1, 0x3a, 0x75, 0xc3, 0x88, 0x74, 0x26, 0x9b, 0x1d, 0x8f, 0x04, 0x24, 0xc2, 0x82, 0xf4,
0x13, 0x9e, 0xcf, 0x3c, 0x2a, 0x06, 0xe3, 0x13, 0xdb, 0x0d, 0x47, 0x1d, 0x1c, 0x29, 0x13, 0xdf,
0xaa, 0xc5, 0x5d, 0xb7, 0xdf, 0x61, 0x43, 0x4f, 0x0a, 0xf3, 0x0e, 0x66, 0xcc, 0xa7, 0xae, 0x52,
0xde, 0x99, 0x6c, 0x62, 0x9f, 0x0d, 0xf0, 0x8c, 0x2a, 0xeb, 0xa7, 0x2a, 0x5c, 0x3f, 0xc4, 0x01,
0x3d, 0x25, 0x5c, 0x38, 0xe4, 0xbb, 0x31, 0xe1, 0x02, 0x7d, 0x09, 0x15, 0x79, 0x09, 0xd3, 0x68,
0x1b, 0x1b, 0xf5, 0xad, 0x3d, 0x3b, 0xb7, 0x66, 0xa7, 0xd6, 0xd4, 0xe2, 0x99, 0xdb, 0xb7, 0xd9,
0xd0, 0xb3, 0xa5, 0x35, 0x5b, 0xb3, 0x66, 0xa7, 0xd6, 0x6c, 0x27, 0x8b, 0x85, 0xa3, 0x54, 0xa2,
0x26, 0xac, 0x46, 0x64, 0x42, 0x39, 0x0d, 0x03, 0xb3, 0xd4, 0x36, 0x36, 0x6a, 0x4e, 0xb6, 0x47,
0x26, 0xac, 0x04, 0xe1, 0x0e, 0x76, 0x07, 0xc4, 0x2c, 0xb7, 0x8d, 0x8d, 0x55, 0x27, 0xdd, 0xa2,
0x36, 0xd4, 0x31, 0x63, 0x07, 0xf8, 0x84, 0xf8, 0x3d, 0x72, 0x66, 0x56, 0x94, 0xa0, 0x4e, 0x42,
0x6f, 0xc3, 0xd5, 0x74, 0xfb, 0x14, 0xfb, 0x63, 0x62, 0x2e, 0x2b, 0x9e, 0x69, 0x22, 0x5a, 0x87,
0x5a, 0x80, 0x47, 0x84, 0x33, 0xec, 0x12, 0x73, 0x55, 0x71, 0xe4, 0x04, 0xf4, 0x1c, 0xd6, 0xb4,
0x4b, 0x1c, 0x87, 0xe3, 0xc8, 0x25, 0x26, 0xa8, 0x18, 0x1c, 0x2c, 0x10, 0x83, 0x6e, 0x51, 0xa7,
0x33, 0x6b, 0x06, 0x7d, 0x0d, 0xcb, 0x0a, 0x37, 0x66, 0xbd, 0x5d, 0x7e, 0x71, 0x31, 0x8f, 0x75,
0xa2, 0x21, 0xac, 0x30, 0x7f, 0xec, 0xd1, 0x80, 0x9b, 0x57, 0x94, 0xfa, 0x27, 0x0b, 0xa8, 0xdf,
0x09, 0x83, 0x53, 0xea, 0x1d, 0xe2, 0x00, 0x7b, 0x64, 0x44, 0x02, 0x71, 0xa4, 0x34, 0x3b, 0xa9,
0x05, 0xf4, 0x3d, 0x34, 0x86, 0x63, 0x2e, 0xc2, 0x11, 0x7d, 0x4e, 0x1e, 0x33, 0x85, 0x6c, 0xf3,
0xaa, 0x0a, 0x62, 0x6f, 0x01, 0xab, 0xbd, 0x82, 0x4a, 0x67, 0xc6, 0x88, 0x04, 0xc9, 0x70, 0x7c,
0x42, 0x9e, 0x92, 0x48, 0xa1, 0xeb, 0x5a, 0x0c, 0x12, 0x8d, 0x14, 0xc3, 0x88, 0x26, 0x3b, 0x6e,
0x5e, 0x6f, 0x97, 0x63, 0x18, 0x65, 0x24, 0xeb, 0x37, 0x03, 0x1a, 0x79, 0x35, 0x70, 0x16, 0x06,
0x5c, 0xa1, 0x66, 0x94, 0xd0, 0xb8, 0x69, 0x28, 0xa1, 0x9c, 0x30, 0x8d, 0xa9, 0x52, 0x11, 0x53,
0xb7, 0xa0, 0x1a, 0xf7, 0x0c, 0x05, 0xe9, 0x9a, 0x93, 0xec, 0xa6, 0xea, 0xa0, 0x52, 0xa8, 0x83,
0x16, 0x00, 0x57, 0xa8, 0xf8, 0xfc, 0x8c, 0x11, 0xb3, 0xaa, 0x4e, 0x35, 0x8a, 0xf5, 0xa3, 0x01,
0xd7, 0x0f, 0x28, 0x17, 0x5d, 0xc6, 0xf8, 0xeb, 0x2d, 0x59, 0x6b, 0x0c, 0x2b, 0x5d, 0xc6, 0xa4,
0x33, 0x68, 0x13, 0x2a, 0x98, 0xb1, 0x38, 0x40, 0xf5, 0xad, 0xdb, 0xb6, 0xd6, 0x18, 0x13, 0x16,
0xf9, 0xcf, 0xf7, 0x02, 0x21, 0x35, 0x4b, 0xd6, 0xe6, 0xc7, 0x50, 0xcb, 0x48, 0xa8, 0x01, 0xe5,
0x21, 0x39, 0x53, 0x17, 0xa8, 0x39, 0x72, 0x89, 0x6e, 0xc2, 0xf2, 0x44, 0xd5, 0x72, 0x6c, 0x35,
0xde, 0xdc, 0x2f, 0x6d, 0x1b, 0xd6, 0xef, 0x65, 0x78, 0x43, 0xfa, 0x79, 0xac, 0x82, 0xd9, 0x65,
0x6c, 0x97, 0x08, 0x4c, 0x7d, 0xfe, 0x64, 0x4c, 0xa2, 0xb3, 0x97, 0x19, 0x8b, 0x3e, 0x54, 0xe3,
0x44, 0x28, 0x9f, 0x5e, 0x74, 0x5f, 0x48, 0x74, 0xe7, 0xcd, 0xa0, 0xfc, 0x12, 0x9a, 0xc1, 0xbc,
0xfa, 0xac, 0xbc, 0x82, 0xfa, 0xb4, 0x7e, 0x28, 0xc1, 0x2d, 0xe9, 0x4e, 0x9e, 0xae, 0xac, 0xc2,
0x10, 0x54, 0x84, 0xc4, 0x7a, 0x9c, 0x7c, 0xb5, 0x46, 0xf7, 0x60, 0x65, 0xc8, 0xc3, 0x20, 0x20,
0x22, 0x89, 0x75, 0x53, 0x87, 0x54, 0x2f, 0x3e, 0xea, 0x32, 0x76, 0xcc, 0x88, 0xeb, 0xa4, 0xac,
0xe8, 0x0e, 0x54, 0x06, 0xc4, 0x1f, 0xa9, 0x6a, 0xab, 0x6f, 0xfd, 0x5f, 0x17, 0x79, 0x44, 0xfc,
0x51, 0xca, 0xaf, 0x98, 0xd0, 0x7d, 0xa8, 0x65, 0x5e, 0x26, 0x31, 0x58, 0x9f, 0x32, 0x92, 0x1e,
0xa6, 0x62, 0x39, 0xbb, 0x94, 0xed, 0xd3, 0x88, 0xb8, 0x92, 0x51, 0x3d, 0x36, 0x05, 0xd9, 0xdd,
0xf4, 0x30, 0x93, 0xcd, 0xd8, 0xad, 0x5f, 0x0d, 0x78, 0x2b, 0x87, 0xaf, 0x93, 0x14, 0xd3, 0x21,
0x11, 0xb8, 0x8f, 0x05, 0x7e, 0xcd, 0x25, 0xfd, 0x67, 0x09, 0xae, 0x4d, 0x47, 0x57, 0xa6, 0x47,
0x76, 0xb4, 0x34, 0x3d, 0x72, 0x8d, 0x8e, 0xe0, 0x0a, 0x09, 0x26, 0x34, 0x0a, 0x03, 0xf9, 0x08,
0xa4, 0x50, 0xfd, 0xe0, 0xe2, 0x1c, 0xd9, 0x7b, 0x1a, 0x7b, 0xdc, 0x05, 0xa6, 0x34, 0xa0, 0x21,
0x00, 0xc3, 0x11, 0x1e, 0x11, 0x41, 0x22, 0x09, 0xc9, 0xf2, 0xa2, 0x90, 0x8c, 0xcd, 0x1f, 0xa5,
0x3a, 0x1d, 0x4d, 0x7d, 0xf3, 0x19, 0xac, 0xcd, 0xf8, 0x33, 0xa7, 0x05, 0xdd, 0xd3, 0x5b, 0x50,
0x7d, 0xab, 0x35, 0xe7, 0x7a, 0x9a, 0x1a, 0xbd, 0x45, 0xfd, 0x51, 0x82, 0xba, 0x86, 0xb8, 0xb9,
0x31, 0x6c, 0x01, 0x28, 0x81, 0x87, 0xd4, 0x27, 0x71, 0x04, 0x6b, 0x8e, 0x46, 0x41, 0x83, 0x39,
0x11, 0x79, 0xb4, 0x40, 0x44, 0xa4, 0x3f, 0x73, 0xc3, 0x21, 0x9f, 0x29, 0x65, 0x97, 0x27, 0x73,
0x53, 0xb2, 0x43, 0x02, 0xae, 0x9d, 0x52, 0x9f, 0x1c, 0xe5, 0x5e, 0x54, 0x95, 0x17, 0x07, 0x0b,
0x7a, 0xf1, 0x50, 0x57, 0xea, 0x14, 0x6c, 0x58, 0xef, 0x43, 0xa3, 0x58, 0x7a, 0xd2, 0x43, 0x3a,
0xc2, 0x5e, 0x16, 0xa7, 0x64, 0x67, 0xfd, 0x62, 0x00, 0x9a, 0xcd, 0xc4, 0x45, 0xe1, 0x1e, 0x6e,
0xf3, 0x74, 0x3e, 0x88, 0x71, 0xaf, 0x51, 0x50, 0x0f, 0xea, 0x7d, 0xc2, 0x05, 0x0d, 0x94, 0xc3,
0x49, 0x43, 0x78, 0xef, 0xf2, 0x94, 0xef, 0xe6, 0x02, 0x8e, 0x2e, 0x6d, 0x7d, 0x01, 0xb7, 0x2f,
0xe5, 0xd6, 0x26, 0x03, 0x63, 0x6a, 0x32, 0xb8, 0x74, 0x9e, 0xb0, 0x10, 0x34, 0x8a, 0x9d, 0xc5,
0x0a, 0x60, 0x4d, 0xc6, 0x74, 0x67, 0x80, 0x23, 0xf1, 0x0a, 0x06, 0x02, 0xeb, 0x13, 0xa8, 0x65,
0xf6, 0xe6, 0x06, 0xba, 0x09, 0xab, 0x93, 0x74, 0xc8, 0x2a, 0xa9, 0x6c, 0x65, 0x7b, 0xab, 0x0b,
0x48, 0x77, 0x36, 0x79, 0x00, 0xee, 0xc0, 0x32, 0x15, 0x64, 0x94, 0x4e, 0x0f, 0xff, 0x2b, 0xf6,
0x6d, 0xc5, 0xee, 0xc4, 0x3c, 0x5b, 0xff, 0x96, 0x61, 0x2d, 0x6f, 0x9f, 0xf2, 0x97, 0xba, 0x04,
0x3d, 0x86, 0xc6, 0x7e, 0xf2, 0x6d, 0x93, 0x4e, 0x70, 0xe8, 0x4d, 0x5d, 0x4f, 0xe1, 0x2b, 0xa7,
0xb9, 0x3e, 0xff, 0x30, 0xf6, 0xc8, 0x5a, 0x42, 0x0f, 0x60, 0x35, 0x9d, 0xb2, 0xa6, 0x15, 0x15,
0x66, 0xaf, 0xe6, 0x8d, 0x39, 0xb3, 0x8e, 0xb5, 0x84, 0xbe, 0x81, 0xab, 0xfb, 0xaa, 0xfb, 0x25,
0xaf, 0x1d, 0x7a, 0x47, 0xe7, 0xbb, 0x70, 0x7c, 0x69, 0x5a, 0x45, 0xb6, 0xd9, 0x07, 0xd3, 0x5a,
0x42, 0x3f, 0x1b, 0x70, 0x63, 0x9f, 0x88, 0xe2, 0xe3, 0x81, 0xee, 0xce, 0x37, 0x72, 0xc1, 0x23,
0xd3, 0xec, 0x2d, 0x04, 0x8c, 0x69, 0x9d, 0xd6, 0x12, 0x3a, 0x52, 0x77, 0xce, 0x13, 0x8c, 0x6e,
0xcf, 0xcd, 0x64, 0x16, 0xba, 0xd6, 0x45, 0xc7, 0xe9, 0x3d, 0x3f, 0x7d, 0xf0, 0xd7, 0x79, 0xcb,
0xf8, 0xfb, 0xbc, 0x65, 0xfc, 0x73, 0xde, 0x32, 0xbe, 0xfa, 0xf0, 0xb2, 0x0f, 0x5f, 0xed, 0x03,
0x1d, 0x33, 0xea, 0xfa, 0x94, 0x04, 0xe2, 0xa4, 0xaa, 0x3e, 0x73, 0x3f, 0xfa, 0x2f, 0x00, 0x00,
0xff, 0xff, 0x61, 0xf3, 0xc7, 0x8e, 0xbf, 0x0f, 0x00, 0x00,
// 1230 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x57, 0xdd, 0x6e, 0x1b, 0xc5,
0x17, 0xcf, 0xda, 0x8e, 0x13, 0x1f, 0xb7, 0x8d, 0x33, 0xed, 0xbf, 0xff, 0xc5, 0xa4, 0x96, 0x59,
0x01, 0x0a, 0x94, 0xae, 0x69, 0xa8, 0x44, 0x55, 0xa4, 0x4a, 0xa6, 0x49, 0x53, 0xe4, 0x44, 0x4d,
0x37, 0x50, 0x89, 0x0f, 0xa9, 0x9a, 0xac, 0x27, 0xeb, 0xc1, 0xeb, 0xdd, 0x61, 0x67, 0x6c, 0xe4,
0xbe, 0x00, 0xdc, 0x23, 0x6e, 0x78, 0x0c, 0x24, 0x2e, 0xb8, 0x47, 0x88, 0x4b, 0x1e, 0x01, 0xe5,
0x92, 0xa7, 0x40, 0x33, 0xfb, 0x35, 0x5e, 0x3b, 0xb9, 0x71, 0x3f, 0x6e, 0xec, 0x99, 0x33, 0x67,
0xce, 0x39, 0xf3, 0x9b, 0xdf, 0x39, 0x73, 0x16, 0xde, 0x8d, 0x08, 0x0b, 0x39, 0x89, 0x26, 0x24,
0xea, 0xa8, 0x21, 0x15, 0x61, 0x34, 0xd5, 0x86, 0x36, 0x8b, 0x42, 0x11, 0x22, 0xc8, 0x25, 0xcd,
0x6b, 0x5e, 0xe8, 0x85, 0x4a, 0xdc, 0x91, 0xa3, 0x58, 0xa3, 0xb9, 0xe5, 0x85, 0xa1, 0xe7, 0x93,
0x0e, 0x66, 0xb4, 0x83, 0x83, 0x20, 0x14, 0x58, 0xd0, 0x30, 0xe0, 0xc9, 0xaa, 0x35, 0xbc, 0xcb,
0x6d, 0x1a, 0xaa, 0x55, 0x37, 0x8c, 0x48, 0x67, 0x72, 0xbb, 0xe3, 0x91, 0x80, 0x44, 0x58, 0x90,
0x7e, 0xa2, 0xf3, 0x99, 0x47, 0xc5, 0x60, 0x7c, 0x62, 0xbb, 0xe1, 0xa8, 0x83, 0x23, 0xe5, 0xe2,
0x5b, 0x35, 0xb8, 0xe5, 0xf6, 0x3b, 0x6c, 0xe8, 0xc9, 0xcd, 0xbc, 0x83, 0x19, 0xf3, 0xa9, 0xab,
0x8c, 0x77, 0x26, 0xb7, 0xb1, 0xcf, 0x06, 0x78, 0xce, 0x94, 0xf5, 0x5b, 0x15, 0x36, 0x0e, 0x71,
0x40, 0x4f, 0x09, 0x17, 0x0e, 0xf9, 0x6e, 0x4c, 0xb8, 0x40, 0x5f, 0x42, 0x45, 0x1e, 0xc2, 0x34,
0xda, 0xc6, 0x76, 0x7d, 0x67, 0xcf, 0xce, 0xbd, 0xd9, 0xa9, 0x37, 0x35, 0x78, 0xe6, 0xf6, 0x6d,
0x36, 0xf4, 0x6c, 0xe9, 0xcd, 0xd6, 0xbc, 0xd9, 0xa9, 0x37, 0xdb, 0xc9, 0xb0, 0x70, 0x94, 0x49,
0xd4, 0x84, 0xf5, 0x88, 0x4c, 0x28, 0xa7, 0x61, 0x60, 0x96, 0xda, 0xc6, 0x76, 0xcd, 0xc9, 0xe6,
0xc8, 0x84, 0xb5, 0x20, 0x7c, 0x80, 0xdd, 0x01, 0x31, 0xcb, 0x6d, 0x63, 0x7b, 0xdd, 0x49, 0xa7,
0xa8, 0x0d, 0x75, 0xcc, 0xd8, 0x01, 0x3e, 0x21, 0x7e, 0x8f, 0x4c, 0xcd, 0x8a, 0xda, 0xa8, 0x8b,
0xd0, 0xdb, 0x70, 0x39, 0x9d, 0x3e, 0xc5, 0xfe, 0x98, 0x98, 0xab, 0x4a, 0x67, 0x56, 0x88, 0xb6,
0xa0, 0x16, 0xe0, 0x11, 0xe1, 0x0c, 0xbb, 0xc4, 0x5c, 0x57, 0x1a, 0xb9, 0x00, 0x3d, 0x87, 0x4d,
0xed, 0x10, 0xc7, 0xe1, 0x38, 0x72, 0x89, 0x09, 0x0a, 0x83, 0x83, 0x25, 0x30, 0xe8, 0x16, 0x6d,
0x3a, 0xf3, 0x6e, 0xd0, 0xd7, 0xb0, 0xaa, 0x78, 0x63, 0xd6, 0xdb, 0xe5, 0x17, 0x87, 0x79, 0x6c,
0x13, 0x0d, 0x61, 0x8d, 0xf9, 0x63, 0x8f, 0x06, 0xdc, 0xbc, 0xa4, 0xcc, 0x3f, 0x59, 0xc2, 0xfc,
0x83, 0x30, 0x38, 0xa5, 0xde, 0x21, 0x0e, 0xb0, 0x47, 0x46, 0x24, 0x10, 0x47, 0xca, 0xb2, 0x93,
0x7a, 0x40, 0xdf, 0x43, 0x63, 0x38, 0xe6, 0x22, 0x1c, 0xd1, 0xe7, 0xe4, 0x31, 0x53, 0xcc, 0x36,
0x2f, 0x2b, 0x10, 0x7b, 0x4b, 0x78, 0xed, 0x15, 0x4c, 0x3a, 0x73, 0x4e, 0x24, 0x49, 0x86, 0xe3,
0x13, 0xf2, 0x94, 0x44, 0x8a, 0x5d, 0x57, 0x62, 0x92, 0x68, 0xa2, 0x98, 0x46, 0x34, 0x99, 0x71,
0x73, 0xa3, 0x5d, 0x8e, 0x69, 0x94, 0x89, 0xd0, 0x36, 0x6c, 0x4c, 0x48, 0x44, 0x4f, 0xa7, 0xc7,
0xd4, 0x0b, 0xb0, 0x18, 0x47, 0xc4, 0x6c, 0x28, 0x2a, 0x16, 0xc5, 0xd6, 0x9f, 0x06, 0x34, 0xf2,
0xbc, 0xe1, 0x2c, 0x0c, 0xb8, 0xe2, 0xd7, 0x28, 0x91, 0x71, 0xd3, 0x50, 0xe6, 0x73, 0xc1, 0x2c,
0xfb, 0x4a, 0x45, 0xf6, 0x5d, 0x87, 0x6a, 0x5c, 0x5d, 0x14, 0xf9, 0x6b, 0x4e, 0x32, 0x9b, 0xc9,
0x98, 0x4a, 0x21, 0x63, 0x5a, 0x00, 0x5c, 0xf1, 0xe7, 0xf3, 0x29, 0x23, 0x66, 0x55, 0xad, 0x6a,
0x12, 0x64, 0xc1, 0xa5, 0x38, 0x6e, 0x87, 0xf0, 0xb1, 0x2f, 0xcc, 0x35, 0xa5, 0x31, 0x23, 0xb3,
0x7e, 0x34, 0x60, 0xe3, 0x80, 0x72, 0xd1, 0x65, 0x8c, 0xbf, 0xde, 0x02, 0x60, 0x8d, 0x61, 0xad,
0xcb, 0x98, 0x0c, 0x06, 0xdd, 0x86, 0x0a, 0x66, 0x2c, 0x06, 0xb1, 0xbe, 0x73, 0xc3, 0xd6, 0xca,
0x6c, 0xa2, 0x22, 0xff, 0xf9, 0x5e, 0x20, 0xa4, 0x65, 0xa9, 0xda, 0xfc, 0x18, 0x6a, 0x99, 0x08,
0x35, 0xa0, 0x3c, 0x24, 0x53, 0x75, 0x80, 0x9a, 0x23, 0x87, 0xe8, 0x1a, 0xac, 0x4e, 0x54, 0x65,
0x88, 0xbd, 0xc6, 0x93, 0x7b, 0xa5, 0xbb, 0x86, 0xf5, 0x6b, 0x19, 0xde, 0x90, 0x71, 0x1e, 0x2b,
0xc0, 0xbb, 0x8c, 0xed, 0x12, 0x81, 0xa9, 0xcf, 0x9f, 0x8c, 0x49, 0x34, 0x7d, 0x99, 0x58, 0xf4,
0xa1, 0x1a, 0x5f, 0x96, 0x8a, 0xe9, 0x45, 0x57, 0x99, 0xc4, 0x76, 0x5e, 0x5a, 0xca, 0x2f, 0xa1,
0xb4, 0x2c, 0xca, 0xf6, 0xca, 0x2b, 0xc8, 0x76, 0xeb, 0x87, 0x12, 0x5c, 0x97, 0xe1, 0xe4, 0xd7,
0x95, 0x65, 0x21, 0x82, 0x8a, 0x90, 0xf9, 0x10, 0x5f, 0xbe, 0x1a, 0xa3, 0x3b, 0xb0, 0x36, 0xe4,
0x61, 0x10, 0x10, 0x91, 0x60, 0xdd, 0xd4, 0x29, 0xd5, 0x8b, 0x97, 0xba, 0x8c, 0x1d, 0x33, 0xe2,
0x3a, 0xa9, 0x2a, 0xba, 0x09, 0x95, 0x01, 0xf1, 0x47, 0x2a, 0x23, 0xeb, 0x3b, 0xff, 0xd7, 0xb7,
0x3c, 0x22, 0xfe, 0x28, 0xd5, 0x57, 0x4a, 0xe8, 0x1e, 0xd4, 0xb2, 0x28, 0x13, 0x0c, 0xb6, 0x66,
0x9c, 0xa4, 0x8b, 0xe9, 0xb6, 0x5c, 0x5d, 0xee, 0xed, 0xd3, 0x88, 0xb8, 0x52, 0x51, 0x3d, 0x5d,
0x85, 0xbd, 0xbb, 0xe9, 0x62, 0xb6, 0x37, 0x53, 0xb7, 0x7e, 0x31, 0xe0, 0xad, 0x9c, 0xbe, 0x4e,
0x92, 0x4c, 0x87, 0x44, 0xe0, 0x3e, 0x16, 0xf8, 0x35, 0xa7, 0xf4, 0x1f, 0x25, 0xb8, 0x32, 0x8b,
0xae, 0xbc, 0x1e, 0x59, 0xf5, 0xd2, 0xeb, 0x91, 0x63, 0x74, 0x04, 0x97, 0x48, 0x30, 0xa1, 0x51,
0x18, 0xc8, 0x27, 0x25, 0xa5, 0xea, 0x07, 0xe7, 0xdf, 0x91, 0xbd, 0xa7, 0xa9, 0xc7, 0x55, 0x60,
0xc6, 0x02, 0x1a, 0x02, 0x30, 0x1c, 0xe1, 0x11, 0x11, 0x24, 0x92, 0x94, 0x2c, 0x2f, 0x4b, 0xc9,
0xd8, 0xfd, 0x51, 0x6a, 0xd3, 0xd1, 0xcc, 0x37, 0x9f, 0xc1, 0xe6, 0x5c, 0x3c, 0x0b, 0x4a, 0xd0,
0x1d, 0xbd, 0x04, 0xd5, 0x77, 0x5a, 0x0b, 0x8e, 0xa7, 0x99, 0xd1, 0x4b, 0xd4, 0xef, 0x25, 0xa8,
0x6b, 0x8c, 0x5b, 0x88, 0x61, 0x0b, 0x40, 0x6d, 0x78, 0x48, 0x7d, 0x12, 0x23, 0x58, 0x73, 0x34,
0x09, 0x1a, 0x2c, 0x40, 0xe4, 0xd1, 0x12, 0x88, 0xc8, 0x78, 0x16, 0xc2, 0x21, 0x9f, 0x32, 0xe5,
0x97, 0x27, 0x5d, 0x58, 0x32, 0x43, 0x02, 0xae, 0x9c, 0x52, 0x9f, 0x1c, 0xe5, 0x51, 0x54, 0x55,
0x14, 0x07, 0x4b, 0x46, 0xf1, 0x50, 0x37, 0xea, 0x14, 0x7c, 0x58, 0xef, 0x43, 0xa3, 0x98, 0x7a,
0x32, 0x42, 0x3a, 0xc2, 0x5e, 0x86, 0x53, 0x32, 0xb3, 0x7e, 0x36, 0x00, 0xcd, 0xdf, 0xc4, 0x79,
0x70, 0x0f, 0xef, 0xf2, 0xb4, 0xdb, 0x88, 0x79, 0xaf, 0x49, 0x50, 0x0f, 0xea, 0x7d, 0xc2, 0x05,
0x0d, 0x54, 0xc0, 0x49, 0x41, 0x78, 0xef, 0xe2, 0x2b, 0xdf, 0xcd, 0x37, 0x38, 0xfa, 0x6e, 0xeb,
0x0b, 0xb8, 0x71, 0xa1, 0xb6, 0xd6, 0x3d, 0x18, 0x33, 0xdd, 0xc3, 0x85, 0x3d, 0x87, 0x85, 0xa0,
0x51, 0xac, 0x2c, 0x56, 0x00, 0x9b, 0x12, 0xd3, 0x07, 0x03, 0x1c, 0x89, 0x57, 0xd0, 0x10, 0x58,
0x9f, 0x40, 0x2d, 0xf3, 0xb7, 0x10, 0xe8, 0x26, 0xac, 0x4f, 0xd2, 0x96, 0xad, 0xa4, 0x6e, 0x2b,
0x9b, 0x5b, 0x5d, 0x40, 0x7a, 0xb0, 0xc9, 0x03, 0x70, 0x13, 0x56, 0xa9, 0x20, 0xa3, 0xb4, 0x7b,
0xf8, 0x5f, 0xb1, 0x6e, 0x2b, 0x75, 0x27, 0xd6, 0xd9, 0xf9, 0xb7, 0x0c, 0x9b, 0x79, 0xf9, 0x94,
0xbf, 0xd4, 0x25, 0xe8, 0x31, 0x34, 0xf6, 0x93, 0x2f, 0xa5, 0xb4, 0xcb, 0x43, 0x6f, 0xea, 0x76,
0x0a, 0xdf, 0x4c, 0xcd, 0xad, 0xc5, 0x8b, 0x71, 0x44, 0xd6, 0x0a, 0xba, 0x0f, 0xeb, 0x69, 0x97,
0x35, 0x6b, 0xa8, 0xd0, 0x7b, 0x35, 0xaf, 0x2e, 0xe8, 0x75, 0xac, 0x15, 0xf4, 0x0d, 0x5c, 0xde,
0x57, 0xd5, 0x2f, 0x79, 0xed, 0xd0, 0x3b, 0xba, 0xde, 0xb9, 0xed, 0x4b, 0xd3, 0x2a, 0xaa, 0xcd,
0x3f, 0x98, 0xd6, 0x0a, 0xfa, 0xc9, 0x80, 0xab, 0xfb, 0x44, 0x14, 0x1f, 0x0f, 0x74, 0x6b, 0xb1,
0x93, 0x73, 0x1e, 0x99, 0x66, 0x6f, 0x29, 0x62, 0xcc, 0xda, 0xb4, 0x56, 0xd0, 0x91, 0x3a, 0x73,
0x7e, 0xc1, 0xe8, 0xc6, 0xc2, 0x9b, 0xcc, 0xa0, 0x6b, 0x9d, 0xb7, 0x9c, 0x9e, 0xf3, 0xd3, 0xfb,
0x7f, 0x9d, 0xb5, 0x8c, 0xbf, 0xcf, 0x5a, 0xc6, 0x3f, 0x67, 0x2d, 0xe3, 0xab, 0x0f, 0x2f, 0xfa,
0x8c, 0xd6, 0x3e, 0xf7, 0x31, 0xa3, 0xae, 0x4f, 0x49, 0x20, 0x4e, 0xaa, 0xea, 0xa3, 0xf9, 0xa3,
0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x2d, 0x37, 0xe2, 0x11, 0x0d, 0x10, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
@ -1431,6 +1451,18 @@ func (m *ManifestRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.VerifySignature {
i--
if m.VerifySignature {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i--
dAtA[i] = 0x1
i--
dAtA[i] = 0x80
}
if len(m.ApiVersions) > 0 {
for iNdEx := len(m.ApiVersions) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.ApiVersions[iNdEx])
@ -1576,6 +1608,13 @@ func (m *ManifestResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if len(m.VerifyResult) > 0 {
i -= len(m.VerifyResult)
copy(dAtA[i:], m.VerifyResult)
i = encodeVarintRepository(dAtA, i, uint64(len(m.VerifyResult)))
i--
dAtA[i] = 0x3a
}
if len(m.SourceType) > 0 {
i -= len(m.SourceType)
copy(dAtA[i:], m.SourceType)
@ -2415,6 +2454,9 @@ func (m *ManifestRequest) Size() (n int) {
n += 1 + l + sovRepository(uint64(l))
}
}
if m.VerifySignature {
n += 3
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -2449,6 +2491,10 @@ func (m *ManifestResponse) Size() (n int) {
if l > 0 {
n += 1 + l + sovRepository(uint64(l))
}
l = len(m.VerifyResult)
if l > 0 {
n += 1 + l + sovRepository(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -3203,6 +3249,26 @@ 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)
}
var v int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.VerifySignature = bool(v != 0)
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])
@ -3417,6 +3483,38 @@ func (m *ManifestResponse) Unmarshal(dAtA []byte) error {
}
m.SourceType = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 7:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field VerifyResult", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowRepository
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthRepository
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthRepository
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.VerifyResult = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipRepository(dAtA[iNdEx:])

71
reposerver/gpgwatcher.go Normal file
View file

@ -0,0 +1,71 @@
package reposerver
import (
"fmt"
"path"
"time"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/util/gpg"
)
// StartGPGWatcher watches a given directory for creation and deletion of files and syncs the GPG keyring
func StartGPGWatcher(sourcePath string) error {
log.Infof("Starting GPG sync watcher on directory '%s'", sourcePath)
forceSync := false
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Remove == fsnotify.Remove {
// In case our watched path is re-created (i.e. during e2e tests), we need to watch again
if event.Name == sourcePath && event.Op&fsnotify.Remove == fsnotify.Remove {
log.Warnf("Re-creating watcher on %s", sourcePath)
time.Sleep(1 * time.Second)
err = watcher.Add(sourcePath)
if err != nil {
log.Errorf("Error re-creating watcher on %s: %v", sourcePath, err)
return
}
// Force sync because we probably missed an event
forceSync = true
}
if gpg.IsShortKeyID(path.Base(event.Name)) || forceSync {
log.Infof("Updating GPG keyring on filesystem event")
added, removed, err := gpg.SyncKeyRingFromDirectory(sourcePath)
if err != nil {
log.Errorf("Could not sync keyring: %s", err.Error())
} else {
log.Infof("Result of sync operation: keys added: %d, keys removed: %d", len(added), len(removed))
}
forceSync = false
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Errorf("%v", err)
}
}
}()
err = watcher.Add(sourcePath)
if err != nil {
return err
}
<-done
return fmt.Errorf("Abnormal termination of GPG watcher, refusing to continue.")
}

View file

@ -53,3 +53,7 @@ func (w *gitClientWrapper) Init() error {
func (w *gitClientWrapper) RevisionMetadata(revision string) (*git.RevisionMetadata, error) {
return w.client.RevisionMetadata(revision)
}
func (w *gitClientWrapper) VerifyCommitSignature(revision string) (string, error) {
return w.client.VerifyCommitSignature(revision)
}

View file

@ -37,6 +37,7 @@ import (
"github.com/argoproj/argo-cd/util/app/discovery"
argopath "github.com/argoproj/argo-cd/util/app/path"
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/gpg"
"github.com/argoproj/argo-cd/util/helm"
"github.com/argoproj/argo-cd/util/ksonnet"
argokube "github.com/argoproj/argo-cd/util/kube"
@ -118,13 +119,15 @@ func (s *Service) runRepoOperation(
revision string,
repo *v1alpha1.Repository,
source *v1alpha1.ApplicationSource,
verifyCommit bool,
getCached func(revision string) bool,
operation func(appPath, repoRoot, revision string) error,
operation func(appPath, repoRoot, revision, verifyResult string) error,
settings operationSettings) error {
var gitClient git.Client
var helmClient helm.Client
var err error
var signature string
revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
if source.IsHelm() {
helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart)
@ -169,7 +172,7 @@ func (s *Service) runRepoOperation(
return err
}
defer io.Close(closer)
return operation(chartPath, chartPath, revision)
return operation(chartPath, chartPath, revision, "")
} else {
s.repoLock.Lock(gitClient.Root())
defer s.repoLock.Unlock(gitClient.Root())
@ -181,11 +184,17 @@ func (s *Service) runRepoOperation(
if err != nil {
return err
}
if verifyCommit {
signature, err = gitClient.VerifyCommitSignature(revision)
if err != nil {
return err
}
}
appPath, err := argopath.Path(gitClient.Root(), source.Path)
if err != nil {
return err
}
return operation(appPath, gitClient.Root(), revision)
return operation(appPath, gitClient.Root(), revision, signature)
}
}
@ -205,13 +214,14 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
}
return false
}
err := s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, getCached, func(appPath, repoRoot, revision string) error {
err := s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, getCached, func(appPath, repoRoot, revision, verifyResult string) error {
var err error
res, err = GenerateManifests(appPath, repoRoot, revision, q, false)
if err != nil {
return err
}
res.Revision = revision
res.VerifyResult = verifyResult
err = s.cache.SetManifests(revision, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, &res)
if err != nil {
log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), revision, err)
@ -672,7 +682,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD
return false
}
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, getCached, func(appPath, repoRoot, revision string) error {
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, getCached, func(appPath, repoRoot, revision, verifyResult string) error {
appSourceType, err := GetAppSourceType(q.Source, appPath)
if err != nil {
return err
@ -810,9 +820,31 @@ func (s *Service) GetRevisionMetadata(ctx context.Context, q *apiclient.RepoServ
if err != nil {
return nil, err
}
// Run gpg verify-commit on the revision
signatureInfo := ""
if gpg.IsGPGEnabled() {
cs, err := gitClient.VerifyCommitSignature(q.Revision)
if err != nil {
log.Debugf("Could not verify commit signature: %v", err)
return nil, err
}
if cs != "" {
vr, err := gpg.ParseGitCommitVerification(cs)
if err != nil {
log.Debugf("Could not parse commit verification: %v", err)
return nil, err
}
signatureInfo = fmt.Sprintf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
} else {
signatureInfo = "Revision is not signed."
}
}
// discard anything after the first new line and then truncate to 64 chars
message := text.Trunc(strings.SplitN(m.Message, "\n", 2)[0], 64)
metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: metav1.Time{Time: m.Date}, Tags: m.Tags, Message: message}
metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: metav1.Time{Time: m.Date}, Tags: m.Tags, Message: message, SignatureInfo: signatureInfo}
_ = s.cache.SetRevisionMetadata(q.Repo.Repo, q.Revision, metadata)
return metadata, nil
}

View file

@ -23,6 +23,8 @@ message ManifestRequest {
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.KustomizeOptions kustomizeOptions = 13;
string kubeVersion = 14;
repeated string apiVersions = 15;
// Request to verify the signature when generating the manifests (only for Git repositories)
bool verifySignature = 16;
}
message ManifestResponse {
@ -32,6 +34,8 @@ message ManifestResponse {
// resolved revision
string revision = 4;
string sourceType = 6;
// Raw response of git verify-commit operation (always the empty string for Helm)
string verifyResult = 7;
}
// ListAppsRequest requests a repository directory structure

View file

@ -29,7 +29,12 @@ import (
helmmocks "github.com/argoproj/argo-cd/util/helm/mocks"
)
func newServiceWithMocks(root string) (*Service, *gitmocks.Client) {
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]
`
func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) {
service := NewService(metrics.NewMetricsServer(), cache.NewCache(
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
1*time.Minute,
@ -46,6 +51,11 @@ func newServiceWithMocks(root string) (*Service, *gitmocks.Client) {
gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil)
gitClient.On("CommitSHA").Return(mock.Anything, nil)
gitClient.On("Root").Return(root)
if signed {
gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil)
} else {
gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil)
}
chart := "my-chart"
version := semver.MustParse("1.1.0")
@ -65,7 +75,12 @@ func newServiceWithMocks(root string) (*Service, *gitmocks.Client) {
}
func newService(root string) *Service {
service, _ := newServiceWithMocks(root)
service, _ := newServiceWithMocks(root, false)
return service
}
func newServiceWithSignature(root string) *Service {
service, _ := newServiceWithMocks(root, true)
return service
}
@ -76,7 +91,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
// update this value if we add/remove manifests
const countOfManifests = 25
const countOfManifests = 26
res1, err := service.GenerateManifest(context.Background(), &q)
@ -107,7 +122,7 @@ func TestHelmManifestFromChartRepo(t *testing.T) {
}
func TestGenerateManifestsUseExactRevision(t *testing.T) {
service, gitClient := newServiceWithMocks(".")
service, gitClient := newServiceWithMocks(".", false)
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
@ -568,7 +583,7 @@ func TestGetHelmCharts(t *testing.T) {
}
func TestGetRevisionMetadata(t *testing.T) {
service, gitClient := newServiceWithMocks("../..")
service, gitClient := newServiceWithMocks("../..", false)
now := time.Now()
gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
@ -591,6 +606,53 @@ func TestGetRevisionMetadata(t *testing.T) {
}
func TestGetSignatureVerificationResult(t *testing.T) {
// Commit with signature and verification requested
{
service := newServiceWithSignature("../..")
src := argoappv1.ApplicationSource{Path: "manifests/base"}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true}
res, err := service.GenerateManifest(context.Background(), &q)
assert.NoError(t, err)
assert.Equal(t, testSignature, res.VerifyResult)
}
// Commit with signature and verification not requested
{
service := newServiceWithSignature("../..")
src := argoappv1.ApplicationSource{Path: "manifests/base"}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
res, err := service.GenerateManifest(context.Background(), &q)
assert.NoError(t, err)
assert.Empty(t, res.VerifyResult)
}
// Commit without signature and verification requested
{
service := newService("../..")
src := argoappv1.ApplicationSource{Path: "manifests/base"}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true}
res, err := service.GenerateManifest(context.Background(), &q)
assert.NoError(t, err)
assert.Empty(t, res.VerifyResult)
}
// Commit without signature and verification not requested
{
service := newService("../..")
src := argoappv1.ApplicationSource{Path: "manifests/base"}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true}
res, err := service.GenerateManifest(context.Background(), &q)
assert.NoError(t, err)
assert.Empty(t, res.VerifyResult)
}
}
func Test_newEnv(t *testing.T) {
assert.Equal(t, &argoappv1.Env{
&argoappv1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"},

View file

@ -1070,6 +1070,12 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR
if a.Spec.SyncPolicy != nil {
syncOptions = a.Spec.SyncPolicy.SyncOptions
}
// 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.")
}
op := appv1.Operation{
Sync: &appv1.SyncOperation{
Revision: revision,

120
server/gpgkey/gpgkey.go Normal file
View file

@ -0,0 +1,120 @@
package gpgkey
import (
"fmt"
"strings"
"golang.org/x/net/context"
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/server/rbacpolicy"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/gpg"
"github.com/argoproj/argo-cd/util/rbac"
)
// Server provides a service of type GPGKeyService
type Server struct {
db db.ArgoDB
repoClientset apiclient.Clientset
enf *rbac.Enforcer
}
// NewServer returns a new instance of the service with type GPGKeyService
func NewServer(
repoClientset apiclient.Clientset,
db db.ArgoDB,
enf *rbac.Enforcer,
) *Server {
return &Server{
db: db,
repoClientset: repoClientset,
enf: enf,
}
}
// ListGnuPGPublicKeys returns a list of GnuPG public keys in the configuration
func (s *Server) List(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*appsv1.GnuPGPublicKeyList, error) {
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionGet, ""); err != nil {
return nil, err
}
keys, err := s.db.ListConfiguredGPGPublicKeys(ctx)
if err != nil {
return nil, err
}
keyList := &appsv1.GnuPGPublicKeyList{}
for _, v := range keys {
// Remove key's data from list result to save some bytes
v.KeyData = ""
keyList.Items = append(keyList.Items, *v)
}
return keyList, nil
}
// GetGnuPGPublicKey retrieves a single GPG public key from the configuration
func (s *Server) Get(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*appsv1.GnuPGPublicKey, error) {
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionGet, ""); err != nil {
return nil, err
}
keyID := gpg.KeyID(q.KeyID)
if keyID == "" {
return nil, fmt.Errorf("KeyID is malformed or empty")
}
keys, err := s.db.ListConfiguredGPGPublicKeys(ctx)
if err != nil {
return nil, err
}
if key, ok := keys[keyID]; ok {
return key, nil
}
return nil, fmt.Errorf("No such key: %s", keyID)
}
// CreateGnuPGPublicKey adds one or more GPG public keys to the server's configuration
func (s *Server) Create(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyCreateRequest) (*gpgkeypkg.GnuPGPublicKeyCreateResponse, error) {
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionCreate, ""); err != nil {
return nil, err
}
keyData := strings.TrimSpace(q.Publickey.KeyData)
if keyData == "" {
return nil, fmt.Errorf("Submitted key data is empty")
}
added, skipped, err := s.db.AddGPGPublicKey(ctx, q.Publickey.KeyData)
if err != nil {
return nil, err
}
items := make([]appsv1.GnuPGPublicKey, 0)
for _, k := range added {
items = append(items, *k)
}
response := &gpgkeypkg.GnuPGPublicKeyCreateResponse{
Created: &appsv1.GnuPGPublicKeyList{Items: items},
Skipped: skipped,
}
return response, nil
}
// DeleteGnuPGPublicKey removes a single GPG public key from the server's configuration
func (s *Server) Delete(ctx context.Context, q *gpgkeypkg.GnuPGPublicKeyQuery) (*gpgkeypkg.GnuPGPublicKeyResponse, error) {
if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceGPGKeys, rbacpolicy.ActionDelete, ""); err != nil {
return nil, err
}
err := s.db.DeleteGPGPublicKey(ctx, q.KeyID)
if err != nil {
return nil, err
}
return &gpgkeypkg.GnuPGPublicKeyResponse{}, nil
}

View file

@ -0,0 +1,62 @@
syntax = "proto3";
option go_package = "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey";
// GPG public key service
//
// GPG public key API performs CRUD actions against GnuPGPublicKey resources
package gpgkey;
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1/generated.proto";
// Message to query the server for configured GPG public keys
message GnuPGPublicKeyQuery {
// The GPG key ID to query for
string keyID = 1;
}
// Request to create one or more public keys on the server
message GnuPGPublicKeyCreateRequest {
// Raw key data of the GPG key(s) to create
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKey publickey = 1;
// Whether to upsert already existing public keys
bool upsert = 2;
}
// Response to a public key creation request
message GnuPGPublicKeyCreateResponse {
// List of GPG public keys that have been created
github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKeyList created = 1;
// List of key IDs that haven been skipped because they already exist on the server
repeated string skipped = 2;
}
// Generic (empty) response for GPG public key CRUD requests
message GnuPGPublicKeyResponse {}
// GPGKeyService implements API for managing GPG public keys on the server
service GPGKeyService {
// List all available repository certificates
rpc List(GnuPGPublicKeyQuery) returns (github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKeyList) {
option (google.api.http).get = "/api/v1/gpgkeys";
}
// Get information about specified GPG public key from the server
rpc Get(GnuPGPublicKeyQuery) returns (github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.GnuPGPublicKey) {
option (google.api.http).get = "/api/v1/gpgkeys/{keyID}";
}
// Create one or more GPG public keys in the server's configuration
rpc Create(GnuPGPublicKeyCreateRequest) returns (GnuPGPublicKeyCreateResponse) {
option (google.api.http) = {
post: "/api/v1/gpgkeys"
body: "publickey"
};
}
// Delete specified GPG public key from the server's configuration
rpc Delete(GnuPGPublicKeyQuery) returns (GnuPGPublicKeyResponse) {
option (google.api.http).delete = "/api/v1/gpgkeys";
}
}

View file

@ -20,6 +20,7 @@ const (
ResourceRepositories = "repositories"
ResourceCertificates = "certificates"
ResourceAccounts = "accounts"
ResourceGPGKeys = "gpgkeys"
// please add new items to Actions
ActionGet = "get"

View file

@ -57,6 +57,7 @@ import (
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
@ -74,6 +75,7 @@ import (
servercache "github.com/argoproj/argo-cd/server/cache"
"github.com/argoproj/argo-cd/server/certificate"
"github.com/argoproj/argo-cd/server/cluster"
"github.com/argoproj/argo-cd/server/gpgkey"
"github.com/argoproj/argo-cd/server/metrics"
"github.com/argoproj/argo-cd/server/project"
"github.com/argoproj/argo-cd/server/rbacpolicy"
@ -463,6 +465,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
"/cluster.ClusterService/Update": true,
"/session.SessionService/Create": true,
"/account.AccountService/UpdatePassword": true,
"/gpgkey.GPGKeyService/CreateGnuPGPublicKey": true,
"/repository.RepositoryService/Create": true,
"/repository.RepositoryService/Update": true,
"/repository.RepositoryService/CreateRepository": true,
@ -515,6 +518,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
settingsService := settings.NewServer(a.settingsMgr, a, a.DisableAuth)
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf)
certificateService := certificate.NewServer(a.RepoClientset, db, a.enf)
gpgkeyService := gpgkey.NewServer(a.RepoClientset, db, a.enf)
versionpkg.RegisterVersionServiceServer(grpcS, &version.Server{})
clusterpkg.RegisterClusterServiceServer(grpcS, clusterService)
applicationpkg.RegisterApplicationServiceServer(grpcS, applicationService)
@ -525,6 +529,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server {
projectpkg.RegisterProjectServiceServer(grpcS, projectService)
accountpkg.RegisterAccountServiceServer(grpcS, accountService)
certificatepkg.RegisterCertificateServiceServer(grpcS, certificateService)
gpgkeypkg.RegisterGPGKeyServiceServer(grpcS, gpgkeyService)
// Register reflection service on gRPC server.
reflection.Register(grpcS)
grpc_prometheus.Register(grpcS)
@ -627,6 +632,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
mustRegisterGWHandler(projectpkg.RegisterProjectServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(accountpkg.RegisterAccountServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(certificatepkg.RegisterCertificateServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
mustRegisterGWHandler(gpgkeypkg.RegisterGPGKeyServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts)
// Swagger UI
swagger.ServeSwaggerUI(mux, assets.SwaggerJSON, "/swagger-ui", a.RootPath)

View file

@ -6,4 +6,4 @@ repo-server: su --pty -m default -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=t
ui: su --pty -m default -c "test \"$ARGOCD_IN_CI\" = \"true\" && exit 0; cd ui && ARGOCD_E2E_YARN_HOST=0.0.0.0 ${ARGOCD_E2E_YARN_CMD:-yarn} start"
sshd: test "$ARGOCD_E2E_TEST" = "true" && /usr/sbin/sshd -p 2222 -D -e
fcgiwrap: test "$ARGOCD_E2E_TEST" = "true" && (fcgiwrap -s unix:/var/run/fcgiwrap.socket & sleep 1 && chmod 777 /var/run/fcgiwrap.socket && wait)
nginx: test "$ARGOCD_E2E_TEST" = "true" && nginx -g 'daemon off;' -c $(pwd)/test/fixture/testrepos/nginx.conf
nginx: test "$ARGOCD_E2E_TEST" = "true" && nginx -g 'daemon off;' -c $(pwd)/test/fixture/testrepos/nginx.conf

View file

@ -45,6 +45,52 @@ const (
guestbookPathLocal = "./testdata/guestbook_local"
)
func TestSyncToUnsignedCommit(t *testing.T) {
Given(t).
Project("gpg").
Path(guestbookPath).
When().
IgnoreErrors().
Create().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToSignedCommitWithoutKnownKey(t *testing.T) {
Given(t).
Project("gpg").
Path(guestbookPath).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
Create().
Sync().
Then().
Expect(OperationPhaseIs(OperationError)).
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
Expect(HealthIs(health.HealthStatusMissing))
}
func TestSyncToSignedCommitKeyWithKnownKey(t *testing.T) {
Given(t).
Project("gpg").
Path(guestbookPath).
GPGPublicKeyAdded().
Sleep(2).
When().
AddSignedFile("test.yaml", "null").
IgnoreErrors().
Create().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(SyncStatusIs(SyncStatusCodeSynced)).
Expect(HealthIs(health.HealthStatusHealthy))
}
func TestAppCreation(t *testing.T) {
ctx := Given(t)

View file

@ -52,6 +52,12 @@ func (a *Actions) AddFile(fileName, fileContents string) *Actions {
return a
}
func (a *Actions) AddSignedFile(fileName, fileContents string) *Actions {
a.context.t.Helper()
fixture.AddSignedFile(a.context.path+"/"+fileName, fileContents)
return a
}
func (a *Actions) CreateFromFile(handler func(app *Application)) *Actions {
a.context.t.Helper()
app := &Application{

View file

@ -8,6 +8,7 @@ import (
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/test/e2e/fixture"
"github.com/argoproj/argo-cd/test/e2e/fixture/certs"
"github.com/argoproj/argo-cd/test/e2e/fixture/gpgkeys"
"github.com/argoproj/argo-cd/test/e2e/fixture/repos"
"github.com/argoproj/argo-cd/util/settings"
)
@ -42,6 +43,16 @@ func Given(t *testing.T) *Context {
return &Context{t: t, destServer: KubernetesInternalAPIServerAddr, repoURLType: fixture.RepoURLTypeFile, name: fixture.Name(), timeout: 10, project: "default", prune: true}
}
func (c *Context) GPGPublicKeyAdded() *Context {
gpgkeys.AddGPGPublicKey()
return c
}
func (c *Context) GPGPublicKeyRemoved() *Context {
gpgkeys.DeleteGPGPublicKey()
return c
}
func (c *Context) CustomCACertAdded() *Context {
certs.AddCustomCACert()
return c
@ -217,6 +228,11 @@ func (c *Context) When() *Actions {
return &Actions{context: c}
}
func (c *Context) Sleep(seconds time.Duration) *Context {
time.Sleep(seconds * time.Second)
return c
}
func (c *Context) Prune(prune bool) *Context {
c.prune = prune
return c

View file

@ -76,6 +76,7 @@ const (
RepoURLTypeHelm = "helm"
GitUsername = "admin"
GitPassword = "password"
GpgGoodKeyID = "D56C4FCA57A46444"
)
// getKubeConfig creates new kubernetes client config using specified config path and config overrides variables
@ -319,6 +320,16 @@ func EnsureCleanState(t *testing.T) {
ClusterResourceWhitelist: []v1.GroupKind{{Group: "*", Kind: "*"}},
})
// Create seperate project for testing gpg signature verification
FailOnErr(RunCli("proj", "create", "gpg"))
SetProjectSpec("gpg", v1alpha1.AppProjectSpec{
OrphanedResources: nil,
SourceRepos: []string{"*"},
Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}},
ClusterResourceWhitelist: []v1.GroupKind{{Group: "*", Kind: "*"}},
SignatureKeys: []v1alpha1.SignatureKey{{KeyID: GpgGoodKeyID}},
})
// remove tmp dir
CheckError(os.RemoveAll(TmpDir))
@ -335,6 +346,21 @@ func EnsureCleanState(t *testing.T) {
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/tls"))
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/ssh"))
// For signing during the tests
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/gpg"))
FailOnErr(Run("", "chmod", "0700", TmpDir+"/gpg"))
prevGnuPGHome := os.Getenv("GNUPGHOME")
os.Setenv("GNUPGHOME", TmpDir+"/gpg")
// nolint:errcheck
Run("", "pkill", "-9", "gpg-agent")
FailOnErr(Run("", "gpg", "--import", "../fixture/gpg/signingkey.asc"))
os.Setenv("GNUPGHOME", prevGnuPGHome)
// recreate GPG directories
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/gpg/source"))
FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/gpg/keys"))
FailOnErr(Run("", "chmod", "0700", TmpDir+"/app/config/gpg/keys"))
// set-up tmp repo, must have unique name
FailOnErr(Run("", "cp", "-Rf", "testdata", repoDirectory()))
FailOnErr(Run(repoDirectory(), "chmod", "777", "."))
@ -418,6 +444,18 @@ func AddFile(path, contents string) {
FailOnErr(Run(repoDirectory(), "git", "commit", "-am", "add file"))
}
func AddSignedFile(path, contents string) {
log.WithFields(log.Fields{"path": path}).Info("adding")
CheckError(ioutil.WriteFile(filepath.Join(repoDirectory(), path), []byte(contents), 0644))
prevGnuPGHome := os.Getenv("GNUPGHOME")
os.Setenv("GNUPGHOME", TmpDir+"/gpg")
FailOnErr(Run(repoDirectory(), "git", "diff"))
FailOnErr(Run(repoDirectory(), "git", "add", "."))
FailOnErr(Run(repoDirectory(), "git", "-c", fmt.Sprintf("user.signingkey=%s", GpgGoodKeyID), "commit", "-S", "-am", "add file"))
os.Setenv("GNUPGHOME", prevGnuPGHome)
}
// create the resource by creating using "kubectl apply", with bonus templating
func Declarative(filename string, values interface{}) (string, error) {

View file

@ -0,0 +1,32 @@
package gpgkeys
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/argo-cd/test/e2e/fixture"
)
// Add GPG public key via API and create appropriate file where the ConfigMap mount would de it as well
func AddGPGPublicKey() {
keyPath, err := filepath.Abs(fmt.Sprintf("../fixture/gpg/%s", fixture.GpgGoodKeyID))
errors.CheckError(err)
args := []string{"gpg", "add", "--from", keyPath}
errors.FailOnErr(fixture.RunCli(args...))
keyData, err := ioutil.ReadFile(keyPath)
errors.CheckError(err)
err = ioutil.WriteFile(fmt.Sprintf("%s/app/config/gpg/source/%s", fixture.TmpDir, fixture.GpgGoodKeyID), keyData, 0644)
errors.CheckError(err)
}
func DeleteGPGPublicKey() {
args := []string{"gpg", "rm", fixture.GpgGoodKeyID}
errors.FailOnErr(fixture.RunCli(args...))
errors.CheckError(os.Remove(fmt.Sprintf("%s/app/config/gpg/source/%s", fixture.TmpDir, fixture.GpgGoodKeyID)))
}

View file

@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF5vlTkBCADFX1JfbbIYZ+dv5QGRAZfiid8jAkcwJSRn0xwd0M3Dld2vDpKV
UAfzul2sHFSpmwqkiXbXpl9QierO8fBBfmT5dZf63JdSzmiH5e8ysmSaC8iOAGBc
nSX/35++8FxM1FtRXiK5/T6kfCQVRZhMKLnjIMREbvse+OMLouGq3JscywIdKvLO
QDM/Ni1aH33grdQ+UbOFpT6Z1Jm0bGxrrzncwBWMLPePrYyVyletYTbXe9wbPx7e
BnxsqVaeb3I6w4exWVjyGJKFMQWpcLOLZ0/7Osbs6Qk+BhI7IYgeVzG/0BeaKsZX
vCTioKcpjAWEw7JRQWWmbl8dqtJr3tXUnt2NABEBAAG0PkFyZ29DRCBFMkUgVGVz
dCBTaWduaW5nIEtleSAoRG8gbm90IHVzZSkgPG5vcmVwbHlAZXhhbXBsZS5jb20+
iQFOBBMBCAA4FiEE6kWbSVlcvj/R+6MD1WxPylekZEQFAl5vlTkCGwMFCwkIBwIG
FQoJCAsCBBYCAwECHgECF4AACgkQ1WxPylekZESZkAgAm8UZCNO1pKYRPT97agML
vX9G8RJ9oBUG2rUZWcMJ09prpaHf1CpkipxBUDAG7ljjpPgl1Y4gmopEEUESvEgf
g6PlNJfM83WE7gw548qOUTxlbCuLnKyLcUyr+YMo5DNgJG++VDG47MPP3fubO/ZL
zSyACeek+bMYrYEy1e3O/bQ61AJIGJGEjxXhmNSLd8koLPFMrdupckshJ1T8tUGs
P14qHUo4UCjytPCca8BN1KOiRcrykENte0S++Y3KT6Z+qMrrh0gb6JYU76oD8AU2
tz9S/lb9Yeg/mpefnPB7bEVKslvZdSPrBWGu6vD16MQ3KQRgvmkVp+L22PGZ1N46
WrkBDQReb5U5AQgAukdIaK1cDe1Q2QXS3e2NLiCNtQgEvJGru2o7Nz58NkWv2vMW
a7Q+JLdFltvciFHaq3HRaw3Xr4ejhKYFYuaHbKBtVu/CbQSP/e6nm2zyfqU6wHgS
nGnFg9toQAcOEE0Sdz5J4plSL4osMJ6LVo3DHqf3wkYX2ajK+cAvKRTj/O9oF1PV
nBUEYOu5GOP0dczpNz6TP5QBMjwU6ORcxxkqX/cY8io/sdaC77PR1Xrmul92NJ4B
kM7fFBd3QCSjhxADYnWGgWogQ6B865V8lpNX4GI6tXVRLjF/XjuapoSiPmax+Al4
Wt2W5m/K4+/Fk0nO0SZawcpUrGI82yTp3CmGXwARAQABiQE2BBgBCAAgFiEE6kWb
SVlcvj/R+6MD1WxPylekZEQFAl5vlTkCGwwACgkQ1WxPylekZETt9gf+JNcc003S
BEyfr/WYz65ktu5kLoGPtjUF1dBF/6MCFS5SCp/rEaK0y8R09I8wYb90sRVB80lM
vES/Ec0KD0beE1vjVAbURIrGC9fXK7lFo+KoBHmdDSKdkP8t289CrZ9g2n4orr2M
aLVobOh8Q8eXR6xyguDR8OcgSbUaHuY8ZsEyeS9IH+p97GojD/mwnZletu4wleDT
2DdUIsiV6L1d40mxRGdCpNAzPzibFn1Nh6dr5yzH0ihaTQ4He4MqcZZgYJXODZFV
5shN+mHofvXnJ5Wt4sKzRH+A6vg/jMUxXoSU3Gu0a/RGUn8Cz6cX9boe6M1Fi+xd
LZtzetfNy78xfg==
=TpiH
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

View file

@ -3,6 +3,7 @@ package test
import (
"context"
"fmt"
"io/ioutil"
"log"
"net"
"time"
@ -59,3 +60,12 @@ func portIsOpen(addr string) bool {
_ = conn.Close()
return true
}
// Read the contents of a file and returns it as string. Panics on error.
func MustLoadFileToString(path string) string {
o, err := ioutil.ReadFile(path)
if err != nil {
panic(err.Error())
}
return string(o)
}

View file

@ -43,6 +43,10 @@ export const RevisionMetadataRows = (props: {applicationName: string; source: Ap
<div className='columns small-9'>{m.message}</div>
</div>
)}
<div className='row'>
<div className='columns small-3'>GPG signature</div>
<div className='columns small-9'>{m.signatureInfo || '-'}</div>
</div>
</div>
)}
</DataLoader>

View file

@ -23,6 +23,8 @@ export const RevisionMetadataPanel = (props: {appName: string; type: string; rev
<br />
</span>
)}
{m.signatureInfo}
<br />
{m.message}
</span>
}
@ -31,7 +33,7 @@ export const RevisionMetadataPanel = (props: {appName: string; type: string; rev
<div className='application-status-panel__item-name'>
{m.author && (
<React.Fragment>
Authored by {m.author}
Authored by {m.author} - {m.signatureInfo}
<br />
</React.Fragment>
)}

View file

@ -0,0 +1,44 @@
@import 'node_modules/argo-ui/src/styles/config';
.gpgkeys-list {
&__top-panel {
padding: 1em;
& > .columns:first-child {
font-size: 8em;
text-align: center;
}
& > .columns:last-child {
text-align: center;
border-left: 2px solid $argo-color-gray-4;
& > p {
margin-bottom: 0;
margin-top: 24px;
&:first-of-type {
font-size: 1.25em;
}
}
& > button {
width: 15em;
}
}
}
.argo-table-list {
.argo-dropdown {
float: right;
}
}
textarea.argo-field {
height: 25em;
width: 1024em;
white-space: pre;
overflow-wrap: normal;
overflow-x: scroll;
}
}

View file

@ -0,0 +1,196 @@
import {DropDownMenu, FormField, NotificationType, SlidingPanel} from 'argo-ui';
import * as PropTypes from 'prop-types';
import * as React from 'react';
import {Form, FormApi, TextArea} from 'react-form';
import {RouteComponentProps} from 'react-router';
import {DataLoader, EmptyState, ErrorNotification, Page} from '../../../shared/components';
import {AppContext} from '../../../shared/context';
import * as models from '../../../shared/models';
import {services} from '../../../shared/services';
require('./gpgkeys-list.scss');
interface NewGnuPGPublicKeyParams {
keyData: string;
}
export class GpgKeysList extends React.Component<RouteComponentProps<any>> {
public static contextTypes = {
router: PropTypes.object,
apis: PropTypes.object,
history: PropTypes.object
};
private formApi: FormApi;
private loader: DataLoader;
public render() {
return (
<Page
title='GnuPG public keys'
toolbar={{
breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'GnuPG public keys'}],
actionMenu: {
className: 'fa fa-plus',
items: [
{
title: 'Add GnuPG key',
action: () => (this.showAddGnuPGKey = true)
}
]
}
}}>
<div className='gpgkeys-list'>
<div className='argo-container'>
<DataLoader load={() => services.gpgkeys.list()} ref={loader => (this.loader = loader)}>
{(gpgkeys: models.GnuPGPublicKey[]) =>
(gpgkeys.length > 0 && (
<div className='argo-table-list'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-3'>KEY ID</div>
<div className='columns small-3'>KEY TYPE</div>
<div className='columns small-6'>IDENTITY</div>
</div>
</div>
{gpgkeys.map(gpgkey => (
<div className='argo-table-list__row' key={gpgkey.keyID}>
<div className='row'>
<div className='columns small-3'>
<i className='fa fa-key' /> {gpgkey.keyID}
</div>
<div className='columns small-3'>{gpgkey.subType.toUpperCase()}</div>
<div className='columns small-6'>
{gpgkey.owner}
<DropDownMenu
anchor={() => (
<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
<i className='fa fa-ellipsis-v' />
</button>
)}
items={[
{
title: 'Remove',
action: () => this.removeKey(gpgkey.keyID)
}
]}
/>
</div>
</div>
</div>
))}
</div>
)) || (
<EmptyState icon='fa fa-key'>
<h4>No GnuPG public keys currently configured</h4>
<h5>You can add GnuPG public keys below..</h5>
<button className='argo-button argo-button--base' onClick={() => (this.showAddGnuPGKey = true)}>
Add GnuPG public key
</button>
</EmptyState>
)
}
</DataLoader>
</div>
</div>
<SlidingPanel
isShown={this.showAddGnuPGKey}
onClose={() => (this.showAddGnuPGKey = false)}
header={
<div>
<button className='argo-button argo-button--base' onClick={() => this.formApi.submitForm(null)}>
Create
</button>{' '}
<button onClick={() => (this.showAddGnuPGKey = false)} className='argo-button argo-button--base-o'>
Cancel
</button>
</div>
}>
<h4>Add GnuPG public key</h4>
<Form
onSubmit={params => this.addGnuPGPublicKey({keyData: params.keyData})}
getApi={api => (this.formApi = api)}
preSubmit={(params: NewGnuPGPublicKeyParams) => ({
keyData: params.keyData
})}
validateError={(params: NewGnuPGPublicKeyParams) => ({
keyData: !params.keyData && 'Key data is required'
})}>
{formApi => (
<form onSubmit={formApi.submitForm} role='form' className='gpgkeys-list width-control' encType='multipart/form-data'>
<div className='argo-form-row'>
<FormField formApi={formApi} label='GnuPG public key data (ASCII-armored)' field='keyData' component={TextArea} />
</div>
</form>
)}
</Form>
</SlidingPanel>
</Page>
);
}
private clearForms() {
this.formApi.resetAll();
}
private validateKeyInputfield(data: string): boolean {
if (data == null || data === '') {
return false;
}
const str = data.trim();
const startNeedle = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n';
const endNeedle = '\n-----END PGP PUBLIC KEY BLOCK-----';
if (str.length < startNeedle.length + endNeedle.length) {
return false;
}
if (!str.startsWith(startNeedle)) {
return false;
}
if (!str.endsWith(endNeedle)) {
return false;
}
return true;
}
private async addGnuPGPublicKey(params: NewGnuPGPublicKeyParams) {
try {
if (!this.validateKeyInputfield(params.keyData)) {
throw {
name: 'Invalid key exception',
message: 'Invalid GnuPG key data found - must be ASCII armored'
};
}
await services.gpgkeys.create({keyData: params.keyData});
this.showAddGnuPGKey = false;
this.loader.reload();
} catch (e) {
this.appContext.apis.notifications.show({
content: <ErrorNotification title='Unable to add GnuPG public key' e={e} />,
type: NotificationType.Error
});
}
}
private async removeKey(keyId: string) {
const confirmed = await this.appContext.apis.popup.confirm('Remove GPG public key', 'Are you sure you want to remove GPG key with ID ' + keyId + '?');
if (confirmed) {
await services.gpgkeys.delete(keyId);
this.loader.reload();
}
}
private get showAddGnuPGKey() {
return new URLSearchParams(this.props.location.search).get('addGnuPGPublicKey') === 'true';
}
private set showAddGnuPGKey(val: boolean) {
this.clearForms();
this.appContext.router.history.push(`${this.props.match.url}?addGnuPGPublicKey=${val}`);
}
private get appContext(): AppContext {
return this.context as AppContext;
}
}

View file

@ -136,6 +136,7 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{name: s
namespaceResourceWhitelist: proj.spec.namespaceResourceWhitelist || [],
roles: proj.spec.roles || [],
syncWindows: proj.spec.syncWindows || [],
signatureKeys: proj.spec.signatureKeys || [],
orphanedResourcesEnabled: !!proj.spec.orphanedResources,
orphanedResourcesWarn:
proj.spec.orphanedResources && (proj.spec.orphanedResources.warn === undefined || proj.spec.orphanedResources.warn)
@ -598,6 +599,28 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{name: s
</div>
)}
<h4>Required signature keys {helpTip('IDs of GnuPG keys that commits must be signed with in order to be allowed to sync to')}</h4>
{((proj.spec.signatureKeys || []).length > 0 && (
<div className='argo-table-list'>
<div className='argo-table-list__head'>
<div className='row'>
<div className='columns small-9'>KEY ID</div>
</div>
</div>
{(proj.spec.signatureKeys || []).map(res => (
<div className='argo-table-list__row' key={`${res.keyID}`}>
<div className='row'>
<div className='columns small-9'>{res.keyID}</div>
</div>
</div>
))}
</div>
)) || (
<div className='white-box'>
<p>Commit signatures are not required</p>
</div>
)}
<h4>Orphaned Resource Monitoring {helpTip('Enables monitoring of top level resources in the application target namespace')}</h4>
<div className='white-box'>

View file

@ -26,6 +26,7 @@ export const ProjectEditPanel = (props: {nameReadonly?: boolean; defaultParams?:
clusterResourceWhitelist: [],
namespaceResourceBlacklist: [],
namespaceResourceWhitelist: [],
signatureKeys: [],
...props.defaultParams
}}
validateError={(params: ProjectParams) => ({
@ -200,6 +201,31 @@ export const ProjectEditPanel = (props: {nameReadonly?: boolean; defaultParams?:
</a>
</React.Fragment>
<DataLoader load={() => services.gpgkeys.list().then(gpgkeys => gpgkeys.map(gpgkey => gpgkey.keyID))}>
{gpgkeys => (
<React.Fragment>
<h4>Required signature keys</h4>
<div>GnuPG key IDs which commits to be synced to must be signed with</div>
{(api.values.signatureKeys as Array<string>).map((_, i) => (
<div key={i} className='row project-edit-panel__form-row'>
<div className='columns small-12'>
<FormField
formApi={api}
field={`signatureKeys[${i}].keyID`}
component={AutocompleteField}
componentProps={{
items: gpgkeys
}}
/>
<i className='fa fa-times' onClick={() => api.setValue('signatureKeys', removeEl(api.values.signatureKeys, i))} />
</div>
</div>
))}
<a onClick={() => api.setValue('signatureKeys', api.values.signatureKeys.concat(gpgkeys[0]))}>add GnuPG key ID</a>
</React.Fragment>
)}
</DataLoader>
<React.Fragment>
<h4>Orphaned Resource Monitoring</h4>
<div>Enables monitoring of top level resources in the application target namespace</div>

View file

@ -5,6 +5,7 @@ import {AccountDetails} from './account-details/account-details';
import {AccountsList} from './accounts-list/accounts-list';
import {CertsList} from './certs-list/certs-list';
import {ClustersList} from './clusters-list/clusters-list';
import {GpgKeysList} from './gpgkeys-list/gpgkeys-list';
import {ProjectDetails} from './project-details/project-details';
import {ProjectsList} from './projects-list/projects-list';
import {ReposList} from './repos-list/repos-list';
@ -15,6 +16,7 @@ export const SettingsContainer = (props: RouteComponentProps<any>) => (
<Route exact={true} path={`${props.match.path}`} component={SettingsOverview} />
<Route exact={true} path={`${props.match.path}/repos`} component={ReposList} />
<Route exact={true} path={`${props.match.path}/certs`} component={CertsList} />
<Route exact={true} path={`${props.match.path}/gpgkeys`} component={GpgKeysList} />
<Route exact={true} path={`${props.match.path}/clusters`} component={ClustersList} />
<Route exact={true} path={`${props.match.path}/projects`} component={ProjectsList} />
<Route exact={true} path={`${props.match.path}/projects/:name`} component={ProjectDetails} />

View file

@ -17,6 +17,11 @@ const settings = [
description: 'Configure certificates for connecting Git repositories',
path: './certs'
},
{
title: 'GnuPG keys',
description: 'Configure GnuPG public keys for commit verification',
path: './gpgkeys'
},
{
title: 'Clusters',
description: 'Configure connected Kubernetes clusters',

View file

@ -80,6 +80,7 @@ export interface RevisionMetadata {
date: models.Time;
tags?: string[];
message?: string;
signatureInfo?: string;
}
export interface SyncOperationResult {
@ -587,6 +588,10 @@ export interface GroupKind {
kind: string;
}
export interface ProjectSignatureKey {
keyID: string;
}
export interface ProjectSpec {
sourceRepos: string[];
destinations: ApplicationDestination[];
@ -595,6 +600,7 @@ export interface ProjectSpec {
clusterResourceWhitelist: GroupKind[];
namespaceResourceBlacklist: GroupKind[];
namespaceResourceWhitelist: GroupKind[];
signatureKeys: ProjectSignatureKey[];
orphanedResources?: {warn?: boolean};
syncWindows?: SyncWindows;
}
@ -668,3 +674,13 @@ export interface Account {
capabilities: string[];
tokens: Token[];
}
export interface GnuPGPublicKey {
keyID?: string;
fingerprint?: string;
subType?: string;
owner?: string;
keyData?: string;
}
export interface GnuPGPublicKeyList extends ItemsList<GnuPGPublicKey> {}

View file

@ -0,0 +1,26 @@
import * as models from '../models';
import requests from './requests';
export class GnuPGPublicKeyService {
public list(): Promise<models.GnuPGPublicKey[]> {
return requests
.get('/gpgkeys')
.then(res => res.body as models.GnuPGPublicKeyList)
.then(list => list.items || []);
}
public create(publickey: models.GnuPGPublicKey): Promise<models.GnuPGPublicKeyList> {
return requests
.post('/gpgkeys')
.send(publickey)
.then(res => res.body as models.GnuPGPublicKeyList);
}
public delete(keyID: string): Promise<models.GnuPGPublicKey> {
return requests
.delete('/gpgkeys')
.query({keyID})
.send()
.then(res => res.body as models.GnuPGPublicKey);
}
}

View file

@ -3,6 +3,7 @@ import {ApplicationsService} from './applications-service';
import {AuthService} from './auth-service';
import {CertificatesService} from './cert-service';
import {ClustersService} from './clusters-service';
import {GnuPGPublicKeyService} from './gpgkey-service';
import {ProjectsService} from './projects-service';
import {RepositoriesService} from './repo-service';
import {RepoCredsService} from './repocreds-service';
@ -22,6 +23,7 @@ export interface Services {
viewPreferences: ViewPreferencesService;
version: VersionService;
accounts: AccountsService;
gpgkeys: GnuPGPublicKeyService;
}
export const services: Services = {
@ -35,7 +37,8 @@ export const services: Services = {
projects: new ProjectsService(),
viewPreferences: new ViewPreferencesService(),
version: new VersionService(),
accounts: new AccountsService()
accounts: new AccountsService(),
gpgkeys: new GnuPGPublicKeyService()
};
export {ProjectParams, ProjectRoleParams, CreateJWTTokenParams, DeleteJWTTokenParams, JWTTokenResponse} from './projects-service';

View file

@ -11,6 +11,7 @@ export interface ProjectParams {
clusterResourceWhitelist: models.GroupKind[];
namespaceResourceBlacklist: models.GroupKind[];
namespaceResourceWhitelist: models.GroupKind[];
signatureKeys: models.ProjectSignatureKey[];
orphanedResourcesEnabled: boolean;
orphanedResourcesWarn: boolean;
syncWindows: models.SyncWindow[];
@ -80,6 +81,7 @@ function paramsToProj(params: ProjectParams) {
clusterResourceWhitelist: params.clusterResourceWhitelist,
namespaceResourceBlacklist: params.namespaceResourceBlacklist,
namespaceResourceWhitelist: params.namespaceResourceWhitelist,
signatureKeys: params.signatureKeys,
orphanedResources: (params.orphanedResourcesEnabled && {warn: !!params.orphanedResourcesWarn}) || null
}
};

View file

@ -59,6 +59,13 @@ type ArgoDB interface {
// ListHelmRepositories lists repositories
ListHelmRepositories(ctx context.Context) ([]*appv1.Repository, error)
// ListConfiguredGPGPublicKeys returns all GPG public key IDs that are configured
ListConfiguredGPGPublicKeys(ctx context.Context) (map[string]*appv1.GnuPGPublicKey, error)
// AddGPGPublicKey adds one ore more GPG public keys to the configuration
AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*appv1.GnuPGPublicKey, []string, error)
// DeleteGPGPublicKey removes a GPG public key from the configuration
DeleteGPGPublicKey(ctx context.Context, keyID string) error
}
type db struct {

142
util/db/gpgkeys.go Normal file
View file

@ -0,0 +1,142 @@
package db
import (
"context"
"fmt"
"io/ioutil"
"os"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/common"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/gpg"
)
// Validates a single GnuPG key and returns the key's ID
func validatePGPKey(keyData string) (*appsv1.GnuPGPublicKey, error) {
f, err := ioutil.TempFile("", "gpg-public-key")
if err != nil {
return nil, err
}
defer os.Remove(f.Name())
err = ioutil.WriteFile(f.Name(), []byte(keyData), 0600)
if err != nil {
return nil, err
}
f.Close()
parsed, err := gpg.ValidatePGPKeys(f.Name())
if err != nil {
return nil, err
}
// Each key/value pair in the config map must exactly contain one public key, with the (short) GPG key ID as key
if len(parsed) != 1 {
return nil, fmt.Errorf("More than one key found in input data")
}
var retKey *appsv1.GnuPGPublicKey = nil
// Is there a better way to get the first element from a map without knowing its key?
for _, k := range parsed {
retKey = k
break
}
if retKey != nil {
retKey.KeyData = keyData
return retKey, nil
} else {
return nil, fmt.Errorf("Could not find the GPG key")
}
}
// ListConfiguredGPGPublicKeys returns a list of all configured GPG public keys from the ConfigMap
func (db *db) ListConfiguredGPGPublicKeys(ctx context.Context) (map[string]*appsv1.GnuPGPublicKey, error) {
log.Debugf("Loading PGP public keys from config map")
result := make(map[string]*appsv1.GnuPGPublicKey)
keysCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
if err != nil {
return nil, err
}
// We have to verify all PGP keys in the ConfigMap to be valid keys before. To do so,
// we write each single one out to a temporary file and validate them through gpg.
// 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 {
if expectedKeyID := gpg.KeyID(k); expectedKeyID != "" {
parsedKey, err := validatePGPKey(p)
if err != nil {
return nil, fmt.Errorf("Could not parse GPG key for entry '%s': %s", expectedKeyID, err.Error())
}
if expectedKeyID != parsedKey.KeyID {
return nil, fmt.Errorf("Key parsed for entry with key ID '%s' had different key ID '%s'", expectedKeyID, parsedKey.KeyID)
}
result[parsedKey.KeyID] = parsedKey
} else {
return nil, fmt.Errorf("Found entry with key '%s' in ConfigMap, but this is not a valid PGP key ID", k)
}
}
return result, nil
}
// AddGPGPublicKey adds one or more public keys to the configuration
func (db *db) AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*appsv1.GnuPGPublicKey, []string, error) {
result := make(map[string]*appsv1.GnuPGPublicKey)
skipped := make([]string, 0)
keys, err := gpg.ValidatePGPKeysFromString(keyData)
if err != nil {
return nil, nil, err
}
keysCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
if err != nil {
return nil, nil, err
}
if keysCM.Data == nil {
keysCM.Data = make(map[string]string)
}
for kid, key := range keys {
if _, ok := keysCM.Data[kid]; ok {
skipped = append(skipped, kid)
log.Debugf("Not adding incoming key with kid=%s because it is configured already", kid)
} else {
result[kid] = key
keysCM.Data[kid] = key.KeyData
log.Debugf("Adding incoming key with kid=%s to database", kid)
}
}
err = db.settingsMgr.SaveGPGPublicKeyData(ctx, keysCM.Data)
if err != nil {
return nil, nil, err
}
return result, skipped, nil
}
// DeleteGPGPublicKey deletes a GPG public key from the configuration
func (db *db) DeleteGPGPublicKey(ctx context.Context, keyID string) error {
keysCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
if err != nil {
return err
}
if keysCM.Data == nil {
return fmt.Errorf("No such key configured: %s", keyID)
}
if _, ok := keysCM.Data[keyID]; !ok {
return fmt.Errorf("No such key configured: %s", keyID)
}
delete(keysCM.Data, keyID)
err = db.settingsMgr.SaveGPGPublicKeyData(ctx, keysCM.Data)
return err
}

304
util/db/gpgkeys_test.go Normal file
View file

@ -0,0 +1,304 @@
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)
}
}

View file

@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Code generated by mockery v1.1.2. DO NOT EDIT.
package mocks
@ -16,6 +16,38 @@ type ArgoDB struct {
mock.Mock
}
// AddGPGPublicKey provides a mock function with given fields: ctx, keyData
func (_m *ArgoDB) AddGPGPublicKey(ctx context.Context, keyData string) (map[string]*v1alpha1.GnuPGPublicKey, []string, error) {
ret := _m.Called(ctx, keyData)
var r0 map[string]*v1alpha1.GnuPGPublicKey
if rf, ok := ret.Get(0).(func(context.Context, string) map[string]*v1alpha1.GnuPGPublicKey); ok {
r0 = rf(ctx, keyData)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*v1alpha1.GnuPGPublicKey)
}
}
var r1 []string
if rf, ok := ret.Get(1).(func(context.Context, string) []string); ok {
r1 = rf(ctx, keyData)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]string)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, string) error); ok {
r2 = rf(ctx, keyData)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// CreateCluster provides a mock function with given fields: ctx, c
func (_m *ArgoDB) CreateCluster(ctx context.Context, c *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
ret := _m.Called(ctx, c)
@ -122,6 +154,20 @@ func (_m *ArgoDB) DeleteCluster(ctx context.Context, server string) error {
return r0
}
// DeleteGPGPublicKey provides a mock function with given fields: ctx, keyID
func (_m *ArgoDB) DeleteGPGPublicKey(ctx context.Context, keyID string) error {
ret := _m.Called(ctx, keyID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, keyID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteRepository provides a mock function with given fields: ctx, name
func (_m *ArgoDB) DeleteRepository(ctx context.Context, name string) error {
ret := _m.Called(ctx, name)
@ -242,6 +288,29 @@ func (_m *ArgoDB) ListClusters(ctx context.Context) (*v1alpha1.ClusterList, erro
return r0, r1
}
// ListConfiguredGPGPublicKeys provides a mock function with given fields: ctx
func (_m *ArgoDB) ListConfiguredGPGPublicKeys(ctx context.Context) (map[string]*v1alpha1.GnuPGPublicKey, error) {
ret := _m.Called(ctx)
var r0 map[string]*v1alpha1.GnuPGPublicKey
if rf, ok := ret.Get(0).(func(context.Context) map[string]*v1alpha1.GnuPGPublicKey); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*v1alpha1.GnuPGPublicKey)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListHelmRepositories provides a mock function with given fields: ctx
func (_m *ArgoDB) ListHelmRepositories(ctx context.Context) ([]*v1alpha1.Repository, error) {
ret := _m.Called(ctx)

View file

@ -47,6 +47,7 @@ type Client interface {
LsLargeFiles() ([]string, error)
CommitSHA() (string, error)
RevisionMetadata(revision string) (*RevisionMetadata, error)
VerifyCommitSignature(string) (string, error)
}
// nativeGitClient implements Client interface using git CLI
@ -439,6 +440,22 @@ func (m *nativeGitClient) RevisionMetadata(revision string) (*RevisionMetadata,
return &RevisionMetadata{author, time.Unix(authorDateUnixTimestamp, 0), tags, message}, nil
}
// VerifyCommitSignature Runs verify-commit on a given revision and returns the output
func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error) {
out, err := m.runGnuPGWrapper("git-verify-wrapper.sh", revision)
if err != nil {
return "", err
}
return out, nil
}
// runWrapper runs a custom command with all the semantics of running the Git client
func (m *nativeGitClient) runGnuPGWrapper(wrapper string, args ...string) (string, error) {
cmd := exec.Command(wrapper, args...)
cmd.Env = append(cmd.Env, fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()))
return m.runCmdOutput(cmd)
}
// runCmd is a convenience function to run a command in a given directory and return its output
func (m *nativeGitClient) runCmd(args ...string) (string, error) {
cmd := exec.Command("git", args...)

View file

@ -270,6 +270,45 @@ func TestLFSClient(t *testing.T) {
}
}
func TestVerifyCommitSignature(t *testing.T) {
p, err := ioutil.TempDir("", "test-verify-commit-sig")
if err != nil {
panic(err.Error())
}
defer os.RemoveAll(p)
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", p, NopCreds{}, false, false)
assert.NoError(t, err)
err = client.Init()
assert.NoError(t, err)
err = client.Fetch()
assert.NoError(t, err)
commitSHA, err := client.LsRemote("HEAD")
assert.NoError(t, err)
err = client.Checkout(commitSHA)
assert.NoError(t, err)
// 28027897aad1262662096745f2ce2d4c74d02b7f is a commit that is signed in the repo
// It doesn't matter whether we know the key or not at this stage
{
out, err := client.VerifyCommitSignature("28027897aad1262662096745f2ce2d4c74d02b7f")
assert.NoError(t, err)
assert.NotEmpty(t, out)
assert.Contains(t, out, "gpg: Signature made")
}
// 85d660f0b967960becce3d49bd51c678ba2a5d24 is a commit that is not signed
{
out, err := client.VerifyCommitSignature("85d660f0b967960becce3d49bd51c678ba2a5d24")
assert.NoError(t, err)
assert.Empty(t, out)
}
}
func TestNewFactory(t *testing.T) {
addBinDirToPath := path.NewBinDirToPath()
defer addBinDirToPath.Close()

View file

@ -2,11 +2,8 @@
package mocks
import (
mock "github.com/stretchr/testify/mock"
git "github.com/argoproj/argo-cd/util/git"
)
import git "github.com/argoproj/argo-cd/util/git"
import mock "github.com/stretchr/testify/mock"
// Client is an autogenerated mock type for the Client type
type Client struct {
@ -179,3 +176,24 @@ func (_m *Client) Root() string {
return r0
}
// VerifyCommitSignature provides a mock function with given fields: _a0
func (_m *Client) VerifyCommitSignature(_a0 string) (string, error) {
ret := _m.Called(_a0)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

682
util/gpg/gpg.go Normal file
View file

@ -0,0 +1,682 @@
package gpg
import (
"bufio"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
executil "github.com/argoproj/gitops-engine/pkg/utils/exec"
"github.com/argoproj/argo-cd/common"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
// Regular expression to match public key beginning
var subTypeMatch = regexp.MustCompile(`^pub\s+([a-z0-9]+)\s\d+-\d+-\d+\s\[[A-Z]+\].*$`)
// Regular expression to match key ID output from gpg
var keyIdMatch = regexp.MustCompile(`^\s+([0-9A-Za-z]+)\s*$`)
// Regular expression to match identity output from gpg
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 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 --generate-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
// creating the trustdb in a specific argocd-repo-server pod.
var batchKeyCreateRecipe = `%no-protection
%transient-key
Key-Type: default
Key-Length: 2048
Key-Usage: sign
Name-Real: Anon Ymous
Name-Comment: ArgoCD key signing key
Name-Email: noreply@argoproj.io
Expire-Date: 6m
%commit
`
type PGPKeyID string
func isHexString(s string) bool {
_, err := hex.DecodeString(s)
if err != nil {
return false
} else {
return true
}
}
// 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 {
if IsLongKeyID(k) {
return k[24:]
} else if IsShortKeyID(k) {
return k
}
// Invalid key
return ""
}
// IsLongKeyID returns true if the string represents a long key ID (aka fingerprint)
func IsLongKeyID(k string) bool {
if len(k) == 40 && isHexString(k) {
return true
} else {
return false
}
}
// IsShortKeyID returns true if the string represents a short key ID
func IsShortKeyID(k string) bool {
if len(k) == 16 && isHexString(k) {
return true
} else {
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"
TrustNone = "never"
TrustMarginal = "marginal"
TrustFull = "full"
TrustUltimate = "ultimate"
)
// Key trust mappings
var pgpTrustLevels = map[string]int{
TrustUnknown: 2,
TrustNone: 3,
TrustMarginal: 4,
TrustFull: 5,
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(), fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()))
}
// Helper function to write some data to a temp file and return its path
func writeKeyToFile(keyData string) (string, error) {
f, err := ioutil.TempFile("", "gpg-public-key")
if err != nil {
return "", err
}
err = ioutil.WriteFile(f.Name(), []byte(keyData), 0600)
if err != nil {
os.Remove(f.Name())
return "", err
}
f.Close()
return f.Name(), nil
}
// IsGPGEnabled returns true if GPG feature is enabled
func IsGPGEnabled() bool {
if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.ToLower(en) == "false" || strings.ToLower(en) == "no" {
return false
}
return true
}
// InitializePGP will initialize a GnuPG working directory and also create a
// transient private key so that the trust DB will work correctly.
func InitializeGnuPG() error {
gnuPgHome := common.GetGnuPGHomePath()
// We only operate if ARGOCD_GNUPGHOME is set
if gnuPgHome == "" {
return fmt.Errorf("%s is not set; refusing to initialize", common.EnvGnuPGHome)
}
// Directory set in ARGOCD_GNUPGHOME must exist and has to be a directory
st, err := os.Stat(gnuPgHome)
if err != nil {
return err
}
if !st.IsDir() {
return fmt.Errorf("%s ('%s') does not point to a directory", common.EnvGnuPGHome, gnuPgHome)
}
// Check for sane permissions as well (GPG will issue a warning otherwise)
if st.Mode().Perm() != 0700 {
return fmt.Errorf("%s at '%s' has too wide permissions, must be 0700", common.EnvGnuPGHome, gnuPgHome)
}
_, err = os.Stat(path.Join(gnuPgHome, "trustdb.gpg"))
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
// We can't initialize a second time
return fmt.Errorf("%s at %s already initialized, can't initialize again.", common.EnvGnuPGHome, gnuPgHome)
}
f, err := ioutil.TempFile("", "gpg-key-recipe")
if err != nil {
return err
}
defer os.Remove(f.Name())
_, err = f.WriteString(batchKeyCreateRecipe)
if err != nil {
return err
}
f.Close()
cmd := exec.Command("gpg", "--logger-fd", "1", "--batch", "--generate-key", f.Name())
cmd.Env = getGPGEnviron()
_, err = executil.Run(cmd)
return err
}
func ParsePGPKeyBlock(keyFile string) ([]string, error) {
return nil, nil
}
func ImportPGPKeysFromString(keyData string) ([]*appsv1.GnuPGPublicKey, error) {
f, err := ioutil.TempFile("", "gpg-key-import")
if err != nil {
return nil, err
}
defer os.Remove(f.Name())
_, err = f.WriteString(keyData)
if err != nil {
return nil, err
}
f.Close()
return ImportPGPKeys(f.Name())
}
// ImportPGPKey imports one or more keys from a file into the local keyring and optionally
// signs them with the transient private key for leveraging the trust DB.
func ImportPGPKeys(keyFile string) ([]*appsv1.GnuPGPublicKey, error) {
keys := make([]*appsv1.GnuPGPublicKey, 0)
cmd := exec.Command("gpg", "--logger-fd", "1", "--import", keyFile)
cmd.Env = getGPGEnviron()
out, err := executil.Run(cmd)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(strings.NewReader(out))
for scanner.Scan() {
if !strings.HasPrefix(scanner.Text(), "gpg: ") {
continue
}
// We ignore lines that are not of interest
token := importMatch.FindStringSubmatch(scanner.Text())
if len(token) != 3 {
continue
}
key := appsv1.GnuPGPublicKey{
KeyID: token[1],
Owner: token[2],
// By default, trust level is unknown
Trust: TrustUnknown,
// Subtype is unknown at this point
SubType: "unknown",
Fingerprint: "",
}
keys = append(keys, &key)
}
return keys, nil
}
func ValidatePGPKeysFromString(keyData string) (map[string]*appsv1.GnuPGPublicKey, error) {
f, err := writeKeyToFile(keyData)
if err != nil {
return nil, err
}
defer os.Remove(f)
return ValidatePGPKeys(f)
}
// ValidatePGPKeys validates whether the keys in keyFile are valid PGP keys and can be imported
// It does so by importing them into a temporary keyring. The returned keys are complete, that
// is, they contain all relevant information
func ValidatePGPKeys(keyFile string) (map[string]*appsv1.GnuPGPublicKey, error) {
keys := make(map[string]*appsv1.GnuPGPublicKey)
tempHome, err := ioutil.TempDir("", "gpg-verify-key")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempHome)
// Remember original GNUPGHOME, then set it to temp directory
oldGPGHome := os.Getenv(common.EnvGnuPGHome)
defer os.Setenv(common.EnvGnuPGHome, oldGPGHome)
os.Setenv(common.EnvGnuPGHome, tempHome)
// Import they keys to our temporary keyring...
_, err = ImportPGPKeys(keyFile)
if err != nil {
return nil, err
}
// ... and export them again, to get key data and fingerprint
imported, err := GetInstalledPGPKeys(nil)
if err != nil {
return nil, err
}
for _, key := range imported {
keys[key.KeyID] = key
}
return keys, nil
}
// SetPGPTrustLevel sets the given trust level on keys with specified key IDs
func SetPGPTrustLevelById(kids []string, trustLevel string) error {
keys := make([]*appsv1.GnuPGPublicKey, 0)
for _, kid := range kids {
keys = append(keys, &appsv1.GnuPGPublicKey{KeyID: kid})
}
return SetPGPTrustLevel(keys, trustLevel)
}
// SetPGPTrustLevel sets the given trust level on specified keys
func SetPGPTrustLevel(pgpKeys []*appsv1.GnuPGPublicKey, trustLevel string) error {
trust, ok := pgpTrustLevels[trustLevel]
if !ok {
return fmt.Errorf("Unknown trust level: %s", trustLevel)
}
// We need to store ownertrust specification in a temp file. Format is <fingerprint>:<level>
f, err := ioutil.TempFile("", "gpg-key-fps")
if err != nil {
return err
}
defer os.Remove(f.Name())
for _, k := range pgpKeys {
_, err := f.WriteString(fmt.Sprintf("%s:%d\n", k.KeyID, trust))
if err != nil {
return err
}
}
f.Close()
// Load ownertrust from the file we have constructed and instruct gpg to update the trustdb
cmd := exec.Command("gpg", "--import-ownertrust", f.Name())
cmd.Env = getGPGEnviron()
_, err = executil.Run(cmd)
if err != nil {
return err
}
// Update the trustdb once we updated the ownertrust, to prevent gpg to do it once we validate a signature
cmd = exec.Command("gpg", "--update-trustdb")
cmd.Env = getGPGEnviron()
_, err = executil.Run(cmd)
if err != nil {
return err
}
return nil
}
// DeletePGPKey deletes a key from our GnuPG key ring
func DeletePGPKey(keyID string) error {
args := append([]string{}, "--yes", "--batch", "--delete-keys", keyID)
cmd := exec.Command("gpg", args...)
cmd.Env = getGPGEnviron()
_, err := executil.Run(cmd)
if err != nil {
return err
}
return nil
}
// IsSecretKey returns true if the keyID also has a private key in the keyring
func IsSecretKey(keyID string) (bool, error) {
args := append([]string{}, "--list-secret-keys", keyID)
cmd := exec.Command("gpg-wrapper.sh", args...)
cmd.Env = getGPGEnviron()
out, err := executil.Run(cmd)
if err != nil {
return false, err
}
if strings.HasPrefix(out, "gpg: error reading key: No secret key") {
return false, nil
}
return true, nil
}
// 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)
args := append([]string{}, "--list-public-keys")
// kids can contain an arbitrary list of key IDs we want to list. If empty, we list all keys.
if len(kids) > 0 {
args = append(args, kids...)
}
cmd := exec.Command("gpg", args...)
cmd.Env = getGPGEnviron()
out, err := executil.Run(cmd)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(strings.NewReader(out))
var curKey *appsv1.GnuPGPublicKey = nil
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "pub ") {
// This is the beginning of a new key, time to store the previously parsed one in our list and start fresh.
if curKey != nil {
keys = append(keys, curKey)
curKey = nil
}
key := appsv1.GnuPGPublicKey{}
// Second field in pub output denotes key sub type (cipher and length)
token := subTypeMatch.FindStringSubmatch(scanner.Text())
if len(token) != 2 {
return nil, fmt.Errorf("Invalid line: %s (len=%d)", scanner.Text(), len(token))
}
key.SubType = token[1]
// Next line should be the key ID, no prefix
if !scanner.Scan() {
return nil, fmt.Errorf("Invalid output from gpg, end of text after primary key")
}
token = keyIdMatch.FindStringSubmatch(scanner.Text())
if len(token) != 2 {
return nil, fmt.Errorf("Invalid output from gpg, no key ID for primary key")
}
key.Fingerprint = token[1]
// KeyID is just the last bytes of the fingerprint
key.KeyID = token[1][24:]
if curKey == nil {
curKey = &key
}
// Next line should be UID
if !scanner.Scan() {
return nil, fmt.Errorf("Invalid output from gpg, end of text after key ID")
}
if !strings.HasPrefix(scanner.Text(), "uid ") {
return nil, fmt.Errorf("Invalid output from gpg, no identity for primary key")
}
token = uidMatch.FindStringSubmatch(scanner.Text())
if len(token) < 3 {
return nil, fmt.Errorf("Malformed identity line: %s (len=%d)", scanner.Text(), len(token))
}
// Store trust level
key.Trust = token[1]
// Identity - we are only interested in the first uid
key.Owner = token[2]
}
}
// Also store the last processed key into our list to be returned
if curKey != nil {
keys = append(keys, curKey)
}
// We need to get the final key for each imported key, so we run --export on each key
for _, key := range keys {
cmd := exec.Command("gpg", "-a", "--export", key.KeyID)
cmd.Env = getGPGEnviron()
out, err := executil.Run(cmd)
if err != nil {
return nil, err
}
key.KeyData = out
}
return keys, nil
}
// ParsePGPCommitSignature parses the output of "git verify-commit" and returns the result
func ParseGitCommitVerification(signature string) (PGPVerifyResult, error) {
result := PGPVerifyResult{Result: VerifyResultUnknown}
parseOk := false
linesParsed := 0
scanner := bufio.NewScanner(strings.NewReader(signature))
for scanner.Scan() && linesParsed < MaxVerificationLinesToParse {
linesParsed += 1
// Indicating the beginning of a signature
start := verificationStartMatch.FindStringSubmatch(scanner.Text())
if len(start) == 2 {
result.Date = start[1]
if !scanner.Scan() {
return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
}
linesParsed += 1
// What key has made the signature?
keyID := verificationKeyIDMatch.FindStringSubmatch(scanner.Text())
if len(keyID) != 3 {
return PGPVerifyResult{}, fmt.Errorf("Could not parse key ID of commit verification output.")
}
result.Cipher = keyID[1]
result.KeyID = KeyID(keyID[2])
if result.KeyID == "" {
return PGPVerifyResult{}, fmt.Errorf("Invalid PGP key ID found in verification result: %s", result.KeyID)
}
// What was the result of signature verification?
if !scanner.Scan() {
return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
}
linesParsed += 1
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 PGPVerifyResult{}, fmt.Errorf("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 successfull - return result
return result, nil
} else if linesParsed >= MaxVerificationLinesToParse {
// Too many output lines, return error
return PGPVerifyResult{}, fmt.Errorf("Too many lines of gpg verify-commit output, abort.")
} else {
// No data found, return error
return PGPVerifyResult{}, fmt.Errorf("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
// in the keyring will be installed to the keyring, files that exist in the keyring but do not exist in
// the directory will be deleted.
func SyncKeyRingFromDirectory(basePath string) ([]string, []string, error) {
configured := make(map[string]interface{})
newKeys := make([]string, 0)
fingerprints := make([]string, 0)
removedKeys := make([]string, 0)
st, err := os.Stat(basePath)
if err != nil {
return nil, nil, err
}
if !st.IsDir() {
return nil, nil, fmt.Errorf("%s is not a directory", basePath)
}
// Collect configuration, i.e. files in basePath
err = filepath.Walk(basePath, func(path string, fi os.FileInfo, err error) error {
if IsShortKeyID(fi.Name()) {
configured[fi.Name()] = true
}
return nil
})
if err != nil {
return nil, nil, err
}
// Collect GPG keys installed in the key ring
installed := make(map[string]*appsv1.GnuPGPublicKey)
keys, err := GetInstalledPGPKeys(nil)
if err != nil {
return nil, nil, err
}
for _, v := range keys {
installed[v.KeyID] = v
}
// First, add all keys that are found in the configuration but are not yet in the keyring
for key := range configured {
if _, ok := installed[key]; !ok {
addedKey, err := ImportPGPKeys(path.Join(basePath, key))
if err != nil {
return nil, nil, err
}
if len(addedKey) != 1 {
return nil, nil, fmt.Errorf("Invalid key found in %s", path.Join(basePath, key))
}
importedKey, err := GetInstalledPGPKeys([]string{addedKey[0].KeyID})
if err != nil {
return nil, nil, err
} else if len(importedKey) != 1 {
return nil, nil, fmt.Errorf("Could not get details of imported key ID %s", importedKey)
}
newKeys = append(newKeys, key)
fingerprints = append(fingerprints, importedKey[0].Fingerprint)
}
}
// Delete all keys from the keyring that are not found in the configuration anymore.
for key := range installed {
secret, err := IsSecretKey(key)
if err != nil {
return nil, nil, err
}
if _, ok := configured[key]; !ok && !secret {
err := DeletePGPKey(key)
if err != nil {
return nil, nil, err
}
removedKeys = append(removedKeys, key)
}
}
// Update owner trust for new keys
if len(fingerprints) > 0 {
_ = SetPGPTrustLevelById(fingerprints, TrustUltimate)
}
return newKeys, removedKeys, err
}

580
util/gpg/gpg_test.go Normal file
View file

@ -0,0 +1,580 @@
package gpg
import (
"fmt"
"io"
"io/ioutil"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/test"
)
const (
longKeyID = "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23"
shortKeyID = "4AEE18F83AFDEB23"
)
var syncTestSources = map[string]string{
"F7842A5CEAA9C0B1": "testdata/janedoe.asc",
"FDC79815400D88A9": "testdata/johndoe.asc",
"4AEE18F83AFDEB23": "testdata/github.asc",
}
// Helper function to create temporary GNUPGHOME
func initTempDir() string {
p, err := ioutil.TempDir("", "gpg-test")
if err != nil {
// makes no sense to continue test without temp dir
panic(err.Error())
}
fmt.Printf("-> Using %s as GNUPGHOME\n", p)
os.Setenv(common.EnvGnuPGHome, p)
return p
}
func Test_IsGPGEnabled(t *testing.T) {
os.Setenv("ARGOCD_GPG_ENABLED", "true")
assert.True(t, IsGPGEnabled())
os.Setenv("ARGOCD_GPG_ENABLED", "false")
assert.False(t, IsGPGEnabled())
os.Setenv("ARGOCD_GPG_ENABLED", "")
assert.True(t, IsGPGEnabled())
}
func Test_GPG_InitializeGnuPG(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
// First run should initialize fine
err := InitializeGnuPG()
assert.NoError(t, err)
// We should have exactly one public key with ultimate trust (our own) in the keyring
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Equal(t, keys[0].Trust, "ultimate")
// Second run should return error
err = InitializeGnuPG()
assert.Error(t, err)
assert.Contains(t, err.Error(), "already initialized")
// GNUPGHOME is a file - we need to error out
f, err := ioutil.TempFile("", "gpg-test")
assert.NoError(t, err)
defer os.Remove(f.Name())
os.Setenv(common.EnvGnuPGHome, f.Name())
err = InitializeGnuPG()
assert.Error(t, err)
assert.Contains(t, err.Error(), "does not point to a directory")
// Unaccessible GNUPGHOME
p = initTempDir()
defer os.RemoveAll(p)
fp := fmt.Sprintf("%s/gpg", p)
err = os.Mkdir(fp, 0000)
if err != nil {
panic(err.Error())
}
if err != nil {
panic(err.Error())
}
os.Setenv(common.EnvGnuPGHome, fp)
err = InitializeGnuPG()
assert.Error(t, err)
// Restore permissions so path can be deleted
err = os.Chmod(fp, 0700)
if err != nil {
panic(err.Error())
}
// GNUPGHOME with too wide permissions
p = initTempDir()
defer os.RemoveAll(p)
err = os.Chmod(p, 0777)
if err != nil {
panic(err.Error())
}
os.Setenv(common.EnvGnuPGHome, p)
err = InitializeGnuPG()
assert.Error(t, err)
}
func Test_GPG_KeyManagement(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
err := InitializeGnuPG()
assert.NoError(t, err)
// Import a single good key
keys, err := ImportPGPKeys("testdata/github.asc")
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
assert.Contains(t, keys[0].Owner, "noreply@github.com")
assert.Equal(t, "unknown", keys[0].Trust)
assert.Equal(t, "unknown", keys[0].SubType)
kids := make([]string, 0)
importedKeyId := keys[0].KeyID
// We should have a total of 2 keys in the keyring now
{
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 2)
}
// We should now have that key in our keyring with unknown trust (trustdb not updated)
{
keys, err := GetInstalledPGPKeys([]string{importedKeyId})
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
assert.Contains(t, keys[0].Owner, "noreply@github.com")
assert.Equal(t, "unknown", keys[0].Trust)
assert.Equal(t, "rsa2048", keys[0].SubType)
kids = append(kids, keys[0].Fingerprint)
}
assert.Len(t, kids, 1)
// Set trust level for our key and check the result
{
err := SetPGPTrustLevelById(kids, "ultimate")
assert.NoError(t, err)
keys, err := GetInstalledPGPKeys(kids)
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Equal(t, kids[0], keys[0].Fingerprint)
assert.Equal(t, "ultimate", keys[0].Trust)
}
// Import garbage - error expected
keys, err = ImportPGPKeys("testdata/garbage.asc")
assert.Error(t, err)
assert.Len(t, keys, 0)
// We should still have a total of 2 keys in the keyring now
{
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 2)
}
// Delete previously imported public key
{
err := DeletePGPKey(importedKeyId)
assert.NoError(t, err)
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 1)
}
// Delete non-existing key
{
err := DeletePGPKey(importedKeyId)
assert.Error(t, err)
}
// Import multiple keys
{
keys, err := ImportPGPKeys("testdata/multi.asc")
assert.NoError(t, err)
assert.Len(t, keys, 2)
assert.Contains(t, keys[0].Owner, "john.doe@example.com")
assert.Contains(t, keys[1].Owner, "jane.doe@example.com")
}
// Check if they were really imported
{
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 3)
}
}
func Test_ImportPGPKeysFromString(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
err := InitializeGnuPG()
assert.NoError(t, err)
// Import a single good key
keys, err := ImportPGPKeysFromString(test.MustLoadFileToString("testdata/github.asc"))
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
assert.Contains(t, keys[0].Owner, "noreply@github.com")
assert.Equal(t, "unknown", keys[0].Trust)
assert.Equal(t, "unknown", keys[0].SubType)
}
func Test_ValidateGPGKeysFromString(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
err := InitializeGnuPG()
assert.NoError(t, err)
{
keyData := test.MustLoadFileToString("testdata/github.asc")
keys, err := ValidatePGPKeysFromString(keyData)
assert.NoError(t, err)
assert.Len(t, keys, 1)
}
{
keyData := test.MustLoadFileToString("testdata/multi.asc")
keys, err := ValidatePGPKeysFromString(keyData)
assert.NoError(t, err)
assert.Len(t, keys, 2)
}
}
func Test_ValidateGPGKeys(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
err := InitializeGnuPG()
assert.NoError(t, err)
// Validation good case - 1 key
{
keys, err := ValidatePGPKeys("testdata/github.asc")
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Contains(t, keys, "4AEE18F83AFDEB23")
}
// Validation bad case
{
keys, err := ValidatePGPKeys("testdata/garbage.asc")
assert.Error(t, err)
assert.Len(t, keys, 0)
}
// We should still have a total of 1 keys in the keyring now
{
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 1)
}
}
func Test_GPG_ParseGitCommitVerification(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
err := InitializeGnuPG()
assert.NoError(t, err)
keys, err := ImportPGPKeys("testdata/github.asc")
assert.NoError(t, err)
assert.Len(t, keys, 1)
// Good case
{
c, err := ioutil.ReadFile("testdata/good_signature.txt")
if err != nil {
panic(err.Error())
}
res, err := ParseGitCommitVerification(string(c))
assert.NoError(t, err)
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 := ioutil.ReadFile("testdata/unknown_signature.txt")
if err != nil {
panic(err.Error())
}
res, err := ParseGitCommitVerification(string(c))
assert.NoError(t, err)
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 := ioutil.ReadFile("testdata/bad_signature_bad.txt")
if err != nil {
panic(err.Error())
}
res, err := ParseGitCommitVerification(string(c))
assert.NoError(t, err)
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 := ioutil.ReadFile("testdata/bad_signature_manipulated.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Could not parse output")
}
// Bad case: Incomplete signature data #1
{
c, err := ioutil.ReadFile("testdata/bad_signature_preeof1.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "end-of-file")
}
// Bad case: Incomplete signature data #2
{
c, err := ioutil.ReadFile("testdata/bad_signature_preeof2.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "end-of-file")
}
// Bad case: No signature data #1
{
c, err := ioutil.ReadFile("testdata/bad_signature_nodata.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "no verification data found")
}
// Bad case: Malformed signature data #1
{
c, err := ioutil.ReadFile("testdata/bad_signature_malformed1.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "no verification data found")
}
// Bad case: Malformed signature data #2
{
c, err := ioutil.ReadFile("testdata/bad_signature_malformed2.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Could not parse key ID")
}
// Bad case: Malformed signature data #3
{
c, err := ioutil.ReadFile("testdata/bad_signature_malformed3.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Could not parse result of verify")
}
// Bad case: Invalid key ID in signature
{
c, err := ioutil.ReadFile("testdata/bad_signature_badkeyid.txt")
if err != nil {
panic(err.Error())
}
_, err = ParseGitCommitVerification(string(c))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Invalid PGP key ID")
}
}
func Test_GetGnuPGHomePath(t *testing.T) {
{
os.Setenv(common.EnvGnuPGHome, "")
p := common.GetGnuPGHomePath()
assert.Equal(t, common.DefaultGnuPgHomePath, p)
}
{
os.Setenv(common.EnvGnuPGHome, "/tmp/gpghome")
p := common.GetGnuPGHomePath()
assert.Equal(t, "/tmp/gpghome", p)
}
}
func Test_KeyID(t *testing.T) {
// Good case - long key ID (aka fingerprint) to short key ID
{
res := KeyID(longKeyID)
assert.Equal(t, shortKeyID, res)
}
// Good case - short key ID remains same
{
res := KeyID(shortKeyID)
assert.Equal(t, shortKeyID, res)
}
// Bad case - key ID too short
{
keyID := "AEE18F83AFDEB23"
res := KeyID(keyID)
assert.Empty(t, res)
}
// Bad case - key ID too long
{
keyID := "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB2323"
res := KeyID(keyID)
assert.Empty(t, res)
}
// Bad case - right length, but not hex string
{
keyID := "abcdefghijklmn"
res := KeyID(keyID)
assert.Empty(t, res)
}
}
func Test_IsShortKeyID(t *testing.T) {
assert.True(t, IsShortKeyID(shortKeyID))
assert.False(t, IsShortKeyID(longKeyID))
assert.False(t, IsShortKeyID("ab"))
}
func Test_IsLongKeyID(t *testing.T) {
assert.True(t, IsLongKeyID(longKeyID))
assert.False(t, IsLongKeyID(shortKeyID))
assert.False(t, IsLongKeyID(longKeyID+"a"))
}
func Test_isHexString(t *testing.T) {
assert.True(t, isHexString("ab0099"))
assert.True(t, isHexString("AB0099"))
assert.False(t, isHexString("foobar"))
}
func Test_IsSecretKey(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
// First run should initialize fine
err := InitializeGnuPG()
assert.NoError(t, err)
// We should have exactly one public key with ultimate trust (our own) in the keyring
keys, err := GetInstalledPGPKeys(nil)
assert.NoError(t, err)
assert.Len(t, keys, 1)
assert.Equal(t, keys[0].Trust, "ultimate")
{
secret, err := IsSecretKey(keys[0].KeyID)
assert.NoError(t, err)
assert.True(t, secret)
}
{
secret, err := IsSecretKey("invalid")
assert.NoError(t, err)
assert.False(t, secret)
}
}
func Test_SyncKeyRingFromDirectory(t *testing.T) {
p := initTempDir()
defer os.RemoveAll(p)
// First run should initialize fine
err := InitializeGnuPG()
assert.NoError(t, err)
tempDir, err := ioutil.TempDir("", "gpg-sync-test")
if err != nil {
panic(err.Error())
}
defer os.RemoveAll(tempDir)
{
new, removed, err := SyncKeyRingFromDirectory(tempDir)
assert.NoError(t, err)
assert.Len(t, new, 0)
assert.Len(t, removed, 0)
}
{
for k, v := range syncTestSources {
src, err := os.Open(v)
if err != nil {
panic(err.Error())
}
defer src.Close()
dst, err := os.Create(path.Join(tempDir, k))
if err != nil {
panic(err.Error())
}
defer dst.Close()
_, err = io.Copy(dst, src)
if err != nil {
panic(err.Error())
}
dst.Close()
}
new, removed, err := SyncKeyRingFromDirectory(tempDir)
assert.NoError(t, err)
assert.Len(t, new, 3)
assert.Len(t, removed, 0)
installed, err := GetInstalledPGPKeys(new)
assert.NoError(t, err)
for _, k := range installed {
assert.Contains(t, new, k.KeyID)
}
}
{
err := os.Remove(path.Join(tempDir, "4AEE18F83AFDEB23"))
if err != nil {
panic(err.Error())
}
new, removed, err := SyncKeyRingFromDirectory(tempDir)
assert.NoError(t, err)
assert.Len(t, new, 0)
assert.Len(t, removed, 1)
installed, err := GetInstalledPGPKeys(new)
assert.NoError(t, err)
for _, k := range installed {
assert.NotEqual(t, k.KeyID, removed[0])
}
}
}

View file

@ -0,0 +1,3 @@
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

@ -0,0 +1,3 @@
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

@ -0,0 +1,3 @@
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

@ -0,0 +1,3 @@
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

@ -0,0 +1,3 @@
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

@ -0,0 +1,6 @@
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

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

View file

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

View file

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

16
util/gpg/testdata/garbage.asc vendored Normal file
View file

@ -0,0 +1,16 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
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-----

17
util/gpg/testdata/github.asc vendored Normal file
View file

@ -0,0 +1,17 @@
-----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-----

3
util/gpg/testdata/good_signature.txt vendored Normal file
View file

@ -0,0 +1,3 @@
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]

30
util/gpg/testdata/janedoe.asc vendored Normal file
View file

@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF5izVcBCADKkNZwGmtcTR7TN1tuC326+oXNewWRraKdnxWiKXW1gUROBDiW
Pic9hImYYjkyt6dz4DkAB/qJfAiRTZG/zz/qnTgbrzK9j3v4TlBTUcTtCI4fF/Sh
zutKpaIfWFDelKSIoWRh/gY6LrtnXm+PRLTckzQxUP71HrHlFFk3462+Ph+7V3z5
PrUZvbv+wJ3U5GdhhYEIBpq2fkvv2K9l9MFVWXcH7mDLxX7p/Q8OaHaSsdTtpBpk
y4IuA0RQiej0gXAEPuoO/TXKwtZ6G7eFjtcndomM2H3N7oYqZZuNW8lU+zcaV8HW
KlwNZFvnkRAO05zCtN8ljUTWkZwM8k6BOp3bABEBAAG0H0phbmUgRG9lIDxqYW5l
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQQM1Ty6UL70MGRjStf3hCpc6qnA
sQUCXmLNVwIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD3hCpc
6qnAsT7uCACeKa0jKSzGmjVhxzTT8uO1bxZXzTLi2vDQcqFBVYUBe0TXgf9I8+0s
WkvZfvZw9Mju3bxY+Tp1/e7+nKsEkQO/7rRureOa7OF/D5jJNX1QUNqUFF6LCPAB
P5RroHS3uGfdCHKyj84jrZPAhTDPMyYlrWkv8EX2YOT6dlnxgElIdc3WcwJSAtFT
WQXoY/kHmjoUe8c8NJFN1nwEHzbKtjsnkcXvs7XruUhhmqsizyCyrIS9We4Sl5r+
4zKi10FKoN/kzCjU3EHOFiQ8/l5rKTMM1lAN4q3Wyq2xeqyJ+UDx7hOGnnxKjeX9
uLay5cAy7XhOwghIQKCXtcd3T/EzlYQNuQENBF5izVcBCACiADPuJIRFkIuMLiov
rWCAtlXt3OyyZrchtRDzxLJW6nL6vaMoJ7nUabD6mlv9mfWRLG4exUID6632/mXb
lVcPYU4ZQM9HFutwi8cgq2SuiX/UJM0deJzmiQKxMNO4hUf7eQU7227jRdxkWaOj
zN7xdayH6yldVyrPWQM8i4qmpsGPZ3/EYswDhxcPYPhkA1gW1tgaUxWf/k9U1+GY
myaAI71pImRExUIc0pIv44IdGQRU3iPusgljDDXgPVhwF1EmAqFQ6aIM32h6x3WM
T6u6OtWfGUksG3RBv3M380Tegppzk2X+2V38YaJH4u/jNUXhwbu/9yR4xuA02/Z1
E44bABEBAAGJATwEGAEIACYWIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIb
DAUJA8JnAAAKCRD3hCpc6qnAsWb/CACKcrLYF4yK+vhdlTJw65znSBjIw16iH5SV
yd+z3MmiQzlqpjxfMg7iJG1wDNl4wDRa3QipRrVhKvc8wRuUK7xUKUcbIJDBmoTc
Mj5iicPucr1WQvv9OY3wXBBoWxdPgxWRBglbuZmp7s2D3ixbrd3nxGisFGXIkOAe
WtOibFHnnaKwsM+xqpYDwn4PL/DYWuh0gT5RW6JG1PWdGWMr1CHjV8VyPD3dR59l
976sRGz2oyu5gefZHtRecI65/BHtKtdf6ZZaonNXilK1XVrD2hahn0CKwumeQtS4
kx/hB+SPrcCgCdUD7FFlMA3kGZ7rBKMkHVxoOondTAJFUw4TAE6j
=ygiZ
-----END PGP PUBLIC KEY BLOCK-----

30
util/gpg/testdata/johndoe.asc vendored Normal file
View file

@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF5izSABCADAED7eSbx+ol3TLt/fJ6UZciaItts4Rar83bj8LPTFZebWTHzy
m0zoNdU3UrH3I8iWhoUUE1voqp2Hs3GEX3fHK70BodhGkGl5W931l8yYqTVlLYhE
8MxwWZKwh3phK6Wcm9GUEA3BQr5rNApWwUfgCK8NHRl2Kmb5ujmPgoap2RsH6Fpn
85gaCfUOvTV7jAZtY+LU84ZsVh0TcNoA4UieYHYWvXtYci9C0EkVbjpoRhZOkv5h
oQSBm/5Kfv+d7kZUluBsm1yyXfdHJBVuNYd7SpHe6PO3+eQ/JgqlRSfs1UBKYgx3
Sxapy16hm8vVAzE9vnxB87z0+kS0uc0Ri+abABEBAAG0H0pvaG4gRG9lIDxqb2hu
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRG7spZs6IkAFi4SpP9x5gVQA2I
qQUCXmLNIAIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9x5gV
QA2IqS84CACXKj4xJ+UAkh5q/M0jXC9JxzQu8JtVE7cGTTFyjLBApGmtfa5RtEct
QShhpdVhpuh2DhsySoza6acvwaP356HywFH64Q0MXo98XosEnSwab74k1yyd2QPR
u+kIskEbfs/j6e5uYpqf0tCvXsxIywktGcdvLE/98ISXqHS8R1uCuMrfWR9Rrz/b
8k4NY5u6IZCa+HmrZ7v3K4s1XaHbSJaz5MzJI2kFT6Ai485KBf7Iof5llr9x5U0L
rEiH1u2xIh64WvqO6u6xqxas1ewzuI6tGECU2sllZxPIt6/onCZy9LnOjJOhEAyT
P7N+q5jsF+NvqvCame9hmYDSfUv6TP9IuQENBF5izSABCACv6y8rmRC0otzl7A9p
yfoNH9FNpLaiYuT6XUMSSC97TG1jjPZ3+6TP1Ff6nEwDxf57zRq8yJZO8LMRXwyA
kIT6ZPB9lY4Z6qy1TZAd2/UVG6KR9kml+S/hOo2Y9WAz8tDpYM9rGieIW+LXcueK
lkI0TYS7FX49UFB/hXJMnnOhzZxihVo/g1rlAPLsxE2i/1TVmDD0EOMwiuOBwoyN
UurJq41sXsxYZQFAjCbUfuvWgXjM/ir97Rr8Vca5SjGNf9C4yLsDGl/eKfKPLUwP
7cgnq/pSpVaWDEAb6DyU8ttY7zZQOQjT5Gwfggxzz9U4qUOOtFkQ6piQoe1Lyzi8
6cHZABEBAAGJATwEGAEIACYWIQRG7spZs6IkAFi4SpP9x5gVQA2IqQUCXmLNIAIb
DAUJA8JnAAAKCRD9x5gVQA2IqX09CACTALlaIOxa9VlBrhaj5bHkMwXJG3DDDLm1
9aJDJfwjqEnCFT7SCggZFCBpu3PqEkq8jHGC/gnWcDoPhWtMldBRVb3MjsxjOi9t
Lk39XcoQOgYo6aFMD1Ughbg+P2QrQwvLhtIl7134MUiB65IsDRLrjXkkMhVEe1Um
0yL4doZPxZ/jm+dGxtFWcAXWBTL4lzE3fWCwMmygiuxljLl9n67glsZG7isRVMfY
U9O8kAMRoMCiktnIe+Ecw1RmAmjgDmA/jTKPGuJRTj/WO5LtWwHUXa7jptJLU1tZ
kdXx0SzOArmDG0dMwggSm9ms4Z8FT+XXe1BWKV1jLTDqBRP1z/KD
=8jOZ
-----END PGP PUBLIC KEY BLOCK-----

56
util/gpg/testdata/multi.asc vendored Normal file
View file

@ -0,0 +1,56 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF5izSABCADAED7eSbx+ol3TLt/fJ6UZciaItts4Rar83bj8LPTFZebWTHzy
m0zoNdU3UrH3I8iWhoUUE1voqp2Hs3GEX3fHK70BodhGkGl5W931l8yYqTVlLYhE
8MxwWZKwh3phK6Wcm9GUEA3BQr5rNApWwUfgCK8NHRl2Kmb5ujmPgoap2RsH6Fpn
85gaCfUOvTV7jAZtY+LU84ZsVh0TcNoA4UieYHYWvXtYci9C0EkVbjpoRhZOkv5h
oQSBm/5Kfv+d7kZUluBsm1yyXfdHJBVuNYd7SpHe6PO3+eQ/JgqlRSfs1UBKYgx3
Sxapy16hm8vVAzE9vnxB87z0+kS0uc0Ri+abABEBAAG0H0pvaG4gRG9lIDxqb2hu
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRG7spZs6IkAFi4SpP9x5gVQA2I
qQUCXmLNIAIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9x5gV
QA2IqS84CACXKj4xJ+UAkh5q/M0jXC9JxzQu8JtVE7cGTTFyjLBApGmtfa5RtEct
QShhpdVhpuh2DhsySoza6acvwaP356HywFH64Q0MXo98XosEnSwab74k1yyd2QPR
u+kIskEbfs/j6e5uYpqf0tCvXsxIywktGcdvLE/98ISXqHS8R1uCuMrfWR9Rrz/b
8k4NY5u6IZCa+HmrZ7v3K4s1XaHbSJaz5MzJI2kFT6Ai485KBf7Iof5llr9x5U0L
rEiH1u2xIh64WvqO6u6xqxas1ewzuI6tGECU2sllZxPIt6/onCZy9LnOjJOhEAyT
P7N+q5jsF+NvqvCame9hmYDSfUv6TP9IuQENBF5izSABCACv6y8rmRC0otzl7A9p
yfoNH9FNpLaiYuT6XUMSSC97TG1jjPZ3+6TP1Ff6nEwDxf57zRq8yJZO8LMRXwyA
kIT6ZPB9lY4Z6qy1TZAd2/UVG6KR9kml+S/hOo2Y9WAz8tDpYM9rGieIW+LXcueK
lkI0TYS7FX49UFB/hXJMnnOhzZxihVo/g1rlAPLsxE2i/1TVmDD0EOMwiuOBwoyN
UurJq41sXsxYZQFAjCbUfuvWgXjM/ir97Rr8Vca5SjGNf9C4yLsDGl/eKfKPLUwP
7cgnq/pSpVaWDEAb6DyU8ttY7zZQOQjT5Gwfggxzz9U4qUOOtFkQ6piQoe1Lyzi8
6cHZABEBAAGJATwEGAEIACYWIQRG7spZs6IkAFi4SpP9x5gVQA2IqQUCXmLNIAIb
DAUJA8JnAAAKCRD9x5gVQA2IqX09CACTALlaIOxa9VlBrhaj5bHkMwXJG3DDDLm1
9aJDJfwjqEnCFT7SCggZFCBpu3PqEkq8jHGC/gnWcDoPhWtMldBRVb3MjsxjOi9t
Lk39XcoQOgYo6aFMD1Ughbg+P2QrQwvLhtIl7134MUiB65IsDRLrjXkkMhVEe1Um
0yL4doZPxZ/jm+dGxtFWcAXWBTL4lzE3fWCwMmygiuxljLl9n67glsZG7isRVMfY
U9O8kAMRoMCiktnIe+Ecw1RmAmjgDmA/jTKPGuJRTj/WO5LtWwHUXa7jptJLU1tZ
kdXx0SzOArmDG0dMwggSm9ms4Z8FT+XXe1BWKV1jLTDqBRP1z/KDmQENBF5izVcB
CADKkNZwGmtcTR7TN1tuC326+oXNewWRraKdnxWiKXW1gUROBDiWPic9hImYYjky
t6dz4DkAB/qJfAiRTZG/zz/qnTgbrzK9j3v4TlBTUcTtCI4fF/ShzutKpaIfWFDe
lKSIoWRh/gY6LrtnXm+PRLTckzQxUP71HrHlFFk3462+Ph+7V3z5PrUZvbv+wJ3U
5GdhhYEIBpq2fkvv2K9l9MFVWXcH7mDLxX7p/Q8OaHaSsdTtpBpky4IuA0RQiej0
gXAEPuoO/TXKwtZ6G7eFjtcndomM2H3N7oYqZZuNW8lU+zcaV8HWKlwNZFvnkRAO
05zCtN8ljUTWkZwM8k6BOp3bABEBAAG0H0phbmUgRG9lIDxqYW5lLmRvZUBleGFt
cGxlLmNvbT6JAVQEEwEIAD4WIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIb
AwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD3hCpc6qnAsT7uCACe
Ka0jKSzGmjVhxzTT8uO1bxZXzTLi2vDQcqFBVYUBe0TXgf9I8+0sWkvZfvZw9Mju
3bxY+Tp1/e7+nKsEkQO/7rRureOa7OF/D5jJNX1QUNqUFF6LCPABP5RroHS3uGfd
CHKyj84jrZPAhTDPMyYlrWkv8EX2YOT6dlnxgElIdc3WcwJSAtFTWQXoY/kHmjoU
e8c8NJFN1nwEHzbKtjsnkcXvs7XruUhhmqsizyCyrIS9We4Sl5r+4zKi10FKoN/k
zCjU3EHOFiQ8/l5rKTMM1lAN4q3Wyq2xeqyJ+UDx7hOGnnxKjeX9uLay5cAy7XhO
wghIQKCXtcd3T/EzlYQNuQENBF5izVcBCACiADPuJIRFkIuMLiovrWCAtlXt3Oyy
ZrchtRDzxLJW6nL6vaMoJ7nUabD6mlv9mfWRLG4exUID6632/mXblVcPYU4ZQM9H
Futwi8cgq2SuiX/UJM0deJzmiQKxMNO4hUf7eQU7227jRdxkWaOjzN7xdayH6yld
VyrPWQM8i4qmpsGPZ3/EYswDhxcPYPhkA1gW1tgaUxWf/k9U1+GYmyaAI71pImRE
xUIc0pIv44IdGQRU3iPusgljDDXgPVhwF1EmAqFQ6aIM32h6x3WMT6u6OtWfGUks
G3RBv3M380Tegppzk2X+2V38YaJH4u/jNUXhwbu/9yR4xuA02/Z1E44bABEBAAGJ
ATwEGAEIACYWIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIbDAUJA8JnAAAK
CRD3hCpc6qnAsWb/CACKcrLYF4yK+vhdlTJw65znSBjIw16iH5SVyd+z3MmiQzlq
pjxfMg7iJG1wDNl4wDRa3QipRrVhKvc8wRuUK7xUKUcbIJDBmoTcMj5iicPucr1W
Qvv9OY3wXBBoWxdPgxWRBglbuZmp7s2D3ixbrd3nxGisFGXIkOAeWtOibFHnnaKw
sM+xqpYDwn4PL/DYWuh0gT5RW6JG1PWdGWMr1CHjV8VyPD3dR59l976sRGz2oyu5
gefZHtRecI65/BHtKtdf6ZZaonNXilK1XVrD2hahn0CKwumeQtS4kx/hB+SPrcCg
CdUD7FFlMA3kGZ7rBKMkHVxoOondTAJFUw4TAE6j
=DEtc
-----END PGP PUBLIC KEY BLOCK-----

73
util/gpg/testdata/multi2.asc vendored Normal file
View file

@ -0,0 +1,73 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBF5izSABCADAED7eSbx+ol3TLt/fJ6UZciaItts4Rar83bj8LPTFZebWTHzy
m0zoNdU3UrH3I8iWhoUUE1voqp2Hs3GEX3fHK70BodhGkGl5W931l8yYqTVlLYhE
8MxwWZKwh3phK6Wcm9GUEA3BQr5rNApWwUfgCK8NHRl2Kmb5ujmPgoap2RsH6Fpn
85gaCfUOvTV7jAZtY+LU84ZsVh0TcNoA4UieYHYWvXtYci9C0EkVbjpoRhZOkv5h
oQSBm/5Kfv+d7kZUluBsm1yyXfdHJBVuNYd7SpHe6PO3+eQ/JgqlRSfs1UBKYgx3
Sxapy16hm8vVAzE9vnxB87z0+kS0uc0Ri+abABEBAAG0H0pvaG4gRG9lIDxqb2hu
LmRvZUBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRG7spZs6IkAFi4SpP9x5gVQA2I
qQUCXmLNIAIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9x5gV
QA2IqS84CACXKj4xJ+UAkh5q/M0jXC9JxzQu8JtVE7cGTTFyjLBApGmtfa5RtEct
QShhpdVhpuh2DhsySoza6acvwaP356HywFH64Q0MXo98XosEnSwab74k1yyd2QPR
u+kIskEbfs/j6e5uYpqf0tCvXsxIywktGcdvLE/98ISXqHS8R1uCuMrfWR9Rrz/b
8k4NY5u6IZCa+HmrZ7v3K4s1XaHbSJaz5MzJI2kFT6Ai485KBf7Iof5llr9x5U0L
rEiH1u2xIh64WvqO6u6xqxas1ewzuI6tGECU2sllZxPIt6/onCZy9LnOjJOhEAyT
P7N+q5jsF+NvqvCame9hmYDSfUv6TP9IuQENBF5izSABCACv6y8rmRC0otzl7A9p
yfoNH9FNpLaiYuT6XUMSSC97TG1jjPZ3+6TP1Ff6nEwDxf57zRq8yJZO8LMRXwyA
kIT6ZPB9lY4Z6qy1TZAd2/UVG6KR9kml+S/hOo2Y9WAz8tDpYM9rGieIW+LXcueK
lkI0TYS7FX49UFB/hXJMnnOhzZxihVo/g1rlAPLsxE2i/1TVmDD0EOMwiuOBwoyN
UurJq41sXsxYZQFAjCbUfuvWgXjM/ir97Rr8Vca5SjGNf9C4yLsDGl/eKfKPLUwP
7cgnq/pSpVaWDEAb6DyU8ttY7zZQOQjT5Gwfggxzz9U4qUOOtFkQ6piQoe1Lyzi8
6cHZABEBAAGJATwEGAEIACYWIQRG7spZs6IkAFi4SpP9x5gVQA2IqQUCXmLNIAIb
DAUJA8JnAAAKCRD9x5gVQA2IqX09CACTALlaIOxa9VlBrhaj5bHkMwXJG3DDDLm1
9aJDJfwjqEnCFT7SCggZFCBpu3PqEkq8jHGC/gnWcDoPhWtMldBRVb3MjsxjOi9t
Lk39XcoQOgYo6aFMD1Ughbg+P2QrQwvLhtIl7134MUiB65IsDRLrjXkkMhVEe1Um
0yL4doZPxZ/jm+dGxtFWcAXWBTL4lzE3fWCwMmygiuxljLl9n67glsZG7isRVMfY
U9O8kAMRoMCiktnIe+Ecw1RmAmjgDmA/jTKPGuJRTj/WO5LtWwHUXa7jptJLU1tZ
kdXx0SzOArmDG0dMwggSm9ms4Z8FT+XXe1BWKV1jLTDqBRP1z/KDmQENBF5izVcB
CADKkNZwGmtcTR7TN1tuC326+oXNewWRraKdnxWiKXW1gUROBDiWPic9hImYYjky
t6dz4DkAB/qJfAiRTZG/zz/qnTgbrzK9j3v4TlBTUcTtCI4fF/ShzutKpaIfWFDe
lKSIoWRh/gY6LrtnXm+PRLTckzQxUP71HrHlFFk3462+Ph+7V3z5PrUZvbv+wJ3U
5GdhhYEIBpq2fkvv2K9l9MFVWXcH7mDLxX7p/Q8OaHaSsdTtpBpky4IuA0RQiej0
gXAEPuoO/TXKwtZ6G7eFjtcndomM2H3N7oYqZZuNW8lU+zcaV8HWKlwNZFvnkRAO
05zCtN8ljUTWkZwM8k6BOp3bABEBAAG0H0phbmUgRG9lIDxqYW5lLmRvZUBleGFt
cGxlLmNvbT6JAVQEEwEIAD4WIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIb
AwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD3hCpc6qnAsT7uCACe
Ka0jKSzGmjVhxzTT8uO1bxZXzTLi2vDQcqFBVYUBe0TXgf9I8+0sWkvZfvZw9Mju
3bxY+Tp1/e7+nKsEkQO/7rRureOa7OF/D5jJNX1QUNqUFF6LCPABP5RroHS3uGfd
CHKyj84jrZPAhTDPMyYlrWkv8EX2YOT6dlnxgElIdc3WcwJSAtFTWQXoY/kHmjoU
e8c8NJFN1nwEHzbKtjsnkcXvs7XruUhhmqsizyCyrIS9We4Sl5r+4zKi10FKoN/k
zCjU3EHOFiQ8/l5rKTMM1lAN4q3Wyq2xeqyJ+UDx7hOGnnxKjeX9uLay5cAy7XhO
wghIQKCXtcd3T/EzlYQNuQENBF5izVcBCACiADPuJIRFkIuMLiovrWCAtlXt3Oyy
ZrchtRDzxLJW6nL6vaMoJ7nUabD6mlv9mfWRLG4exUID6632/mXblVcPYU4ZQM9H
Futwi8cgq2SuiX/UJM0deJzmiQKxMNO4hUf7eQU7227jRdxkWaOjzN7xdayH6yld
VyrPWQM8i4qmpsGPZ3/EYswDhxcPYPhkA1gW1tgaUxWf/k9U1+GYmyaAI71pImRE
xUIc0pIv44IdGQRU3iPusgljDDXgPVhwF1EmAqFQ6aIM32h6x3WMT6u6OtWfGUks
G3RBv3M380Tegppzk2X+2V38YaJH4u/jNUXhwbu/9yR4xuA02/Z1E44bABEBAAGJ
ATwEGAEIACYWIQQM1Ty6UL70MGRjStf3hCpc6qnAsQUCXmLNVwIbDAUJA8JnAAAK
CRD3hCpc6qnAsWb/CACKcrLYF4yK+vhdlTJw65znSBjIw16iH5SVyd+z3MmiQzlq
pjxfMg7iJG1wDNl4wDRa3QipRrVhKvc8wRuUK7xUKUcbIJDBmoTcMj5iicPucr1W
Qvv9OY3wXBBoWxdPgxWRBglbuZmp7s2D3ixbrd3nxGisFGXIkOAeWtOibFHnnaKw
sM+xqpYDwn4PL/DYWuh0gT5RW6JG1PWdGWMr1CHjV8VyPD3dR59l976sRGz2oyu5
gefZHtRecI65/BHtKtdf6ZZaonNXilK1XVrD2hahn0CKwumeQtS4kx/hB+SPrcCg
CdUD7FFlMA3kGZ7rBKMkHVxoOondTAJFUw4TAE6j
=DEtc
-----END PGP PUBLIC KEY BLOCK-----
-----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-----

View file

@ -0,0 +1,3 @@
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

@ -891,6 +891,27 @@ func (mgr *SettingsManager) SaveTLSCertificateData(ctx context.Context, tlsCerti
return mgr.ResyncInformers()
}
func (mgr *SettingsManager) SaveGPGPublicKeyData(ctx context.Context, gpgPublicKeys map[string]string) error {
err := mgr.ensureSynced(false)
if err != nil {
return err
}
keysCM, err := mgr.GetConfigMapByName(common.ArgoCDGPGKeysConfigMapName)
if err != nil {
return err
}
keysCM.Data = gpgPublicKeys
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Update(keysCM)
if err != nil {
return err
}
return mgr.ResyncInformers()
}
// NewSettingsManager generates a new SettingsManager pointer and returns it
func NewSettingsManager(ctx context.Context, clientset kubernetes.Interface, namespace string) *SettingsManager {