diff --git a/mgmtmain/cli.go b/mgmtmain/cli.go index eb90b462..351c26be 100644 --- a/mgmtmain/cli.go +++ b/mgmtmain/cli.go @@ -96,6 +96,16 @@ func run(c *cli.Context) error { obj.NoCaching = c.Bool("no-caching") obj.Depth = uint16(c.Int("depth")) + obj.NoPgp = c.Bool("no-pgp") + + if kp := c.String("pgp-key-path"); c.IsSet("pgp-key-path") { + obj.PgpKeyPath = &kp + } + + if us := c.String("pgp-identity"); c.IsSet("pgp-identity") { + obj.PgpIdentity = &us + } + if err := obj.Init(); err != nil { return err } @@ -288,6 +298,20 @@ func CLI(program, version string) error { Value: 0, Usage: "specify depth in remote hierarchy", }, + cli.BoolFlag{ + Name: "no-pgp", + Usage: "don't create pgp keys", + }, + cli.StringFlag{ + Name: "pgp-key-path", + Value: "", + Usage: "path for instance key pair", + }, + cli.StringFlag{ + Name: "pgp-identity", + Value: "", + Usage: "default identity used for generation", + }, }, }, } diff --git a/mgmtmain/main.go b/mgmtmain/main.go index 5a497db3..c536196b 100644 --- a/mgmtmain/main.go +++ b/mgmtmain/main.go @@ -29,6 +29,7 @@ import ( "github.com/purpleidea/mgmt/converger" "github.com/purpleidea/mgmt/etcd" "github.com/purpleidea/mgmt/gapi" + "github.com/purpleidea/mgmt/pgp" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/remote" @@ -82,6 +83,11 @@ type Main struct { serverURLs etcdtypes.URLs // processed server urls value idealClusterSize uint16 // processed ideal cluster size value + NoPgp bool // disallow pgp functionality + PgpKeyPath *string // import a pre-made key pair + PgpIdentity *string + pgpKeys *pgp.PGP // agent key pair + exit chan error // exit signal } @@ -212,6 +218,49 @@ func (obj *Main) Run() error { return errwrap.Wrapf(err, "Can't create pgraph prefix") } + if !obj.NoPgp { + pgpPrefix := fmt.Sprintf("%s/", path.Join(prefix, "pgp")) + if err := os.MkdirAll(pgpPrefix, 0770); err != nil { + return errwrap.Wrapf(err, "Can't create pgp prefix") + } + + pgpKeyringPath := path.Join(pgpPrefix, pgp.DefaultKeyringFile) // default path + + if p := obj.PgpKeyPath; p != nil { + pgpKeyringPath = *p + } + + var err error + if obj.pgpKeys, err = pgp.Import(pgpKeyringPath); err != nil && !os.IsNotExist(err) { + return errwrap.Wrapf(err, "Can't import pgp key") + } + + if obj.pgpKeys == nil { + + identity := fmt.Sprintf("%s <%s> %s", obj.Program, "root@"+hostname, "generated by "+obj.Program) + if p := obj.PgpIdentity; p != nil { + identity = *p + } + + name, comment, email, err := pgp.ParseIdentity(identity) + if err != nil { + return errwrap.Wrapf(err, "Can't parse user string") + + } + + // TODO: Make hash configurable + if obj.pgpKeys, err = pgp.Generate(name, comment, email, nil); err != nil { + return errwrap.Wrapf(err, "Can't creating pgp key") + } + + if err := obj.pgpKeys.SaveKey(pgpKeyringPath); err != nil { + return errwrap.Wrapf(err, "Can't save pgp key") + } + } + + // TODO: Import admin key + } + var wg sync.WaitGroup var G, oldGraph *pgraph.Graph diff --git a/pgp/pgp.go b/pgp/pgp.go new file mode 100644 index 00000000..d8e3b086 --- /dev/null +++ b/pgp/pgp.go @@ -0,0 +1,230 @@ +// Mgmt +// Copyright (C) 2013-2016+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package pgp + +import ( + "bufio" + "bytes" + "crypto" + "encoding/base64" + "io/ioutil" + "log" + "os" + "strings" + + errwrap "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +// DefaultKeyringFile is the default file name for keyrings. +const DefaultKeyringFile = "keyring.pgp" + +// CONFIG set default Hash. +var CONFIG packet.Config + +func init() { + CONFIG.DefaultHash = crypto.SHA256 +} + +// PGP contains base entity. +type PGP struct { + Entity *openpgp.Entity +} + +// Import private key from defined path. +func Import(privKeyPath string) (*PGP, error) { + + privKeyFile, err := os.Open(privKeyPath) + if err != nil { + return nil, err + } + defer privKeyFile.Close() + + file := packet.NewReader(bufio.NewReader(privKeyFile)) + entity, err := openpgp.ReadEntity(file) + if err != nil { + return nil, errwrap.Wrapf(err, "can't read entity from path") + } + + obj := &PGP{ + Entity: entity, + } + + log.Printf("PGP: Imported key: %s", obj.Entity.PrivateKey.KeyIdShortString()) + return obj, nil +} + +// Generate creates new key pair. This key pair must be saved or it will be lost. +func Generate(name, comment, email string, hash *crypto.Hash) (*PGP, error) { + if hash != nil { + CONFIG.DefaultHash = *hash + } + // generate a new public/private key pair + entity, err := openpgp.NewEntity(name, comment, email, &CONFIG) + if err != nil { + return nil, errwrap.Wrapf(err, "can't generate entity") + } + + obj := &PGP{ + Entity: entity, + } + + log.Printf("PGP: Created key: %s", obj.Entity.PrivateKey.KeyIdShortString()) + return obj, nil +} + +// SaveKey writes the whole entity (including private key!) to a .gpg file. +func (obj *PGP) SaveKey(path string) error { + f, err := os.Create(path) + if err != nil { + return errwrap.Wrapf(err, "can't create file from given path") + } + + w := bufio.NewWriter(f) + if err != nil { + return errwrap.Wrapf(err, "can't create writer") + } + + if err := obj.Entity.SerializePrivate(w, &CONFIG); err != nil { + return errwrap.Wrapf(err, "can't serialize private key") + } + + for _, ident := range obj.Entity.Identities { + for _, sig := range ident.Signatures { + if err := sig.Serialize(w); err != nil { + return errwrap.Wrapf(err, "can't serialize signature") + } + } + } + + if err := w.Flush(); err != nil { + return errwrap.Wrapf(err, "enable to flush writer") + } + + return nil +} + +// WriteFile from given buffer in specified path. +func (obj *PGP) WriteFile(path string, buff *bytes.Buffer) error { + w, err := createWriter(path) + if err != nil { + return errwrap.Wrapf(err, "can't create writer") + } + buff.WriteTo(w) + + if err := w.Flush(); err != nil { + return errwrap.Wrapf(err, "can't flush buffered data") + } + return nil +} + +// CreateWriter remove duplicate function. +func createWriter(path string) (*bufio.Writer, error) { + f, err := os.Create(path) + if err != nil { + return nil, errwrap.Wrapf(err, "can't create file from given path") + } + return bufio.NewWriter(f), nil +} + +// Encrypt message for specified entity. +func (obj *PGP) Encrypt(to *openpgp.Entity, msg string) (string, error) { + buf, err := obj.EncryptMsg(to, msg) + if err != nil { + return "", errwrap.Wrapf(err, "can't encrypt message") + } + + // encode to base64 + bytes, err := ioutil.ReadAll(buf) + if err != nil { + return "", errwrap.Wrapf(err, "can't read unverified body") + } + return base64.StdEncoding.EncodeToString(bytes), nil +} + +// EncryptMsg encrypts the message. +func (obj *PGP) EncryptMsg(to *openpgp.Entity, msg string) (*bytes.Buffer, error) { + ents := []*openpgp.Entity{to} + + buf := new(bytes.Buffer) + w, err := openpgp.Encrypt(buf, ents, obj.Entity, nil, nil) + if err != nil { + return nil, errwrap.Wrapf(err, "can't encrypt message") + } + + _, err = w.Write([]byte(msg)) + if err != nil { + return nil, errwrap.Wrapf(err, "can't write to buffer") + } + + if err = w.Close(); err != nil { + return nil, errwrap.Wrapf(err, "can't close writer") + } + return buf, nil +} + +// Decrypt an encrypted msg. +func (obj *PGP) Decrypt(encString string) (string, error) { + entityList := openpgp.EntityList{obj.Entity} + + // decode the base64 string + dec, err := base64.StdEncoding.DecodeString(encString) + if err != nil { + return "", errwrap.Wrapf(err, "fail at decoding encrypted string") + } + + // decrypt it with the contents of the private key + md, err := openpgp.ReadMessage(bytes.NewBuffer(dec), entityList, nil, nil) + if err != nil { + return "", errwrap.Wrapf(err, "can't read message") + } + + bytes, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + return "", errwrap.Wrapf(err, "can't read unverified body") + } + return string(bytes), nil +} + +// GetIdentities return the first identities from current object. +func (obj *PGP) GetIdentities() (string, error) { + identities := []*openpgp.Identity{} + + for _, v := range obj.Entity.Identities { + identities = append(identities, v) + } + return identities[0].Name, nil +} + +// ParseIdentity parses an identity into name, comment and email components. +func ParseIdentity(identity string) (name, comment, email string, err error) { + // get name + n := strings.Split(identity, " <") + if len(n) != 2 { + return "", "", "", errwrap.Wrap(err, "user string mal formated") + } + + // get email and comment + ec := strings.Split(n[1], "> ") + if len(ec) != 2 { + return "", "", "", errwrap.Wrap(err, "user string mal formated") + } + + return n[0], ec[1], ec[0], nil +}