Use a Pipe() rather than a file to pass the passphrase to the command line tool. Pass the file descriptor to read the passphrase from as fd '3'. Signed-off-by: Stefan Berger <stefanb@linux.ibm.com>
		
			
				
	
	
		
			427 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			427 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   Copyright The containerd Authors.
 | 
						|
 | 
						|
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
   you may not use this file except in compliance with the License.
 | 
						|
   You may obtain a copy of the License at
 | 
						|
 | 
						|
       http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
   Unless required by applicable law or agreed to in writing, software
 | 
						|
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
   See the License for the specific language governing permissions and
 | 
						|
   limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package encryption
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/containerd/containerd/errdefs"
 | 
						|
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"golang.org/x/crypto/ssh/terminal"
 | 
						|
)
 | 
						|
 | 
						|
// GPGVersion enum representing the GPG client version to use.
 | 
						|
type GPGVersion int
 | 
						|
 | 
						|
const (
 | 
						|
	// GPGv2 signifies gpgv2+
 | 
						|
	GPGv2 GPGVersion = iota
 | 
						|
	// GPGv1 signifies gpgv1+
 | 
						|
	GPGv1
 | 
						|
	// GPGVersionUndetermined signifies gpg client version undetermined
 | 
						|
	GPGVersionUndetermined
 | 
						|
)
 | 
						|
 | 
						|
// GPGClient defines an interface for wrapping the gpg command line tools
 | 
						|
type GPGClient interface {
 | 
						|
	// ReadGPGPubRingFile gets the byte sequence of the gpg public keyring
 | 
						|
	ReadGPGPubRingFile() ([]byte, error)
 | 
						|
	// GetGPGPrivateKey gets the private key bytes of a keyid given a passphrase
 | 
						|
	GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error)
 | 
						|
	// GetSecretKeyDetails gets the details of a secret key
 | 
						|
	GetSecretKeyDetails(keyid uint64) ([]byte, bool, error)
 | 
						|
	// GetKeyDetails gets the details of a public key
 | 
						|
	GetKeyDetails(keyid uint64) ([]byte, bool, error)
 | 
						|
	// ResolveRecipients resolves PGP key ids to user names
 | 
						|
	ResolveRecipients([]string) []string
 | 
						|
}
 | 
						|
 | 
						|
// gpgClient contains generic gpg client information
 | 
						|
type gpgClient struct {
 | 
						|
	gpgHomeDir string
 | 
						|
}
 | 
						|
 | 
						|
// gpgv2Client is a gpg2 client
 | 
						|
type gpgv2Client struct {
 | 
						|
	gpgClient
 | 
						|
}
 | 
						|
 | 
						|
// gpgv1Client is a gpg client
 | 
						|
type gpgv1Client struct {
 | 
						|
	gpgClient
 | 
						|
}
 | 
						|
 | 
						|
// GuessGPGVersion guesses the version of gpg. Defaults to gpg2 if exists, if
 | 
						|
// not defaults to regular gpg.
 | 
						|
func GuessGPGVersion() GPGVersion {
 | 
						|
	if err := exec.Command("gpg2", "--version").Run(); err == nil {
 | 
						|
		return GPGv2
 | 
						|
	} else if err := exec.Command("gpg", "--version").Run(); err == nil {
 | 
						|
		return GPGv1
 | 
						|
	} else {
 | 
						|
		return GPGVersionUndetermined
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// NewGPGClient creates a new GPGClient object representing the given version
 | 
						|
// and using the given home directory
 | 
						|
func NewGPGClient(gpgVersion, gpgHomeDir string) (GPGClient, error) {
 | 
						|
	v := new(GPGVersion)
 | 
						|
	switch gpgVersion {
 | 
						|
	case "v1":
 | 
						|
		*v = GPGv1
 | 
						|
	case "v2":
 | 
						|
		*v = GPGv2
 | 
						|
	default:
 | 
						|
		v = nil
 | 
						|
	}
 | 
						|
	return newGPGClient(v, gpgHomeDir)
 | 
						|
}
 | 
						|
 | 
						|
func newGPGClient(version *GPGVersion, homedir string) (GPGClient, error) {
 | 
						|
	var gpgVersion GPGVersion
 | 
						|
	if version != nil {
 | 
						|
		gpgVersion = *version
 | 
						|
	} else {
 | 
						|
		gpgVersion = GuessGPGVersion()
 | 
						|
	}
 | 
						|
 | 
						|
	switch gpgVersion {
 | 
						|
	case GPGv1:
 | 
						|
		return &gpgv1Client{
 | 
						|
			gpgClient: gpgClient{gpgHomeDir: homedir},
 | 
						|
		}, nil
 | 
						|
	case GPGv2:
 | 
						|
		return &gpgv2Client{
 | 
						|
			gpgClient: gpgClient{gpgHomeDir: homedir},
 | 
						|
		}, nil
 | 
						|
	case GPGVersionUndetermined:
 | 
						|
		return nil, fmt.Errorf("unable to determine GPG version")
 | 
						|
	default:
 | 
						|
		return nil, fmt.Errorf("unhandled case: NewGPGClient")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
 | 
						|
func (gc *gpgv2Client) GetGPGPrivateKey(keyid uint64, passphrase string) ([]byte, error) {
 | 
						|
	var args []string
 | 
						|
 | 
						|
	if gc.gpgHomeDir != "" {
 | 
						|
		args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
 | 
						|
	}
 | 
						|
 | 
						|
	rfile, wfile, err := os.Pipe()
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrapf(err, "could not create pipe")
 | 
						|
	}
 | 
						|
	defer func() {
 | 
						|
		rfile.Close()
 | 
						|
		wfile.Close()
 | 
						|
	}()
 | 
						|
	// fill pipe in background
 | 
						|
	go func(passphrase string) {
 | 
						|
		wfile.Write([]byte(passphrase))
 | 
						|
		wfile.Close()
 | 
						|
	}(passphrase)
 | 
						|
 | 
						|
	args = append(args, []string{"--pinentry-mode", "loopback", "--batch", "--passphrase-fd", fmt.Sprintf("%d", 3), "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...)
 | 
						|
 | 
						|
	cmd := exec.Command("gpg2", args...)
 | 
						|
	cmd.ExtraFiles = []*os.File{rfile}
 | 
						|
 | 
						|
	return runGPGGetOutput(cmd)
 | 
						|
}
 | 
						|
 | 
						|
// ReadGPGPubRingFile reads the GPG public key ring file
 | 
						|
func (gc *gpgv2Client) ReadGPGPubRingFile() ([]byte, error) {
 | 
						|
	var args []string
 | 
						|
 | 
						|
	if gc.gpgHomeDir != "" {
 | 
						|
		args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
 | 
						|
	}
 | 
						|
	args = append(args, []string{"--batch", "--export"}...)
 | 
						|
 | 
						|
	cmd := exec.Command("gpg2", args...)
 | 
						|
 | 
						|
	return runGPGGetOutput(cmd)
 | 
						|
}
 | 
						|
 | 
						|
func (gc *gpgv2Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) {
 | 
						|
	var args []string
 | 
						|
 | 
						|
	if gc.gpgHomeDir != "" {
 | 
						|
		args = append([]string{"--homedir", gc.gpgHomeDir})
 | 
						|
	}
 | 
						|
	args = append(args, option, fmt.Sprintf("0x%x", keyid))
 | 
						|
 | 
						|
	cmd := exec.Command("gpg2", args...)
 | 
						|
 | 
						|
	keydata, err := runGPGGetOutput(cmd)
 | 
						|
	return keydata, err == nil, err
 | 
						|
}
 | 
						|
 | 
						|
// GetSecretKeyDetails retrives the secret key details of key with keyid.
 | 
						|
// returns a byte array of the details and a bool if the key exists
 | 
						|
func (gc *gpgv2Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) {
 | 
						|
	return gc.getKeyDetails("-K", keyid)
 | 
						|
}
 | 
						|
 | 
						|
// GetKeyDetails retrives the public key details of key with keyid.
 | 
						|
// returns a byte array of the details and a bool if the key exists
 | 
						|
func (gc *gpgv2Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) {
 | 
						|
	return gc.getKeyDetails("-k", keyid)
 | 
						|
}
 | 
						|
 | 
						|
// ResolveRecipients converts PGP keyids to email addresses, if possible
 | 
						|
func (gc *gpgv2Client) ResolveRecipients(recipients []string) []string {
 | 
						|
	return resolveRecipients(gc, recipients)
 | 
						|
}
 | 
						|
 | 
						|
// GetGPGPrivateKey gets the bytes of a specified keyid, supplying a passphrase
 | 
						|
func (gc *gpgv1Client) GetGPGPrivateKey(keyid uint64, _ string) ([]byte, error) {
 | 
						|
	var args []string
 | 
						|
 | 
						|
	if gc.gpgHomeDir != "" {
 | 
						|
		args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
 | 
						|
	}
 | 
						|
	args = append(args, []string{"--batch", "--export-secret-key", fmt.Sprintf("0x%x", keyid)}...)
 | 
						|
 | 
						|
	cmd := exec.Command("gpg", args...)
 | 
						|
 | 
						|
	return runGPGGetOutput(cmd)
 | 
						|
}
 | 
						|
 | 
						|
// ReadGPGPubRingFile reads the GPG public key ring file
 | 
						|
func (gc *gpgv1Client) ReadGPGPubRingFile() ([]byte, error) {
 | 
						|
	var args []string
 | 
						|
 | 
						|
	if gc.gpgHomeDir != "" {
 | 
						|
		args = append(args, []string{"--homedir", gc.gpgHomeDir}...)
 | 
						|
	}
 | 
						|
	args = append(args, []string{"--batch", "--export"}...)
 | 
						|
 | 
						|
	cmd := exec.Command("gpg", args...)
 | 
						|
 | 
						|
	return runGPGGetOutput(cmd)
 | 
						|
}
 | 
						|
 | 
						|
func (gc *gpgv1Client) getKeyDetails(option string, keyid uint64) ([]byte, bool, error) {
 | 
						|
	var args []string
 | 
						|
 | 
						|
	if gc.gpgHomeDir != "" {
 | 
						|
		args = append([]string{"--homedir", gc.gpgHomeDir})
 | 
						|
	}
 | 
						|
	args = append(args, option, fmt.Sprintf("0x%x", keyid))
 | 
						|
 | 
						|
	cmd := exec.Command("gpg", args...)
 | 
						|
 | 
						|
	keydata, err := runGPGGetOutput(cmd)
 | 
						|
 | 
						|
	return keydata, err == nil, err
 | 
						|
}
 | 
						|
 | 
						|
// GetSecretKeyDetails retrives the secret key details of key with keyid.
 | 
						|
// returns a byte array of the details and a bool if the key exists
 | 
						|
func (gc *gpgv1Client) GetSecretKeyDetails(keyid uint64) ([]byte, bool, error) {
 | 
						|
	return gc.getKeyDetails("-K", keyid)
 | 
						|
}
 | 
						|
 | 
						|
// GetKeyDetails retrives the public key details of key with keyid.
 | 
						|
// returns a byte array of the details and a bool if the key exists
 | 
						|
func (gc *gpgv1Client) GetKeyDetails(keyid uint64) ([]byte, bool, error) {
 | 
						|
	return gc.getKeyDetails("-k", keyid)
 | 
						|
}
 | 
						|
 | 
						|
// ResolveRecipients converts PGP keyids to email addresses, if possible
 | 
						|
func (gc *gpgv1Client) ResolveRecipients(recipients []string) []string {
 | 
						|
	return resolveRecipients(gc, recipients)
 | 
						|
}
 | 
						|
 | 
						|
// runGPGGetOutput runs the GPG commandline and returns stdout as byte array
 | 
						|
// and any stderr in the error
 | 
						|
func runGPGGetOutput(cmd *exec.Cmd) ([]byte, error) {
 | 
						|
	stdout, err := cmd.StdoutPipe()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	stderr, err := cmd.StderrPipe()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if err := cmd.Start(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	stdoutstr, err2 := ioutil.ReadAll(stdout)
 | 
						|
	stderrstr, _ := ioutil.ReadAll(stderr)
 | 
						|
 | 
						|
	if err := cmd.Wait(); err != nil {
 | 
						|
		return nil, fmt.Errorf("error from %s: %s", cmd.Path, string(stderrstr))
 | 
						|
	}
 | 
						|
 | 
						|
	return stdoutstr, err2
 | 
						|
}
 | 
						|
 | 
						|
// resolveRecipients walks the list of recipients and attempts to convert
 | 
						|
// all keyIds to email addresses; if something goes wrong during the
 | 
						|
// conversion of a recipient, the original string is returned for that
 | 
						|
// recpient
 | 
						|
func resolveRecipients(gc GPGClient, recipients []string) []string {
 | 
						|
	var result []string
 | 
						|
 | 
						|
	for _, recipient := range recipients {
 | 
						|
		keyID, err := strconv.ParseUint(recipient, 0, 64)
 | 
						|
		if err != nil {
 | 
						|
			result = append(result, recipient)
 | 
						|
		} else {
 | 
						|
			details, found, _ := gc.GetKeyDetails(keyID)
 | 
						|
			if !found {
 | 
						|
				result = append(result, recipient)
 | 
						|
			} else {
 | 
						|
				email := extractEmailFromDetails(details)
 | 
						|
				if email == "" {
 | 
						|
					result = append(result, recipient)
 | 
						|
				} else {
 | 
						|
					result = append(result, email)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result
 | 
						|
}
 | 
						|
 | 
						|
var emailPattern = regexp.MustCompile(`uid\s+\[.*\]\s.*\s<(?P<email>.+)>`)
 | 
						|
 | 
						|
func extractEmailFromDetails(details []byte) string {
 | 
						|
	loc := emailPattern.FindSubmatchIndex(details)
 | 
						|
	if len(loc) == 0 {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	return string(emailPattern.Expand(nil, []byte("$email"), details, loc))
 | 
						|
}
 | 
						|
 | 
						|
// uint64ToStringArray converts an array of uint64's to an array of strings
 | 
						|
// by applying a format string to each uint64
 | 
						|
func uint64ToStringArray(format string, in []uint64) []string {
 | 
						|
	var ret []string
 | 
						|
 | 
						|
	for _, v := range in {
 | 
						|
		ret = append(ret, fmt.Sprintf(format, v))
 | 
						|
	}
 | 
						|
	return ret
 | 
						|
}
 | 
						|
 | 
						|
// GPGGetPrivateKey walks the list of layerInfos and tries to decrypt the
 | 
						|
// wrapped symmetric keys. For this it determines whether a private key is
 | 
						|
// in the GPGVault or on this system and prompts for the passwords for those
 | 
						|
// that are available. If we do not find a private key on the system for
 | 
						|
// getting to the symmetric key of a layer then an error is generated.
 | 
						|
func GPGGetPrivateKey(descs []ocispec.Descriptor, gpgClient GPGClient, gpgVault GPGVault, mustFindKey bool) (gpgPrivKeys [][]byte, gpgPrivKeysPwds [][]byte, err error) {
 | 
						|
	// PrivateKeyData describes a private key
 | 
						|
	type PrivateKeyData struct {
 | 
						|
		KeyData         []byte
 | 
						|
		KeyDataPassword []byte
 | 
						|
	}
 | 
						|
	var pkd PrivateKeyData
 | 
						|
	keyIDPasswordMap := make(map[uint64]PrivateKeyData)
 | 
						|
 | 
						|
	for _, desc := range descs {
 | 
						|
		for scheme, b64pgpPackets := range GetWrappedKeysMap(desc) {
 | 
						|
			if scheme != "pgp" {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			keywrapper := GetKeyWrapper(scheme)
 | 
						|
			if keywrapper == nil {
 | 
						|
				return nil, nil, errors.Errorf("could not get KeyWrapper for %s\n", scheme)
 | 
						|
			}
 | 
						|
			keyIds, err := keywrapper.GetKeyIdsFromPacket(b64pgpPackets)
 | 
						|
			if err != nil {
 | 
						|
				return nil, nil, err
 | 
						|
			}
 | 
						|
 | 
						|
			found := false
 | 
						|
			for _, keyid := range keyIds {
 | 
						|
				// do we have this key? -- first check the vault
 | 
						|
				if gpgVault != nil {
 | 
						|
					_, keydata := gpgVault.GetGPGPrivateKey(keyid)
 | 
						|
					if len(keydata) > 0 {
 | 
						|
						pkd = PrivateKeyData{
 | 
						|
							KeyData:         keydata,
 | 
						|
							KeyDataPassword: nil, // password not supported in this case
 | 
						|
						}
 | 
						|
						keyIDPasswordMap[keyid] = pkd
 | 
						|
						found = true
 | 
						|
						break
 | 
						|
					}
 | 
						|
				} else if gpgClient != nil {
 | 
						|
					// check the local system's gpg installation
 | 
						|
					keyinfo, haveKey, _ := gpgClient.GetSecretKeyDetails(keyid)
 | 
						|
					// this may fail if the key is not here; we ignore the error
 | 
						|
					if !haveKey {
 | 
						|
						// key not on this system
 | 
						|
						continue
 | 
						|
					}
 | 
						|
 | 
						|
					_, found = keyIDPasswordMap[keyid]
 | 
						|
					if !found {
 | 
						|
						fmt.Printf("Passphrase required for Key id 0x%x: \n%v", keyid, string(keyinfo))
 | 
						|
						fmt.Printf("Enter passphrase for key with Id 0x%x: ", keyid)
 | 
						|
 | 
						|
						password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
 | 
						|
						fmt.Printf("\n")
 | 
						|
						if err != nil {
 | 
						|
							return nil, nil, err
 | 
						|
						}
 | 
						|
						keydata, err := gpgClient.GetGPGPrivateKey(keyid, string(password))
 | 
						|
						if err != nil {
 | 
						|
							return nil, nil, err
 | 
						|
						}
 | 
						|
						pkd = PrivateKeyData{
 | 
						|
							KeyData:         keydata,
 | 
						|
							KeyDataPassword: password,
 | 
						|
						}
 | 
						|
						keyIDPasswordMap[keyid] = pkd
 | 
						|
						found = true
 | 
						|
					}
 | 
						|
					break
 | 
						|
				} else {
 | 
						|
					return nil, nil, errors.Wrapf(errdefs.ErrInvalidArgument, "no GPGVault or GPGClient passed.")
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if !found && len(b64pgpPackets) > 0 && mustFindKey {
 | 
						|
				ids := uint64ToStringArray("0x%x", keyIds)
 | 
						|
 | 
						|
				return nil, nil, errors.Wrapf(errdefs.ErrNotFound, "missing key for decryption of layer %x of %s. Need one of the following keys: %s", desc.Digest, desc.Platform, strings.Join(ids, ", "))
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, pkd := range keyIDPasswordMap {
 | 
						|
		gpgPrivKeys = append(gpgPrivKeys, pkd.KeyData)
 | 
						|
		gpgPrivKeysPwds = append(gpgPrivKeysPwds, pkd.KeyDataPassword)
 | 
						|
	}
 | 
						|
 | 
						|
	return gpgPrivKeys, gpgPrivKeysPwds, nil
 | 
						|
}
 |