github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/asserts/gpgkeypairmgr.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2021 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package asserts
    21  
    22  import (
    23  	"bytes"
    24  	"errors"
    25  	"fmt"
    26  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"golang.org/x/crypto/openpgp/packet"
    32  
    33  	"github.com/snapcore/snapd/osutil"
    34  )
    35  
    36  func ensureGPGHomeDirectory() (string, error) {
    37  	real, err := osutil.UserMaybeSudoUser()
    38  	if err != nil {
    39  		return "", err
    40  	}
    41  
    42  	uid, gid, err := osutil.UidGid(real)
    43  	if err != nil {
    44  		return "", err
    45  	}
    46  
    47  	homedir := os.Getenv("SNAP_GNUPG_HOME")
    48  	if homedir == "" {
    49  		homedir = filepath.Join(real.HomeDir, ".snap", "gnupg")
    50  	}
    51  
    52  	if err := osutil.MkdirAllChown(homedir, 0700, uid, gid); err != nil {
    53  		return "", err
    54  	}
    55  	return homedir, nil
    56  }
    57  
    58  // findGPGCommand returns the path to a suitable GnuPG binary to use.
    59  // GnuPG 2 is mainly intended for desktop use, and is hard for us to use
    60  // here: in particular, it's extremely difficult to use it to delete a
    61  // secret key without a pinentry prompt (which would be necessary in our
    62  // test suite).  GnuPG 1 is still supported so it's reasonable to continue
    63  // using that for now.
    64  func findGPGCommand() (string, error) {
    65  	if path := os.Getenv("SNAP_GNUPG_CMD"); path != "" {
    66  		return path, nil
    67  	}
    68  
    69  	path, err := exec.LookPath("gpg1")
    70  	if err != nil {
    71  		path, err = exec.LookPath("gpg")
    72  	}
    73  	return path, err
    74  }
    75  
    76  func runGPGImpl(input []byte, args ...string) ([]byte, error) {
    77  	homedir, err := ensureGPGHomeDirectory()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	// Ensure the gpg-agent knows what tty to talk to to ask for
    83  	// the passphrase. This is needed because we drive gpg over
    84  	// a pipe and if the agent is not already started it will
    85  	// fail to be able to ask for a password.
    86  	if os.Getenv("GPG_TTY") == "" {
    87  		tty, err := os.Readlink("/proc/self/fd/0")
    88  		if err != nil {
    89  			return nil, err
    90  		}
    91  		os.Setenv("GPG_TTY", tty)
    92  	}
    93  
    94  	general := []string{"--homedir", homedir, "-q", "--no-auto-check-trustdb"}
    95  	allArgs := append(general, args...)
    96  
    97  	path, err := findGPGCommand()
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	cmd := exec.Command(path, allArgs...)
   102  	var outBuf bytes.Buffer
   103  	var errBuf bytes.Buffer
   104  
   105  	if len(input) != 0 {
   106  		cmd.Stdin = bytes.NewBuffer(input)
   107  	}
   108  
   109  	cmd.Stdout = &outBuf
   110  	cmd.Stderr = &errBuf
   111  
   112  	if err := cmd.Run(); err != nil {
   113  		return nil, fmt.Errorf("%s %s failed: %v (%q)", path, strings.Join(args, " "), err, errBuf.Bytes())
   114  	}
   115  
   116  	return outBuf.Bytes(), nil
   117  }
   118  
   119  var runGPG = runGPGImpl
   120  
   121  // A key pair manager backed by a local GnuPG setup.
   122  type GPGKeypairManager struct{}
   123  
   124  func (gkm *GPGKeypairManager) gpg(input []byte, args ...string) ([]byte, error) {
   125  	return runGPG(input, args...)
   126  }
   127  
   128  // NewGPGKeypairManager creates a new key pair manager backed by a local GnuPG setup.
   129  // Importing keys through the keypair manager interface is not
   130  // suppored.
   131  // Main purpose is allowing signing using keys from a GPG setup.
   132  func NewGPGKeypairManager() *GPGKeypairManager {
   133  	return &GPGKeypairManager{}
   134  }
   135  
   136  func (gkm *GPGKeypairManager) retrieve(fpr string) (PrivateKey, error) {
   137  	out, err := gkm.gpg(nil, "--batch", "--export", "--export-options", "export-minimal,export-clean,no-export-attributes", "0x"+fpr)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	if len(out) == 0 {
   142  		return nil, fmt.Errorf("cannot retrieve key with fingerprint %q in GPG keyring", fpr)
   143  	}
   144  
   145  	pubKeyBuf := bytes.NewBuffer(out)
   146  	privKey, err := newExtPGPPrivateKey(pubKeyBuf, "GPG", func(content []byte) (*packet.Signature, error) {
   147  		return gkm.sign(fpr, content)
   148  	})
   149  	if err != nil {
   150  		return nil, fmt.Errorf("cannot load GPG public key with fingerprint %q: %v", fpr, err)
   151  	}
   152  	gotFingerprint := privKey.externalID
   153  	if gotFingerprint != fpr {
   154  		return nil, fmt.Errorf("got wrong public key from GPG, expected fingerprint %q: %s", fpr, gotFingerprint)
   155  	}
   156  	return privKey, nil
   157  }
   158  
   159  // Walk iterates over all the RSA private keys in the local GPG setup calling the provided callback until this returns an error
   160  func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint string, uid string) error) error {
   161  	// see GPG source doc/DETAILS
   162  	out, err := gkm.gpg(nil, "--batch", "--list-secret-keys", "--fingerprint", "--with-colons", "--fixed-list-mode")
   163  	if err != nil {
   164  		return err
   165  	}
   166  	lines := strings.Split(string(out), "\n")
   167  	n := len(lines)
   168  	if n > 0 && lines[n-1] == "" {
   169  		n--
   170  	}
   171  	if n == 0 {
   172  		return nil
   173  	}
   174  	lines = lines[:n]
   175  	for j := 0; j < n; j++ {
   176  		// sec: line
   177  		line := lines[j]
   178  		if !strings.HasPrefix(line, "sec:") {
   179  			continue
   180  		}
   181  		secFields := strings.Split(line, ":")
   182  		if len(secFields) < 5 {
   183  			continue
   184  		}
   185  		if secFields[3] != "1" { // not RSA
   186  			continue
   187  		}
   188  		keyID := secFields[4]
   189  		uid := ""
   190  		fpr := ""
   191  		var privKey PrivateKey
   192  		// look for fpr:, uid: lines, order may vary and gpg2.1
   193  		// may springle additional lines in (like gpr:)
   194  	Loop:
   195  		for k := j + 1; k < n && !strings.HasPrefix(lines[k], "sec:"); k++ {
   196  			switch {
   197  			case strings.HasPrefix(lines[k], "fpr:"):
   198  				fprFields := strings.Split(lines[k], ":")
   199  				// extract "Field 10 - User-ID"
   200  				// A FPR record stores the fingerprint here.
   201  				if len(fprFields) < 10 {
   202  					break Loop
   203  				}
   204  				fpr = fprFields[9]
   205  				if !strings.HasSuffix(fpr, keyID) {
   206  					break // strange, skip
   207  				}
   208  				privKey, err = gkm.retrieve(fpr)
   209  				if err != nil {
   210  					return err
   211  				}
   212  			case strings.HasPrefix(lines[k], "uid:"):
   213  				uidFields := strings.Split(lines[k], ":")
   214  				// extract "*** Field 10 - User-ID"
   215  				if len(uidFields) < 10 {
   216  					break Loop
   217  				}
   218  				uid = uidFields[9]
   219  			}
   220  		}
   221  		// sanity checking
   222  		if privKey == nil || uid == "" {
   223  			continue
   224  		}
   225  		// collected it all
   226  		err = consider(privKey, fpr, uid)
   227  		if err != nil {
   228  			return err
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  func (gkm *GPGKeypairManager) Put(privKey PrivateKey) error {
   235  	// NOTE: we don't need this initially at least and this keypair mgr is not for general arbitrary usage
   236  	return fmt.Errorf("cannot import private key into GPG keyring")
   237  }
   238  
   239  func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) {
   240  	stop := errors.New("stop marker")
   241  	var hit PrivateKey
   242  	match := func(privk PrivateKey, fpr string, uid string) error {
   243  		if privk.PublicKey().ID() == keyID {
   244  			hit = privk
   245  			return stop
   246  		}
   247  		return nil
   248  	}
   249  	err := gkm.Walk(match)
   250  	if err == stop {
   251  		return hit, nil
   252  	}
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  	return nil, fmt.Errorf("cannot find key %q in GPG keyring", keyID)
   257  }
   258  
   259  func (gkm *GPGKeypairManager) sign(fingerprint string, content []byte) (*packet.Signature, error) {
   260  	out, err := gkm.gpg(content, "--personal-digest-preferences", "SHA512", "--default-key", "0x"+fingerprint, "--detach-sign")
   261  	if err != nil {
   262  		return nil, fmt.Errorf("cannot sign using GPG: %v", err)
   263  	}
   264  
   265  	badSig := "bad GPG produced signature: "
   266  	sigpkt, err := packet.Read(bytes.NewBuffer(out))
   267  	if err != nil {
   268  		return nil, fmt.Errorf(badSig+"%v", err)
   269  	}
   270  
   271  	sig, ok := sigpkt.(*packet.Signature)
   272  	if !ok {
   273  		return nil, fmt.Errorf(badSig+"got %T", sigpkt)
   274  	}
   275  
   276  	return sig, nil
   277  }
   278  
   279  type gpgKeypairInfo struct {
   280  	privKey     PrivateKey
   281  	fingerprint string
   282  }
   283  
   284  func (gkm *GPGKeypairManager) findByName(name string) (*gpgKeypairInfo, error) {
   285  	stop := errors.New("stop marker")
   286  	var hit *gpgKeypairInfo
   287  	match := func(privk PrivateKey, fpr string, uid string) error {
   288  		if uid == name {
   289  			hit = &gpgKeypairInfo{
   290  				privKey:     privk,
   291  				fingerprint: fpr,
   292  			}
   293  			return stop
   294  		}
   295  		return nil
   296  	}
   297  	err := gkm.Walk(match)
   298  	if err == stop {
   299  		return hit, nil
   300  	}
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	return nil, fmt.Errorf("cannot find key named %q in GPG keyring", name)
   305  }
   306  
   307  // GetByName looks up a private key by name and returns it.
   308  func (gkm *GPGKeypairManager) GetByName(name string) (PrivateKey, error) {
   309  	keyInfo, err := gkm.findByName(name)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	return keyInfo.privKey, nil
   314  }
   315  
   316  var generateTemplate = `
   317  Key-Type: RSA
   318  Key-Length: 4096
   319  Name-Real: %s
   320  Creation-Date: seconds=%d
   321  Preferences: SHA512
   322  `
   323  
   324  func (gkm *GPGKeypairManager) parametersForGenerate(passphrase string, name string) string {
   325  	fixedCreationTime := v1FixedTimestamp.Unix()
   326  	generateParams := fmt.Sprintf(generateTemplate, name, fixedCreationTime)
   327  	if passphrase != "" {
   328  		generateParams += "Passphrase: " + passphrase + "\n"
   329  	}
   330  	return generateParams
   331  }
   332  
   333  // Generate creates a new key with the given passphrase and name.
   334  func (gkm *GPGKeypairManager) Generate(passphrase string, name string) error {
   335  	_, err := gkm.findByName(name)
   336  	if err == nil {
   337  		return fmt.Errorf("key named %q already exists in GPG keyring", name)
   338  	}
   339  	generateParams := gkm.parametersForGenerate(passphrase, name)
   340  	_, err = gkm.gpg([]byte(generateParams), "--batch", "--gen-key")
   341  	if err != nil {
   342  		return err
   343  	}
   344  	return nil
   345  }
   346  
   347  // Export returns the encoded text of the named public key.
   348  func (gkm *GPGKeypairManager) Export(name string) ([]byte, error) {
   349  	keyInfo, err := gkm.findByName(name)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	return EncodePublicKey(keyInfo.privKey.PublicKey())
   354  }
   355  
   356  // Delete removes the named key pair from GnuPG's storage.
   357  func (gkm *GPGKeypairManager) Delete(name string) error {
   358  	keyInfo, err := gkm.findByName(name)
   359  	if err != nil {
   360  		return err
   361  	}
   362  	_, err = gkm.gpg(nil, "--batch", "--delete-secret-and-public-key", "0x"+keyInfo.fingerprint)
   363  	if err != nil {
   364  		return err
   365  	}
   366  	return nil
   367  }
   368  
   369  func (gkm *GPGKeypairManager) List() (res []ExternalKeyInfo, err error) {
   370  	collect := func(privk PrivateKey, fpr string, uid string) error {
   371  		key := ExternalKeyInfo{
   372  			Name: uid,
   373  			ID:   privk.PublicKey().ID(),
   374  		}
   375  		res = append(res, key)
   376  		return nil
   377  	}
   378  	if err := gkm.Walk(collect); err != nil {
   379  		return nil, err
   380  	}
   381  	return res, nil
   382  }