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 }