github.com/argoproj/argo-cd@v1.8.7/util/gpg/gpg.go (about)

     1  package gpg
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/argoproj/argo-cd/common"
    16  	appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    17  	executil "github.com/argoproj/argo-cd/util/exec"
    18  )
    19  
    20  // Regular expression to match public key beginning
    21  var subTypeMatch = regexp.MustCompile(`^pub\s+([a-z0-9]+)\s\d+-\d+-\d+\s\[[A-Z]+\].*$`)
    22  
    23  // Regular expression to match key ID output from gpg
    24  var keyIdMatch = regexp.MustCompile(`^\s+([0-9A-Za-z]+)\s*$`)
    25  
    26  // Regular expression to match identity output from gpg
    27  var uidMatch = regexp.MustCompile(`^uid\s*\[\s*([a-z]+)\s*\]\s+(.*)$`)
    28  
    29  // Regular expression to match import status
    30  var importMatch = regexp.MustCompile(`^gpg: key ([A-Z0-9]+): public key "([^"]+)" imported$`)
    31  
    32  // Regular expression to match the start of a commit signature verification
    33  var verificationStartMatch = regexp.MustCompile(`^gpg: Signature made ([a-zA-Z0-9\ :]+)$`)
    34  
    35  // Regular expression to match the key ID of a commit signature verification
    36  var verificationKeyIDMatch = regexp.MustCompile(`^gpg:\s+using\s([A-Za-z]+)\skey\s([a-zA-Z0-9]+)$`)
    37  
    38  // Regular expression to match possible additional fields of a commit signature verification
    39  var verificationAdditionalFields = regexp.MustCompile(`^gpg:\s+issuer\s.+$`)
    40  
    41  // Regular expression to match the signature status of a commit signature verification
    42  var verificationStatusMatch = regexp.MustCompile(`^gpg: ([a-zA-Z]+) signature from "([^"]+)" \[([a-zA-Z]+)\]$`)
    43  
    44  // This is the recipe for automatic key generation, passed to gpg --batch --generate-key
    45  // for initializing our keyring with a trustdb. A new private key will be generated each
    46  // time argocd-server starts, so it's transient and is not used for anything except for
    47  // creating the trustdb in a specific argocd-repo-server pod.
    48  var batchKeyCreateRecipe = `%no-protection
    49  %transient-key
    50  Key-Type: default
    51  Key-Length: 2048
    52  Key-Usage: sign
    53  Name-Real: Anon Ymous
    54  Name-Comment: ArgoCD key signing key
    55  Name-Email: noreply@argoproj.io
    56  Expire-Date: 6m
    57  %commit
    58  `
    59  
    60  // Canary marker for GNUPGHOME created by Argo CD
    61  const canaryMarkerFilename = ".argocd-generated"
    62  
    63  type PGPKeyID string
    64  
    65  func isHexString(s string) bool {
    66  	_, err := hex.DecodeString(s)
    67  	if err != nil {
    68  		return false
    69  	} else {
    70  		return true
    71  	}
    72  }
    73  
    74  // 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.
    75  func KeyID(k string) string {
    76  	if IsLongKeyID(k) {
    77  		return k[24:]
    78  	} else if IsShortKeyID(k) {
    79  		return k
    80  	}
    81  	// Invalid key
    82  	return ""
    83  }
    84  
    85  // IsLongKeyID returns true if the string represents a long key ID (aka fingerprint)
    86  func IsLongKeyID(k string) bool {
    87  	if len(k) == 40 && isHexString(k) {
    88  		return true
    89  	} else {
    90  		return false
    91  	}
    92  }
    93  
    94  // IsShortKeyID returns true if the string represents a short key ID
    95  func IsShortKeyID(k string) bool {
    96  	if len(k) == 16 && isHexString(k) {
    97  		return true
    98  	} else {
    99  		return false
   100  	}
   101  }
   102  
   103  // Result of a git commit verification
   104  type PGPVerifyResult struct {
   105  	// Date the signature was made
   106  	Date string
   107  	// KeyID the signature was made with
   108  	KeyID string
   109  	// Identity
   110  	Identity string
   111  	// Trust level of the key
   112  	Trust string
   113  	// Cipher of the key the signature was made with
   114  	Cipher string
   115  	// Result of verification - "unknown", "good" or "bad"
   116  	Result string
   117  	// Additional informational message
   118  	Message string
   119  }
   120  
   121  // Signature verification results
   122  const (
   123  	VerifyResultGood    = "Good"
   124  	VerifyResultBad     = "Bad"
   125  	VerifyResultInvalid = "Invalid"
   126  	VerifyResultUnknown = "Unknown"
   127  )
   128  
   129  // Key trust values
   130  const (
   131  	TrustUnknown  = "unknown"
   132  	TrustNone     = "never"
   133  	TrustMarginal = "marginal"
   134  	TrustFull     = "full"
   135  	TrustUltimate = "ultimate"
   136  )
   137  
   138  // Key trust mappings
   139  var pgpTrustLevels = map[string]int{
   140  	TrustUnknown:  2,
   141  	TrustNone:     3,
   142  	TrustMarginal: 4,
   143  	TrustFull:     5,
   144  	TrustUltimate: 6,
   145  }
   146  
   147  // Maximum number of lines to parse for a gpg verify-commit output
   148  const MaxVerificationLinesToParse = 40
   149  
   150  // Helper function to append GNUPGHOME for a command execution environment
   151  func getGPGEnviron() []string {
   152  	return append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()))
   153  }
   154  
   155  // Helper function to write some data to a temp file and return its path
   156  func writeKeyToFile(keyData string) (string, error) {
   157  	f, err := ioutil.TempFile("", "gpg-public-key")
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  
   162  	err = ioutil.WriteFile(f.Name(), []byte(keyData), 0600)
   163  	if err != nil {
   164  		os.Remove(f.Name())
   165  		return "", err
   166  	}
   167  	f.Close()
   168  	return f.Name(), nil
   169  }
   170  
   171  // removeKeyRing removes an already initialized keyring from the file system
   172  // This must only be called on container startup, when no gpg-agent is running
   173  // yet, otherwise key generation will fail.
   174  func removeKeyRing(path string) error {
   175  	_, err := os.Stat(filepath.Join(path, canaryMarkerFilename))
   176  	if err != nil {
   177  		if os.IsNotExist(err) {
   178  			return fmt.Errorf("refusing to remove directory %s: it's not initialized by Argo CD", path)
   179  		} else {
   180  			return err
   181  		}
   182  	}
   183  	rd, err := os.Open(path)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer rd.Close()
   188  	dns, err := rd.Readdirnames(-1)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	for _, p := range dns {
   193  		if p == "." || p == ".." {
   194  			continue
   195  		}
   196  		err := os.RemoveAll(filepath.Join(path, p))
   197  		if err != nil {
   198  			return err
   199  		}
   200  	}
   201  	return nil
   202  }
   203  
   204  // IsGPGEnabled returns true if GPG feature is enabled
   205  func IsGPGEnabled() bool {
   206  	if en := os.Getenv("ARGOCD_GPG_ENABLED"); strings.ToLower(en) == "false" || strings.ToLower(en) == "no" {
   207  		return false
   208  	}
   209  	return true
   210  }
   211  
   212  // InitializePGP will initialize a GnuPG working directory and also create a
   213  // transient private key so that the trust DB will work correctly.
   214  func InitializeGnuPG() error {
   215  
   216  	gnuPgHome := common.GetGnuPGHomePath()
   217  
   218  	// We only operate if ARGOCD_GNUPGHOME is set
   219  	if gnuPgHome == "" {
   220  		return fmt.Errorf("%s is not set; refusing to initialize", common.EnvGnuPGHome)
   221  	}
   222  
   223  	// Directory set in ARGOCD_GNUPGHOME must exist and has to be a directory
   224  	st, err := os.Stat(gnuPgHome)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	if !st.IsDir() {
   230  		return fmt.Errorf("%s ('%s') does not point to a directory", common.EnvGnuPGHome, gnuPgHome)
   231  	}
   232  
   233  	_, err = os.Stat(path.Join(gnuPgHome, "trustdb.gpg"))
   234  	if err != nil {
   235  		if !os.IsNotExist(err) {
   236  			return err
   237  		}
   238  	} else {
   239  		// This usually happens with emptyDir mount on container crash - we need to
   240  		// re-initialize key ring.
   241  		err = removeKeyRing(gnuPgHome)
   242  		if err != nil {
   243  			return fmt.Errorf("re-initializing keyring at %s failed: %v", gnuPgHome, err)
   244  		}
   245  	}
   246  
   247  	err = ioutil.WriteFile(filepath.Join(gnuPgHome, canaryMarkerFilename), []byte("canary"), 0644)
   248  	if err != nil {
   249  		return fmt.Errorf("could not create canary: %v", err)
   250  	}
   251  
   252  	f, err := ioutil.TempFile("", "gpg-key-recipe")
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	defer os.Remove(f.Name())
   258  
   259  	_, err = f.WriteString(batchKeyCreateRecipe)
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	f.Close()
   265  
   266  	cmd := exec.Command("gpg", "--no-permission-warning", "--logger-fd", "1", "--batch", "--generate-key", f.Name())
   267  	cmd.Env = getGPGEnviron()
   268  
   269  	_, err = executil.Run(cmd)
   270  	return err
   271  }
   272  
   273  func ParsePGPKeyBlock(keyFile string) ([]string, error) {
   274  	return nil, nil
   275  }
   276  
   277  func ImportPGPKeysFromString(keyData string) ([]*appsv1.GnuPGPublicKey, error) {
   278  	f, err := ioutil.TempFile("", "gpg-key-import")
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	defer os.Remove(f.Name())
   283  	_, err = f.WriteString(keyData)
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  	f.Close()
   288  	return ImportPGPKeys(f.Name())
   289  }
   290  
   291  // ImportPGPKey imports one or more keys from a file into the local keyring and optionally
   292  // signs them with the transient private key for leveraging the trust DB.
   293  func ImportPGPKeys(keyFile string) ([]*appsv1.GnuPGPublicKey, error) {
   294  	keys := make([]*appsv1.GnuPGPublicKey, 0)
   295  
   296  	cmd := exec.Command("gpg", "--no-permission-warning", "--logger-fd", "1", "--import", keyFile)
   297  	cmd.Env = getGPGEnviron()
   298  
   299  	out, err := executil.Run(cmd)
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  
   304  	scanner := bufio.NewScanner(strings.NewReader(out))
   305  	for scanner.Scan() {
   306  		if !strings.HasPrefix(scanner.Text(), "gpg: ") {
   307  			continue
   308  		}
   309  		// We ignore lines that are not of interest
   310  		token := importMatch.FindStringSubmatch(scanner.Text())
   311  		if len(token) != 3 {
   312  			continue
   313  		}
   314  
   315  		key := appsv1.GnuPGPublicKey{
   316  			KeyID: token[1],
   317  			Owner: token[2],
   318  			// By default, trust level is unknown
   319  			Trust: TrustUnknown,
   320  			// Subtype is unknown at this point
   321  			SubType:     "unknown",
   322  			Fingerprint: "",
   323  		}
   324  
   325  		keys = append(keys, &key)
   326  	}
   327  
   328  	return keys, nil
   329  }
   330  
   331  func ValidatePGPKeysFromString(keyData string) (map[string]*appsv1.GnuPGPublicKey, error) {
   332  	f, err := writeKeyToFile(keyData)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	defer os.Remove(f)
   337  
   338  	return ValidatePGPKeys(f)
   339  }
   340  
   341  // ValidatePGPKeys validates whether the keys in keyFile are valid PGP keys and can be imported
   342  // It does so by importing them into a temporary keyring. The returned keys are complete, that
   343  // is, they contain all relevant information
   344  func ValidatePGPKeys(keyFile string) (map[string]*appsv1.GnuPGPublicKey, error) {
   345  	keys := make(map[string]*appsv1.GnuPGPublicKey)
   346  	tempHome, err := ioutil.TempDir("", "gpg-verify-key")
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  	defer os.RemoveAll(tempHome)
   351  
   352  	// Remember original GNUPGHOME, then set it to temp directory
   353  	oldGPGHome := os.Getenv(common.EnvGnuPGHome)
   354  	defer os.Setenv(common.EnvGnuPGHome, oldGPGHome)
   355  	os.Setenv(common.EnvGnuPGHome, tempHome)
   356  
   357  	// Import they keys to our temporary keyring...
   358  	_, err = ImportPGPKeys(keyFile)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	// ... and export them again, to get key data and fingerprint
   364  	imported, err := GetInstalledPGPKeys(nil)
   365  	if err != nil {
   366  		return nil, err
   367  	}
   368  
   369  	for _, key := range imported {
   370  		keys[key.KeyID] = key
   371  	}
   372  
   373  	return keys, nil
   374  }
   375  
   376  // SetPGPTrustLevel sets the given trust level on keys with specified key IDs
   377  func SetPGPTrustLevelById(kids []string, trustLevel string) error {
   378  	keys := make([]*appsv1.GnuPGPublicKey, 0)
   379  	for _, kid := range kids {
   380  		keys = append(keys, &appsv1.GnuPGPublicKey{KeyID: kid})
   381  	}
   382  	return SetPGPTrustLevel(keys, trustLevel)
   383  }
   384  
   385  // SetPGPTrustLevel sets the given trust level on specified keys
   386  func SetPGPTrustLevel(pgpKeys []*appsv1.GnuPGPublicKey, trustLevel string) error {
   387  	trust, ok := pgpTrustLevels[trustLevel]
   388  	if !ok {
   389  		return fmt.Errorf("Unknown trust level: %s", trustLevel)
   390  	}
   391  
   392  	// We need to store ownertrust specification in a temp file. Format is <fingerprint>:<level>
   393  	f, err := ioutil.TempFile("", "gpg-key-fps")
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	defer os.Remove(f.Name())
   399  
   400  	for _, k := range pgpKeys {
   401  		_, err := f.WriteString(fmt.Sprintf("%s:%d\n", k.KeyID, trust))
   402  		if err != nil {
   403  			return err
   404  		}
   405  	}
   406  
   407  	f.Close()
   408  
   409  	// Load ownertrust from the file we have constructed and instruct gpg to update the trustdb
   410  	cmd := exec.Command("gpg", "--no-permission-warning", "--import-ownertrust", f.Name())
   411  	cmd.Env = getGPGEnviron()
   412  
   413  	_, err = executil.Run(cmd)
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	// Update the trustdb once we updated the ownertrust, to prevent gpg to do it once we validate a signature
   419  	cmd = exec.Command("gpg", "--no-permission-warning", "--update-trustdb")
   420  	cmd.Env = getGPGEnviron()
   421  	_, err = executil.Run(cmd)
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	return nil
   427  }
   428  
   429  // DeletePGPKey deletes a key from our GnuPG key ring
   430  func DeletePGPKey(keyID string) error {
   431  	args := append([]string{}, "--no-permission-warning", "--yes", "--batch", "--delete-keys", keyID)
   432  	cmd := exec.Command("gpg", args...)
   433  	cmd.Env = getGPGEnviron()
   434  
   435  	_, err := executil.Run(cmd)
   436  	if err != nil {
   437  		return err
   438  	}
   439  
   440  	return nil
   441  }
   442  
   443  // IsSecretKey returns true if the keyID also has a private key in the keyring
   444  func IsSecretKey(keyID string) (bool, error) {
   445  	args := append([]string{}, "--no-permission-warning", "--list-secret-keys", keyID)
   446  	cmd := exec.Command("gpg-wrapper.sh", args...)
   447  	cmd.Env = getGPGEnviron()
   448  	out, err := executil.Run(cmd)
   449  	if err != nil {
   450  		return false, err
   451  	}
   452  	if strings.HasPrefix(out, "gpg: error reading key: No secret key") {
   453  		return false, nil
   454  	}
   455  	return true, nil
   456  }
   457  
   458  // GetInstalledPGPKeys() runs gpg to retrieve public keys from our keyring. If kids is non-empty, limit result to those key IDs
   459  func GetInstalledPGPKeys(kids []string) ([]*appsv1.GnuPGPublicKey, error) {
   460  	keys := make([]*appsv1.GnuPGPublicKey, 0)
   461  
   462  	args := append([]string{}, "--no-permission-warning", "--list-public-keys")
   463  	// kids can contain an arbitrary list of key IDs we want to list. If empty, we list all keys.
   464  	if len(kids) > 0 {
   465  		args = append(args, kids...)
   466  	}
   467  	cmd := exec.Command("gpg", args...)
   468  	cmd.Env = getGPGEnviron()
   469  
   470  	out, err := executil.Run(cmd)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  
   475  	scanner := bufio.NewScanner(strings.NewReader(out))
   476  	var curKey *appsv1.GnuPGPublicKey = nil
   477  	for scanner.Scan() {
   478  		if strings.HasPrefix(scanner.Text(), "pub ") {
   479  			// This is the beginning of a new key, time to store the previously parsed one in our list and start fresh.
   480  			if curKey != nil {
   481  				keys = append(keys, curKey)
   482  				curKey = nil
   483  			}
   484  
   485  			key := appsv1.GnuPGPublicKey{}
   486  
   487  			// Second field in pub output denotes key sub type (cipher and length)
   488  			token := subTypeMatch.FindStringSubmatch(scanner.Text())
   489  			if len(token) != 2 {
   490  				return nil, fmt.Errorf("Invalid line: %s (len=%d)", scanner.Text(), len(token))
   491  			}
   492  			key.SubType = token[1]
   493  
   494  			// Next line should be the key ID, no prefix
   495  			if !scanner.Scan() {
   496  				return nil, fmt.Errorf("Invalid output from gpg, end of text after primary key")
   497  			}
   498  
   499  			token = keyIdMatch.FindStringSubmatch(scanner.Text())
   500  			if len(token) != 2 {
   501  				return nil, fmt.Errorf("Invalid output from gpg, no key ID for primary key")
   502  			}
   503  
   504  			key.Fingerprint = token[1]
   505  			// KeyID is just the last bytes of the fingerprint
   506  			key.KeyID = token[1][24:]
   507  
   508  			if curKey == nil {
   509  				curKey = &key
   510  			}
   511  
   512  			// Next line should be UID
   513  			if !scanner.Scan() {
   514  				return nil, fmt.Errorf("Invalid output from gpg, end of text after key ID")
   515  			}
   516  
   517  			if !strings.HasPrefix(scanner.Text(), "uid ") {
   518  				return nil, fmt.Errorf("Invalid output from gpg, no identity for primary key")
   519  			}
   520  
   521  			token = uidMatch.FindStringSubmatch(scanner.Text())
   522  
   523  			if len(token) < 3 {
   524  				return nil, fmt.Errorf("Malformed identity line: %s (len=%d)", scanner.Text(), len(token))
   525  			}
   526  
   527  			// Store trust level
   528  			key.Trust = token[1]
   529  
   530  			// Identity - we are only interested in the first uid
   531  			key.Owner = token[2]
   532  		}
   533  	}
   534  
   535  	// Also store the last processed key into our list to be returned
   536  	if curKey != nil {
   537  		keys = append(keys, curKey)
   538  	}
   539  
   540  	// We need to get the final key for each imported key, so we run --export on each key
   541  	for _, key := range keys {
   542  		cmd := exec.Command("gpg", "--no-permission-warning", "-a", "--export", key.KeyID)
   543  		cmd.Env = getGPGEnviron()
   544  
   545  		out, err := executil.Run(cmd)
   546  		if err != nil {
   547  			return nil, err
   548  		}
   549  		key.KeyData = out
   550  	}
   551  
   552  	return keys, nil
   553  }
   554  
   555  // ParsePGPCommitSignature parses the output of "git verify-commit" and returns the result
   556  func ParseGitCommitVerification(signature string) (PGPVerifyResult, error) {
   557  	result := PGPVerifyResult{Result: VerifyResultUnknown}
   558  	parseOk := false
   559  	linesParsed := 0
   560  
   561  	scanner := bufio.NewScanner(strings.NewReader(signature))
   562  	for scanner.Scan() && linesParsed < MaxVerificationLinesToParse {
   563  		linesParsed += 1
   564  
   565  		// Indicating the beginning of a signature
   566  		start := verificationStartMatch.FindStringSubmatch(scanner.Text())
   567  		if len(start) == 2 {
   568  			result.Date = start[1]
   569  			if !scanner.Scan() {
   570  				return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
   571  			}
   572  
   573  			linesParsed += 1
   574  
   575  			// What key has made the signature?
   576  			keyID := verificationKeyIDMatch.FindStringSubmatch(scanner.Text())
   577  			if len(keyID) != 3 {
   578  				return PGPVerifyResult{}, fmt.Errorf("Could not parse key ID of commit verification output.")
   579  			}
   580  
   581  			result.Cipher = keyID[1]
   582  			result.KeyID = KeyID(keyID[2])
   583  			if result.KeyID == "" {
   584  				return PGPVerifyResult{}, fmt.Errorf("Invalid PGP key ID found in verification result: %s", result.KeyID)
   585  			}
   586  
   587  			// What was the result of signature verification?
   588  			if !scanner.Scan() {
   589  				return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
   590  			}
   591  
   592  			linesParsed += 1
   593  
   594  			// Skip additional fields
   595  			for verificationAdditionalFields.MatchString(scanner.Text()) {
   596  				if !scanner.Scan() {
   597  					return PGPVerifyResult{}, fmt.Errorf("Unexpected end-of-file while parsing commit verification output.")
   598  				}
   599  
   600  				linesParsed += 1
   601  			}
   602  
   603  			if strings.HasPrefix(scanner.Text(), "gpg: Can't check signature: ") {
   604  				result.Result = VerifyResultInvalid
   605  				result.Identity = "unknown"
   606  				result.Trust = TrustUnknown
   607  				result.Message = scanner.Text()
   608  			} else {
   609  				sigState := verificationStatusMatch.FindStringSubmatch(scanner.Text())
   610  				if len(sigState) != 4 {
   611  					return PGPVerifyResult{}, fmt.Errorf("Could not parse result of verify operation, check logs for more information.")
   612  				}
   613  
   614  				switch strings.ToLower(sigState[1]) {
   615  				case "good":
   616  					result.Result = VerifyResultGood
   617  				case "bad":
   618  					result.Result = VerifyResultBad
   619  				default:
   620  					result.Result = VerifyResultInvalid
   621  				}
   622  				result.Identity = sigState[2]
   623  
   624  				// Did we catch a valid trust?
   625  				if _, ok := pgpTrustLevels[sigState[3]]; ok {
   626  					result.Trust = sigState[3]
   627  				} else {
   628  					result.Trust = TrustUnknown
   629  				}
   630  				result.Message = "Success verifying the commit signature."
   631  			}
   632  
   633  			// No more data to parse here
   634  			parseOk = true
   635  			break
   636  		}
   637  	}
   638  
   639  	if parseOk && linesParsed < MaxVerificationLinesToParse {
   640  		// Operation successfull - return result
   641  		return result, nil
   642  	} else if linesParsed >= MaxVerificationLinesToParse {
   643  		// Too many output lines, return error
   644  		return PGPVerifyResult{}, fmt.Errorf("Too many lines of gpg verify-commit output, abort.")
   645  	} else {
   646  		// No data found, return error
   647  		return PGPVerifyResult{}, fmt.Errorf("Could not parse output of verify-commit, no verification data found.")
   648  	}
   649  }
   650  
   651  // SyncKeyRingFromDirectory will sync the GPG keyring with files in a directory. This is a one-way sync,
   652  // with the configuration being the leading information.
   653  // Files must have a file name matching their Key ID. Keys that are found in the directory but are not
   654  // in the keyring will be installed to the keyring, files that exist in the keyring but do not exist in
   655  // the directory will be deleted.
   656  func SyncKeyRingFromDirectory(basePath string) ([]string, []string, error) {
   657  	configured := make(map[string]interface{})
   658  	newKeys := make([]string, 0)
   659  	fingerprints := make([]string, 0)
   660  	removedKeys := make([]string, 0)
   661  	st, err := os.Stat(basePath)
   662  
   663  	if err != nil {
   664  		return nil, nil, err
   665  	}
   666  	if !st.IsDir() {
   667  		return nil, nil, fmt.Errorf("%s is not a directory", basePath)
   668  	}
   669  
   670  	// Collect configuration, i.e. files in basePath
   671  	err = filepath.Walk(basePath, func(path string, fi os.FileInfo, err error) error {
   672  		if err != nil {
   673  			return err
   674  		}
   675  		if fi == nil {
   676  			return nil
   677  		}
   678  		if IsShortKeyID(fi.Name()) {
   679  			configured[fi.Name()] = true
   680  		}
   681  		return nil
   682  	})
   683  	if err != nil {
   684  		return nil, nil, err
   685  	}
   686  
   687  	// Collect GPG keys installed in the key ring
   688  	installed := make(map[string]*appsv1.GnuPGPublicKey)
   689  	keys, err := GetInstalledPGPKeys(nil)
   690  	if err != nil {
   691  		return nil, nil, err
   692  	}
   693  	for _, v := range keys {
   694  		installed[v.KeyID] = v
   695  	}
   696  
   697  	// First, add all keys that are found in the configuration but are not yet in the keyring
   698  	for key := range configured {
   699  		if _, ok := installed[key]; !ok {
   700  			addedKey, err := ImportPGPKeys(path.Join(basePath, key))
   701  			if err != nil {
   702  				return nil, nil, err
   703  			}
   704  			if len(addedKey) != 1 {
   705  				return nil, nil, fmt.Errorf("Invalid key found in %s", path.Join(basePath, key))
   706  			}
   707  			importedKey, err := GetInstalledPGPKeys([]string{addedKey[0].KeyID})
   708  			if err != nil {
   709  				return nil, nil, err
   710  			} else if len(importedKey) != 1 {
   711  				return nil, nil, fmt.Errorf("Could not get details of imported key ID %s", importedKey)
   712  			}
   713  			newKeys = append(newKeys, key)
   714  			fingerprints = append(fingerprints, importedKey[0].Fingerprint)
   715  		}
   716  	}
   717  
   718  	// Delete all keys from the keyring that are not found in the configuration anymore.
   719  	for key := range installed {
   720  		secret, err := IsSecretKey(key)
   721  		if err != nil {
   722  			return nil, nil, err
   723  		}
   724  		if _, ok := configured[key]; !ok && !secret {
   725  			err := DeletePGPKey(key)
   726  			if err != nil {
   727  				return nil, nil, err
   728  			}
   729  			removedKeys = append(removedKeys, key)
   730  		}
   731  	}
   732  
   733  	// Update owner trust for new keys
   734  	if len(fingerprints) > 0 {
   735  		_ = SetPGPTrustLevelById(fingerprints, TrustUltimate)
   736  	}
   737  
   738  	return newKeys, removedKeys, err
   739  }