133728e barerepo 1mo
1
// Package auth also reads gpg keys, because a signature is only useful against a key barerepo holds.
133728e barerepo 1mo
2
package auth
133728e barerepo 1mo
4
import (
133728e barerepo 1mo
5
"context"
133728e barerepo 1mo
6
"errors"
133728e barerepo 1mo
7
"os"
133728e barerepo 1mo
8
"os/exec"
133728e barerepo 1mo
9
"path/filepath"
133728e barerepo 1mo
10
"strings"
133728e barerepo 1mo
13
// GPGKey is what one pasted armor says about itself.
133728e barerepo 1mo
14
type GPGKey struct {
133728e barerepo 1mo
15
Fingerprint string
133728e barerepo 1mo
16
UID string
133728e barerepo 1mo
19
// ReadGPGKey asks gpg what the armor holds without keeping any of it. It never imports.
133728e barerepo 1mo
20
func ReadGPGKey(ctx context.Context, armor string) (GPGKey, error) {
133728e barerepo 1mo
21
if !strings.Contains(armor, "BEGIN PGP PUBLIC KEY BLOCK") {
133728e barerepo 1mo
22
return GPGKey{}, errors.New("that is not a public key block. paste the output of gpg --armor --export")
133728e barerepo 1mo
24
if strings.Contains(armor, "PRIVATE KEY BLOCK") {
133728e barerepo 1mo
25
return GPGKey{}, errors.New("that is a private key. export the public half instead, and treat this one as compromised")
133728e barerepo 1mo
27
cmd := exec.CommandContext(ctx, "gpg", "--batch", "--with-colons",
133728e barerepo 1mo
28
"--import-options", "show-only", "--import")
133728e barerepo 1mo
29
cmd.Stdin = strings.NewReader(armor)
133728e barerepo 1mo
30
out, err := cmd.Output()
133728e barerepo 1mo
31
if err != nil {
133728e barerepo 1mo
32
return GPGKey{}, errors.New("that key does not read as a public key")
133728e barerepo 1mo
34
return parseColons(string(out))
133728e barerepo 1mo
37
// parseColons reads the fingerprint and the first user id out of gpg's machine format.
133728e barerepo 1mo
38
func parseColons(out string) (GPGKey, error) {
133728e barerepo 1mo
39
var k GPGKey
133728e barerepo 1mo
40
for _, line := range strings.Split(out, "\n") {
133728e barerepo 1mo
41
f := strings.Split(line, ":")
133728e barerepo 1mo
42
switch {
133728e barerepo 1mo
43
case len(f) > 9 && f[0] == "fpr" && k.Fingerprint == "":
133728e barerepo 1mo
44
k.Fingerprint = f[9]
133728e barerepo 1mo
45
case len(f) > 9 && f[0] == "uid" && k.UID == "":
133728e barerepo 1mo
46
k.UID = strings.ReplaceAll(f[9], `\x3a`, ":")
133728e barerepo 1mo
49
if k.Fingerprint == "" {
133728e barerepo 1mo
50
return k, errors.New("that key has no fingerprint, so it is not a key")
133728e barerepo 1mo
52
return k, nil
133728e barerepo 1mo
55
// Keyring writes every held key into one directory, which is what gpg needs to check a signature.
133728e barerepo 1mo
56
func Keyring(ctx context.Context, dir string, armors []string) (string, error) {
133728e barerepo 1mo
57
if len(armors) == 0 {
133728e barerepo 1mo
58
return "", errors.New("no keys")
133728e barerepo 1mo
60
if err := os.MkdirAll(dir, 0o700); err != nil {
133728e barerepo 1mo
61
return "", err
133728e barerepo 1mo
63
// No agent, because a ring of public keys never needs one and its socket path can be too long.
133728e barerepo 1mo
64
if err := os.WriteFile(filepath.Join(dir, "gpg.conf"), []byte("no-autostart\n"), 0o600); err != nil {
133728e barerepo 1mo
65
return "", err
133728e barerepo 1mo
67
cmd := exec.CommandContext(ctx, "gpg", "--batch", "--quiet", "--no-autostart", "--import")
133728e barerepo 1mo
68
cmd.Env = append(os.Environ(), "GNUPGHOME="+dir)
133728e barerepo 1mo
69
cmd.Stdin = strings.NewReader(strings.Join(armors, "\n"))
133728e barerepo 1mo
70
// The exit code is not the answer: gpg fails on a missing agent it did not need. Ask the ring.
133728e barerepo 1mo
71
_ = cmd.Run()
133728e barerepo 1mo
72
list := exec.CommandContext(ctx, "gpg", "--batch", "--no-autostart", "--list-keys", "--with-colons")
133728e barerepo 1mo
73
list.Env = append(os.Environ(), "GNUPGHOME="+dir)
133728e barerepo 1mo
74
out, err := list.Output()
133728e barerepo 1mo
75
if err != nil || !strings.Contains(string(out), "fpr:") {
133728e barerepo 1mo
76
return "", errors.New("no key could be read into the keyring")
133728e barerepo 1mo
78
return filepath.Clean(dir), nil