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 }