Merge "client: help (wrt to the gpg key) initialize the config"

This commit is contained in:
Brad Fitzpatrick 2013-06-22 01:48:31 +00:00 committed by Gerrit Code Review
commit f557a7235b
5 changed files with 129 additions and 66 deletions

View File

@ -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=<pubid> 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=<pubid> 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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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 {