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 }