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 }