github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/asserts/gpgkeypairmgr.go (about)

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