github.com/coreos/rocket@v1.30.1-0.20200224141603-171c416fac02/rkt/pubkey/pubkey.go (about)

     1  // Copyright 2015 The rkt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package pubkey
    16  
    17  import (
    18  	"bufio"
    19  	"crypto/tls"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  	"os"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/hashicorp/errwrap"
    32  	"github.com/rkt/rkt/pkg/keystore"
    33  	rktlog "github.com/rkt/rkt/pkg/log"
    34  	"github.com/rkt/rkt/rkt/config"
    35  
    36  	"github.com/appc/spec/discovery"
    37  	"golang.org/x/crypto/openpgp"
    38  	"golang.org/x/crypto/ssh/terminal"
    39  )
    40  
    41  type Manager struct {
    42  	AuthPerHost          map[string]config.Headerer
    43  	InsecureAllowHTTP    bool
    44  	InsecureSkipTLSCheck bool
    45  	TrustKeysFromHTTPS   bool
    46  	Ks                   *keystore.Keystore
    47  	Debug                bool
    48  }
    49  
    50  type AcceptOption int
    51  
    52  const (
    53  	AcceptForce AcceptOption = iota
    54  	AcceptAsk
    55  )
    56  
    57  var (
    58  	log    *rktlog.Logger
    59  	stdout *rktlog.Logger = rktlog.New(os.Stdout, "", false)
    60  
    61  	secureClient   = newClient(false)
    62  	insecureClient = newClient(true)
    63  )
    64  
    65  func ensureLogger(debug bool) {
    66  	if log == nil {
    67  		log = rktlog.New(os.Stderr, "pubkey", debug)
    68  	}
    69  }
    70  
    71  // GetPubKeyLocations discovers locations at prefix
    72  func (m *Manager) GetPubKeyLocations(prefix string) ([]string, error) {
    73  	ensureLogger(m.Debug)
    74  	if prefix == "" {
    75  		return nil, fmt.Errorf("empty prefix")
    76  	}
    77  
    78  	kls, err := m.metaDiscoverPubKeyLocations(prefix)
    79  	if err != nil {
    80  		return nil, errwrap.Wrap(errors.New("prefix meta discovery error"), err)
    81  	}
    82  
    83  	if len(kls) == 0 {
    84  		return nil, fmt.Errorf("meta discovery on %s resulted in no keys", prefix)
    85  	}
    86  
    87  	return kls, nil
    88  }
    89  
    90  // AddKeys adds the keys listed in pkls at prefix
    91  func (m *Manager) AddKeys(pkls []string, prefix string, accept AcceptOption) error {
    92  	ensureLogger(m.Debug)
    93  	if m.Ks == nil {
    94  		return fmt.Errorf("no keystore available to add keys to")
    95  	}
    96  
    97  	for _, pkl := range pkls {
    98  		u, err := url.Parse(pkl)
    99  		if err != nil {
   100  			return err
   101  		}
   102  		pk, err := m.getPubKey(u)
   103  		if err != nil {
   104  			return errwrap.Wrap(fmt.Errorf("error accessing the key %s", pkl), err)
   105  		}
   106  		defer pk.Close()
   107  
   108  		err = displayKey(prefix, pkl, pk)
   109  		if err != nil {
   110  			return errwrap.Wrap(fmt.Errorf("error displaying the key %s", pkl), err)
   111  		}
   112  
   113  		if m.TrustKeysFromHTTPS && u.Scheme == "https" {
   114  			accept = AcceptForce
   115  		}
   116  
   117  		if accept == AcceptAsk {
   118  			if !terminal.IsTerminal(int(os.Stdin.Fd())) || !terminal.IsTerminal(int(os.Stderr.Fd())) {
   119  				log.Printf("To trust the key for %q, do one of the following:", prefix)
   120  				log.Printf(" - call rkt with --trust-keys-from-https")
   121  				log.Printf(" - run: rkt trust --prefix %q", prefix)
   122  				return fmt.Errorf("error reviewing key: unable to ask user to review fingerprint due to lack of tty")
   123  			}
   124  			accepted, err := reviewKey()
   125  			if err != nil {
   126  				return errwrap.Wrap(errors.New("error reviewing key"), err)
   127  			}
   128  			if !accepted {
   129  				log.Printf("not trusting %q", pkl)
   130  				continue
   131  			}
   132  		}
   133  
   134  		if accept == AcceptForce {
   135  			stdout.Printf("Trusting %q for prefix %q without fingerprint review.", pkl, prefix)
   136  		} else {
   137  			stdout.Printf("Trusting %q for prefix %q after fingerprint review.", pkl, prefix)
   138  		}
   139  
   140  		if prefix == "" {
   141  			path, err := m.Ks.StoreTrustedKeyRoot(pk)
   142  			if err != nil {
   143  				return errwrap.Wrap(errors.New("error adding root key"), err)
   144  			}
   145  			stdout.Printf("Added root key at %q", path)
   146  		} else {
   147  			path, err := m.Ks.StoreTrustedKeyPrefix(prefix, pk)
   148  			if err != nil {
   149  				return errwrap.Wrap(fmt.Errorf("error adding key for prefix %q", prefix), err)
   150  			}
   151  			stdout.Printf("Added key for prefix %q at %q", prefix, path)
   152  		}
   153  	}
   154  	return nil
   155  }
   156  
   157  // metaDiscoverPubKeyLocations discovers the locations of public keys through ACDiscovery by applying prefix as an ACApp
   158  func (m *Manager) metaDiscoverPubKeyLocations(prefix string) ([]string, error) {
   159  	app, err := discovery.NewAppFromString(prefix)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	hostHeaders := config.ResolveAuthPerHost(m.AuthPerHost)
   165  	insecure := discovery.InsecureNone
   166  	if m.InsecureAllowHTTP {
   167  		insecure = insecure | discovery.InsecureHTTP
   168  	}
   169  	if m.InsecureSkipTLSCheck {
   170  		insecure = insecure | discovery.InsecureTLS
   171  	}
   172  
   173  	keys, attempts, err := discovery.DiscoverPublicKeys(*app, hostHeaders, insecure, 0)
   174  	if err != nil && m.Debug {
   175  		for _, a := range attempts {
   176  			log.PrintE(fmt.Sprintf("meta tag 'ac-discovery-pubkeys' not found on %s", a.Prefix), a.Error)
   177  		}
   178  	}
   179  
   180  	return keys, err
   181  }
   182  
   183  // getPubKey retrieves a public key (if remote), and verifies it's a gpg key
   184  func (m *Manager) getPubKey(u *url.URL) (*os.File, error) {
   185  	switch u.Scheme {
   186  	case "":
   187  		return os.Open(u.Path)
   188  	case "http":
   189  		if !m.InsecureAllowHTTP {
   190  			return nil, fmt.Errorf("--insecure-allow-http required for http URLs")
   191  		}
   192  		fallthrough
   193  	case "https":
   194  		return downloadKey(u, m.InsecureSkipTLSCheck)
   195  	}
   196  
   197  	return nil, fmt.Errorf("only local files and http or https URLs supported")
   198  }
   199  
   200  // downloadKey retrieves the file, storing it in a deleted tempfile
   201  func downloadKey(u *url.URL, skipTLSCheck bool) (*os.File, error) {
   202  	tf, err := ioutil.TempFile("", "")
   203  	if err != nil {
   204  		return nil, errwrap.Wrap(errors.New("error creating tempfile"), err)
   205  	}
   206  	os.Remove(tf.Name()) // no need to keep the tempfile around
   207  
   208  	defer func() {
   209  		if tf != nil {
   210  			tf.Close()
   211  		}
   212  	}()
   213  
   214  	// TODO(krnowak): we should probably apply credential headers
   215  	// from config here
   216  	var client *http.Client
   217  	if skipTLSCheck {
   218  		client = insecureClient
   219  	} else {
   220  		client = secureClient
   221  	}
   222  
   223  	res, err := client.Get(u.String())
   224  	if err != nil {
   225  		return nil, errwrap.Wrap(errors.New("error getting key"), err)
   226  	}
   227  	defer res.Body.Close()
   228  
   229  	if res.StatusCode != http.StatusOK {
   230  		return nil, fmt.Errorf("bad HTTP status code: %d", res.StatusCode)
   231  	}
   232  
   233  	if _, err := io.Copy(tf, res.Body); err != nil {
   234  		return nil, errwrap.Wrap(errors.New("error copying key"), err)
   235  	}
   236  
   237  	if _, err = tf.Seek(0, os.SEEK_SET); err != nil {
   238  		return nil, errwrap.Wrap(errors.New("error seeking"), err)
   239  	}
   240  
   241  	retTf := tf
   242  	tf = nil
   243  	return retTf, nil
   244  }
   245  
   246  func newClient(skipTLSCheck bool) *http.Client {
   247  	dialer := &net.Dialer{
   248  		Timeout:   30 * time.Second,
   249  		KeepAlive: 30 * time.Second,
   250  	} // values taken from stdlib v1.5.3
   251  
   252  	tr := &http.Transport{
   253  		Proxy:               http.ProxyFromEnvironment,
   254  		Dial:                dialer.Dial,
   255  		TLSHandshakeTimeout: 10 * time.Second,
   256  	} // values taken from stdlib v1.5.3
   257  
   258  	if skipTLSCheck {
   259  		tr.TLSClientConfig = &tls.Config{
   260  			InsecureSkipVerify: true,
   261  		}
   262  	}
   263  
   264  	return &http.Client{
   265  		Transport: tr,
   266  
   267  		// keys are rather small, long download times are not expected, hence setting a client timeout.
   268  		// firefox uses a 30s read timeout, being pessimistic bump to 2 minutes.
   269  		// Note that this includes connection, and tls handshake times, see https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/#clienttimeouts
   270  		Timeout: 2 * time.Minute,
   271  	}
   272  }
   273  
   274  // displayKey shows the key summary
   275  func displayKey(prefix, location string, key *os.File) error {
   276  	defer key.Seek(0, os.SEEK_SET)
   277  
   278  	kr, err := openpgp.ReadArmoredKeyRing(key)
   279  	if err != nil {
   280  		return errwrap.Wrap(errors.New("error reading key"), err)
   281  	}
   282  
   283  	log.Printf("prefix: %q\nkey: %q", prefix, location)
   284  	for _, k := range kr {
   285  		stdout.Printf("gpg key fingerprint is: %s", fingerToString(k.PrimaryKey.Fingerprint))
   286  		for _, sk := range k.Subkeys {
   287  			stdout.Printf("    Subkey fingerprint: %s", fingerToString(sk.PublicKey.Fingerprint))
   288  		}
   289  
   290  		for n := range k.Identities {
   291  			stdout.Printf("\t%s", n)
   292  		}
   293  	}
   294  
   295  	return nil
   296  }
   297  
   298  // reviewKey asks the user to accept the key
   299  func reviewKey() (bool, error) {
   300  	in := bufio.NewReader(os.Stdin)
   301  	for {
   302  		stdout.Printf("Are you sure you want to trust this key (yes/no)?")
   303  		input, err := in.ReadString('\n')
   304  		if err != nil {
   305  			return false, errwrap.Wrap(errors.New("error reading input"), err)
   306  		}
   307  		switch input {
   308  		case "yes\n":
   309  			return true, nil
   310  		case "no\n":
   311  			return false, nil
   312  		default:
   313  			stdout.Printf("Please enter 'yes' or 'no'")
   314  		}
   315  	}
   316  }
   317  
   318  func fingerToString(fpr [20]byte) string {
   319  	str := ""
   320  	for i, b := range fpr {
   321  		if i > 0 && i%2 == 0 {
   322  			str += " "
   323  			if i == 10 {
   324  				str += " "
   325  			}
   326  		}
   327  		str += strings.ToUpper(fmt.Sprintf("%.2x", b))
   328  	}
   329  	return str
   330  }