summaryrefslogtreecommitdiff
path: root/vendor/github.com/containers/ocicrypt/gpg.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/containers/ocicrypt/gpg.go')
-rw-r--r--vendor/github.com/containers/ocicrypt/gpg.go425
1 files changed, 425 insertions, 0 deletions
diff --git a/vendor/github.com/containers/ocicrypt/gpg.go b/vendor/github.com/containers/ocicrypt/gpg.go
new file mode 100644
index 000000000..44cafae0c
--- /dev/null
+++ b/vendor/github.com/containers/ocicrypt/gpg.go
@@ -0,0 +1,425 @@
+/*
+ Copyright The ocicrypt 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 ocicrypt
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "regexp"
+ "strconv"
+ "strings"
+
+ 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.New("no GPGVault or GPGClient passed")
+ }
+ }
+ if !found && len(b64pgpPackets) > 0 && mustFindKey {
+ ids := uint64ToStringArray("0x%x", keyIds)
+
+ return nil, nil, errors.Errorf("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
+}