github.com/argoproj/argo-cd/v3@v3.2.1/util/gpg/gpg.go (about)

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