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 }