// Package auth also reads gpg keys, because a signature is only useful against a key barerepo holds. package auth import ( "context" "errors" "os" "os/exec" "path/filepath" "strings" ) // GPGKey is what one pasted armor says about itself. type GPGKey struct { Fingerprint string UID string } // ReadGPGKey asks gpg what the armor holds without keeping any of it. It never imports. func ReadGPGKey(ctx context.Context, armor string) (GPGKey, error) { if !strings.Contains(armor, "BEGIN PGP PUBLIC KEY BLOCK") { return GPGKey{}, errors.New("that is not a public key block. paste the output of gpg --armor --export") } if strings.Contains(armor, "PRIVATE KEY BLOCK") { return GPGKey{}, errors.New("that is a private key. export the public half instead, and treat this one as compromised") } cmd := exec.CommandContext(ctx, "gpg", "--batch", "--with-colons", "--import-options", "show-only", "--import") cmd.Stdin = strings.NewReader(armor) out, err := cmd.Output() if err != nil { return GPGKey{}, errors.New("that key does not read as a public key") } return parseColons(string(out)) } // parseColons reads the fingerprint and the first user id out of gpg's machine format. func parseColons(out string) (GPGKey, error) { var k GPGKey for _, line := range strings.Split(out, "\n") { f := strings.Split(line, ":") switch { case len(f) > 9 && f[0] == "fpr" && k.Fingerprint == "": k.Fingerprint = f[9] case len(f) > 9 && f[0] == "uid" && k.UID == "": k.UID = strings.ReplaceAll(f[9], `\x3a`, ":") } } if k.Fingerprint == "" { return k, errors.New("that key has no fingerprint, so it is not a key") } return k, nil } // Keyring writes every held key into one directory, which is what gpg needs to check a signature. func Keyring(ctx context.Context, dir string, armors []string) (string, error) { if len(armors) == 0 { return "", errors.New("no keys") } if err := os.MkdirAll(dir, 0o700); err != nil { return "", err } // No agent, because a ring of public keys never needs one and its socket path can be too long. if err := os.WriteFile(filepath.Join(dir, "gpg.conf"), []byte("no-autostart\n"), 0o600); err != nil { return "", err } cmd := exec.CommandContext(ctx, "gpg", "--batch", "--quiet", "--no-autostart", "--import") cmd.Env = append(os.Environ(), "GNUPGHOME="+dir) cmd.Stdin = strings.NewReader(strings.Join(armors, "\n")) // The exit code is not the answer: gpg fails on a missing agent it did not need. Ask the ring. _ = cmd.Run() list := exec.CommandContext(ctx, "gpg", "--batch", "--no-autostart", "--list-keys", "--with-colons") list.Env = append(os.Environ(), "GNUPGHOME="+dir) out, err := list.Output() if err != nil || !strings.Contains(string(out), "fpr:") { return "", errors.New("no key could be read into the keyring") } return filepath.Clean(dir), nil }