diff --git a/cmd/helm/package.go b/cmd/helm/package.go index d99309c4a..f9cf4cdbc 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -23,8 +23,10 @@ import ( "io/ioutil" "os" "path/filepath" + "syscall" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/chartutil" @@ -145,6 +147,10 @@ func (p *packageCmd) clearsign(filename string) error { return err } + if err := signer.DecryptKey(promptUser); err != nil { + return err + } + sig, err := signer.ClearSign(filename) if err != nil { return err @@ -156,3 +162,11 @@ func (p *packageCmd) clearsign(filename string) error { return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) } + +// promptUser implements provenance.PassphraseFetcher +func promptUser(name string) ([]byte, error) { + fmt.Printf("Password for key %q > ", name) + pw, err := terminal.ReadPassword(int(syscall.Stdin)) + fmt.Println() + return []byte(pw), err +} diff --git a/docs/provenance.md b/docs/provenance.md index e427c533a..6a6faac36 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -27,11 +27,14 @@ This section describes a potential workflow for using provenance data effectivel Prerequisites: -- A valid, passphrase-less PGP keypair in a binary (not ASCII-armored) format +- A valid PGP keypair in a binary (not ASCII-armored) format - The `helm` command line tool - GnuPG command line tools (optional) - Keybase command line tools (optional) +**NOTE:** If your PGP private key has a passphrase, you will be prompted to enter +that passphrase for any commands that support the `--sign` option. + Creating a new chart is the same as before: ``` diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 90b14cdc5..480932648 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -144,10 +144,55 @@ func NewFromKeyring(keyringfile, id string) (*Signatory, error) { if vague { return s, fmt.Errorf("more than one key contain the id %q", id) } + s.Entity = candidate return s, nil } +// PassphraseFetcher returns a passphrase for decrypting keys. +// +// This is used as a callback to read a passphrase from some other location. The +// given name is the Name field on the key, typically of the form: +// +// USER_NAME (COMMENT) +type PassphraseFetcher func(name string) ([]byte, error) + +// DecryptKey decrypts a private key in the Signatory. +// +// If the key is not encrypted, this will return without error. +// +// If the key does not exist, this will return an error. +// +// If the key exists, but cannot be unlocked with the passphrase returned by +// the PassphraseFetcher, this will return an error. +// +// If the key is successfully unlocked, it will return nil. +func (s *Signatory) DecryptKey(fn PassphraseFetcher) error { + if s.Entity == nil || s.Entity.PrivateKey == nil { + return errors.New("private key not found") + } + + // Nothing else to do if key is not encrypted. + if !s.Entity.PrivateKey.Encrypted { + return nil + } + + fname := "Unknown" + for i := range s.Entity.Identities { + if i != "" { + fname = i + break + } + } + + p, err := fn(fname) + if err != nil { + return err + } + + return s.Entity.PrivateKey.Decrypt(p) +} + // ClearSign signs a chart with the given key. // // This takes the path to a chart archive file and a key, and it returns a clear signature. diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 747a9376a..388941deb 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -32,6 +32,9 @@ const ( // phrase. Use `gpg --export-secret-keys helm-test` to export the secret. testKeyfile = "testdata/helm-test-key.secret" + // testPasswordKeyFile is a keyfile with a password. + testPasswordKeyfile = "testdata/helm-password-key.secret" + // testPubfile is the public key file. // Use `gpg --export helm-test` to export the public key. testPubfile = "testdata/helm-test-key.pub" @@ -39,6 +42,8 @@ const ( // Generated name for the PGP key in testKeyFile. testKeyName = `Helm Testing (This key should only be used for testing. DO NOT TRUST.) ` + testPasswordKeyName = `password key (fake) ` + testChartfile = "testdata/hashtest-1.2.3.tgz" // testSigBlock points to a signature generated by an external tool. @@ -177,6 +182,36 @@ func TestDigestFile(t *testing.T) { } } +func TestDecryptKey(t *testing.T) { + k, err := NewFromKeyring(testPasswordKeyfile, testPasswordKeyName) + if err != nil { + t.Fatal(err) + } + + if !k.Entity.PrivateKey.Encrypted { + t.Fatal("Key is not encrypted") + } + + // We give this a simple callback that returns the password. + if err := k.DecryptKey(func(s string) ([]byte, error) { + return []byte("secret"), nil + }); err != nil { + t.Fatal(err) + } + + // Re-read the key (since we already unlocked it) + k, err = NewFromKeyring(testPasswordKeyfile, testPasswordKeyName) + if err != nil { + t.Fatal(err) + } + // Now we give it a bogus password. + if err := k.DecryptKey(func(s string) ([]byte, error) { + return []byte("secrets_and_lies"), nil + }); err == nil { + t.Fatal("Expected an error when giving a bogus passphrase") + } +} + func TestClearSign(t *testing.T) { signer, err := NewFromFiles(testKeyfile, testPubfile) if err != nil { diff --git a/pkg/provenance/testdata/helm-password-key.secret b/pkg/provenance/testdata/helm-password-key.secret new file mode 100644 index 000000000..03c8aa583 Binary files /dev/null and b/pkg/provenance/testdata/helm-password-key.secret differ