From 0de2881a1600960c166d07f8de943831e214cf3d Mon Sep 17 00:00:00 2001 From: mpl Date: Fri, 21 Jun 2013 16:10:40 +0200 Subject: [PATCH] client: help (wrt to the gpg key) initialize the config Change-Id: I983e4396abacbc4d8fc354863cffeece65dd5b90 --- cmd/camput/init.go | 60 +++++++++++++++++++++++-------- pkg/client/config.go | 46 +++++++++++++++++------- pkg/jsonsign/keys.go | 43 ++++++++++++++++++++++ pkg/osutil/paths.go | 6 +++- server/camlistored/camlistored.go | 40 ++------------------- 5 files changed, 129 insertions(+), 66 deletions(-) diff --git a/cmd/camput/init.go b/cmd/camput/init.go index 37a682b20..58996f8ce 100644 --- a/cmd/camput/init.go +++ b/cmd/camput/init.go @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO(mpl): Shouldn't we move this subcommand to camtool? e.g as 'camtool configinit' ? +// It does not feel like a good fit in camput since this does not send anything +// to a server. + package main import ( @@ -28,26 +32,27 @@ import ( "path" "camlistore.org/pkg/blobref" - "camlistore.org/pkg/client" "camlistore.org/pkg/cmdmain" "camlistore.org/pkg/jsonsign" "camlistore.org/pkg/osutil" ) type initCmd struct { + newKey bool gpgkey string } func init() { cmdmain.RegisterCommand("init", func(flags *flag.FlagSet) cmdmain.CommandRunner { cmd := new(initCmd) + flags.BoolVar(&cmd.newKey, "newkey", false, "Automatically generate a new identity in a new secret ring.") flags.StringVar(&cmd.gpgkey, "gpgkey", "", "GPG key to use for signing (overrides $GPGKEY environment)") return cmd }) } func (c *initCmd) Describe() string { - return "Initialize the camput configuration file." + return "Initialize the camput configuration file. With no option, it tries to use the GPG key found in the default identity secret ring." } func (c *initCmd) Usage() { @@ -58,10 +63,14 @@ func (c *initCmd) Examples() []string { return []string{ "", "--gpgkey=XXXXX", + "--newkey Creates a new identity", } } -func (c *initCmd) keyId() (string, error) { +// keyId returns the current keyId. It checks, in this order, +// the --gpgkey flag, the GPGKEY env var, and the default +// identity secret ring. +func (c *initCmd) keyId(secRing string) (string, error) { if k := c.gpgkey; k != "" { return k, nil } @@ -69,13 +78,19 @@ func (c *initCmd) keyId() (string, error) { return k, nil } - // TODO: move camlistored.go's keyIdFromRing into - // pkg/jsonsign/keys.go and use that (which looks for an - // identify file with exactly one identity) + k, err := jsonsign.KeyIdFromRing(secRing) + if err != nil { + log.Printf("No suitable gpg key was found in %v: %v", secRing, err) + } else { + if k != "" { + log.Printf("Re-using identity with keyId %q found in file %s", k, secRing) + return k, nil + } + } // TODO: run and parse gpg --list-secret-keys and see if there's just one and suggest that? Or show // a list of them? - return "", errors.New("Initialization requires your public GPG key. Set --gpgkey= or set $GPGKEY in your environment. Run gpg --list-secret-keys to find their key IDs.") + return "", errors.New("Initialization requires your public GPG key.\nYou can set --gpgkey= or set $GPGKEY in your environment. Run gpg --list-secret-keys to find their key IDs.\nOr you can create a new secret ring and key with 'camput init --newkey'.") } func (c *initCmd) getPublicKeyArmoredFromFile(secretRingFileName, keyId string) (b []byte, err error) { @@ -112,13 +127,27 @@ func (c *initCmd) RunCommand(args []string) error { return cmdmain.ErrUsage } + if c.newKey && c.gpgkey != "" { + log.Fatal("--newkey and --gpgkey are mutually exclusive") + } + blobDir := path.Join(osutil.CamliConfigDir(), "keyblobs") os.Mkdir(osutil.CamliConfigDir(), 0700) os.Mkdir(blobDir, 0700) - keyId, err := c.keyId() - if err != nil { - return err + var keyId string + var err error + secRing := osutil.IdentitySecretRing() + if c.newKey { + keyId, err = jsonsign.GenerateNewSecRing(secRing) + if err != nil { + return err + } + } else { + keyId, err = c.keyId(secRing) + if err != nil { + return err + } } if os.Getenv("GPG_AGENT_INFO") == "" { @@ -143,12 +172,13 @@ func (c *initCmd) RunCommand(args []string) error { log.Printf("Your Camlistore identity (your GPG public key's blobref) is: %s", bref.String()) - _, err = os.Stat(client.ConfigFilePath()) + configFilePath := osutil.UserClientConfigPath() + _, err = os.Stat(configFilePath) if err == nil { - log.Fatalf("Config file %q already exists; quitting without touching it.", client.ConfigFilePath()) + log.Fatalf("Config file %q already exists; quitting without touching it.", configFilePath) } - if f, err := os.OpenFile(client.ConfigFilePath(), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600); err == nil { + if f, err := os.OpenFile(configFilePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600); err == nil { defer f.Close() m := make(map[string]interface{}) m["keyId"] = keyId // TODO(bradfitz): make this 'identity' to match server config? @@ -163,9 +193,9 @@ func (c *initCmd) RunCommand(args []string) error { } _, err = f.Write(jsonBytes) if err != nil { - log.Fatalf("Error writing to %q: %v", client.ConfigFilePath(), err) + log.Fatalf("Error writing to %q: %v", configFilePath, err) } - log.Printf("Wrote %q; modify as necessary.", client.ConfigFilePath()) + log.Printf("Wrote %q; modify as necessary.", configFilePath) } return nil } diff --git a/pkg/client/config.go b/pkg/client/config.go index bc594c101..439c3d7ff 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -18,6 +18,7 @@ package client import ( "flag" + "fmt" "io/ioutil" "log" "os" @@ -42,7 +43,7 @@ var ( ) func AddFlags() { - defaultPath := ConfigFilePath() + defaultPath := osutil.UserClientConfigPath() flag.StringVar(&flagServer, "server", "", "Camlistore server prefix. If blank, the default from the \"server\" field of "+defaultPath+" is used. Acceptable forms: https://you.example.com, example.com:1345 (https assumed), or http://you.example.com/alt-root") flag.StringVar(&flagSecretRing, "secret-keyring", "", "GnuPG secret keyring file to use.") } @@ -52,19 +53,40 @@ func ExplicitServer() string { return flagServer } -func ConfigFilePath() string { - return filepath.Join(osutil.CamliConfigDir(), "config") -} - var configOnce sync.Once var config = make(map[string]interface{}) -var parseConfigErr error + +// serverGPGKey returns the public gpg key id ("identity" field) +// from the user's server config , if any. +// It returns the empty string otherwise. +func serverKeyId() string { + serverConfigFile := osutil.UserServerConfigPath() + if _, err := os.Stat(serverConfigFile); err != nil { + if os.IsNotExist(err) { + return "" + } + log.Fatalf("Could not stat %v: %v", serverConfigFile, err) + } + obj, err := jsonconfig.ReadFile(serverConfigFile) + if err != nil { + return "" + } + keyId, ok := obj["identity"].(string) + if !ok { + return "" + } + return keyId +} func parseConfig() { - configPath := ConfigFilePath() + configPath := osutil.UserClientConfigPath() if _, err := os.Stat(configPath); os.IsNotExist(err) { - parseConfigErr = os.ErrNotExist - return + errMsg := fmt.Sprintf("Client configuration file %v does not exist. See 'camput init' to generate it.", configPath) + if keyId := serverKeyId(); keyId != "" { + hint := fmt.Sprintf("\nThe key id %v was found in the server config %v, so you might want:\n'camput init -gpgkey %v'", keyId, osutil.UserServerConfigPath(), keyId) + errMsg += hint + } + log.Fatal(errMsg) } var err error @@ -98,7 +120,7 @@ func serverOrDie() string { } server = cleanServer(server) if !ok || server == "" { - log.Fatalf("Missing or invalid \"server\" in %q", ConfigFilePath()) + log.Fatalf("Missing or invalid \"server\" in %q", osutil.UserClientConfigPath()) } return server } @@ -181,7 +203,7 @@ func getSignerPublicKeyBlobref() *blobref.BlobRef { key := "keyId" keyId, ok := config[key].(string) if !ok { - log.Printf("No key %q in JSON configuration file %q; have you run \"camput init\"?", key, ConfigFilePath()) + log.Printf("No key %q in JSON configuration file %q; have you run \"camput init\"?", key, osutil.UserClientConfigPath()) return nil } keyRing, hasKeyRing := config["secretRing"].(string) @@ -208,7 +230,7 @@ func getSignerPublicKeyBlobref() *blobref.BlobRef { selfPubKeyDir, ok := config["selfPubKeyDir"].(string) if !ok { - log.Printf("No 'selfPubKeyDir' defined in %q", ConfigFilePath()) + log.Printf("No 'selfPubKeyDir' defined in %q", osutil.UserClientConfigPath()) return nil } fi, err := os.Stat(selfPubKeyDir) diff --git a/pkg/jsonsign/keys.go b/pkg/jsonsign/keys.go index a32cdfc01..ecf36e7a1 100644 --- a/pkg/jsonsign/keys.go +++ b/pkg/jsonsign/keys.go @@ -152,3 +152,46 @@ func WriteKeyRing(w io.Writer, el openpgp.EntityList) error { } return nil } + +// KeyIdFromRing returns the public keyId contained in the secret +// ring file secRing. It expects only one keyId in this secret ring +// and returns an error otherwise. +func KeyIdFromRing(secRing string) (keyId string, err error) { + f, err := os.Open(secRing) + if err != nil { + return "", fmt.Errorf("Could not open secret ring file %v: %v", secRing, err) + } + defer f.Close() + el, err := openpgp.ReadKeyRing(f) + if err != nil { + return "", fmt.Errorf("Could not read secret ring file %s: %v", secRing, err) + } + if len(el) != 1 { + return "", fmt.Errorf("Secret ring file %v contained %d identities; expected 1", secRing, len(el)) + } + ent := el[0] + return ent.PrimaryKey.KeyIdShortString(), nil +} + +// GenerateNewSecRing creates a new secret ring file secRing, with +// a new GPG identity. It returns the public keyId of that identity. +// It returns an error if the file already exists. +func GenerateNewSecRing(secRing string) (keyId string, err error) { + ent, err := NewEntity() + if err != nil { + return "", fmt.Errorf("generating new identity: %v", err) + } + f, err := os.OpenFile(secRing, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", err + } + err = WriteKeyRing(f, openpgp.EntityList([]*openpgp.Entity{ent})) + if err != nil { + f.Close() + return "", fmt.Errorf("Could not write new key ring to %s: %v", secRing, err) + } + if err := f.Close(); err != nil { + return "", fmt.Errorf("Could not close %v: %v", secRing, err) + } + return ent.PrimaryKey.KeyIdShortString(), nil +} diff --git a/pkg/osutil/paths.go b/pkg/osutil/paths.go index 656704313..247669ebf 100644 --- a/pkg/osutil/paths.go +++ b/pkg/osutil/paths.go @@ -83,11 +83,15 @@ func UserServerConfigPath() string { return filepath.Join(CamliConfigDir(), "server-config.json") } +func UserClientConfigPath() string { + return filepath.Join(CamliConfigDir(), "config") +} + func IdentitySecretRing() string { return filepath.Join(CamliConfigDir(), "identity-secring.gpg") } -// Find the correct absolute path corresponding to a relative path, +// Find the correct absolute path corresponding to a relative path, // searching the following sequence of directories: // 1. Working Directory // 2. CAMLI_CONFIG_DIR (deprecated, will complain if this is on env) diff --git a/server/camlistored/camlistored.go b/server/camlistored/camlistored.go index 8db12bbfd..c0fa5991c 100644 --- a/server/camlistored/camlistored.go +++ b/server/camlistored/camlistored.go @@ -63,8 +63,6 @@ import ( // Handlers: _ "camlistore.org/pkg/search" _ "camlistore.org/pkg/server" // UI, publish, etc - - "camlistore.org/third_party/code.google.com/p/go.crypto/openpgp" ) const ( @@ -198,40 +196,6 @@ func findConfigFile(file string) (absPath string, err error) { return } -func keyIdFromRing(filename string) (keyId string, err error) { - f, err := os.Open(filename) - if err != nil { - return "", fmt.Errorf("reading identity secret ring file: %v", err) - } - defer f.Close() - el, err := openpgp.ReadKeyRing(f) - if err != nil { - return "", fmt.Errorf("reading identity secret ring file %s: %v", filename, err) - } - if len(el) != 1 { - return "", fmt.Errorf("identity secret ring file contained %d identities; expected 1", len(el)) - } - ent := el[0] - return ent.PrimaryKey.KeyIdShortString(), nil -} - -func generateNewSecRing(filename string) (keyId string, err error) { - ent, err := jsonsign.NewEntity() - if err != nil { - return "", fmt.Errorf("generating new identity: %v", err) - } - f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) - if err != nil { - return "", err - } - defer f.Close() - err = jsonsign.WriteKeyRing(f, openpgp.EntityList([]*openpgp.Entity{ent})) - if err != nil { - return "", fmt.Errorf("writing new key ring to %s: %v", filename, err) - } - return ent.PrimaryKey.KeyIdShortString(), nil -} - type defaultConfigFile struct { Listen string `json:"listen"` HTTPS bool `json:"https"` @@ -266,10 +230,10 @@ func newDefaultConfigFile(path string) error { _, err := os.Stat(secRing) switch { case err == nil: - keyId, err = keyIdFromRing(secRing) + keyId, err = jsonsign.KeyIdFromRing(secRing) log.Printf("Re-using identity with keyId %q found in file %s", keyId, secRing) case os.IsNotExist(err): - keyId, err = generateNewSecRing(secRing) + keyId, err = jsonsign.GenerateNewSecRing(secRing) log.Printf("Generated new identity with keyId %q in file %s", keyId, secRing) } if err != nil {