19 Commits
Author SHA1 Message Date
dm efbe79e786 tests: added NewKeyPair and ListKeys
Signed-off-by: Derek McQuay <derekmcquay@gmail.com>
2018-02-27 20:10:59 -08:00
dm c4d67105f2 add checkDir for keyring
ensureDir will create the dir, which for calls like List, Sign, Remove,
etc. should not occur. It should only perform the actions if something
exists there

Signed-off-by: Derek McQuay <derekmcquay@gmail.com>
2018-02-27 19:39:14 -08:00
sm e043384c67 Adds pm key remove <id> 2018-02-26 19:33:45 -08:00
sm 040b438cd1 Consistently align usage 2018-02-26 19:07:33 -08:00
sm 1f49d14e58 Adds dep files 2018-02-25 20:37:30 -08:00
sm 2a69f39e4b Adds pm keyring verify <file> <sig> 2018-02-25 20:10:18 -08:00
sm ff137c7036 Adds pm keyring sign 2018-02-25 20:10:15 -08:00
sm 72bbf22cbd Adds pm key import 2018-02-25 20:10:13 -08:00
sm 6f41544659 Adds pm key export 2018-02-25 20:10:10 -08:00
sm e18aee5b4f Validate keyring input
Linc helped me discover this bug by being into pushing buttons on my
kc71 and watching the colors change.
2018-02-25 20:10:10 -08:00
sm a00e058178 Adds pm key ls 2018-02-25 20:10:10 -08:00
sm 9f921135f7 Adds pm key create 2018-02-25 20:10:09 -08:00
sm b22d99bf53 Creates pgp dir if it doesn't exist 2018-02-25 20:10:07 -08:00
sm 4d8f3f0e79 Creates openpgp.Entity 2018-02-25 20:10:06 -08:00
sm d333acfdbd Stubs out passing args to keyring creation 2018-02-25 20:10:03 -08:00
sm ea383b7a34 parse name/email 2018-02-25 20:10:03 -08:00
sm 280ba234c8 Adds env subcommand 2018-02-25 20:10:00 -08:00
sm 5495f69b5a Stubs out pm key create subcommand 2018-02-25 20:09:58 -08:00
sm 20d0b556ea Stubs in keyring subcommand 2018-02-25 20:09:49 -08:00
6 changed files with 655 additions and 0 deletions
+1
View File
@@ -1 +1,2 @@
*.swp
vendor
Generated
+35
View File
@@ -0,0 +1,35 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"cast5",
"openpgp",
"openpgp/armor",
"openpgp/elgamal",
"openpgp/errors",
"openpgp/packet",
"openpgp/s2k"
]
revision = "49796115aa4b964c318aad4f3084fdb41e9aa067"
[[projects]]
name = "mcquay.me/fs"
packages = ["."]
revision = "d40468470ff3ca131b1eaf3d99ca05c2dd3bcdfc"
version = "v1.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "9bcd44fda9f03cef35ea82538ef76216e012e27acc1b32a8f5a684f814942428"
solver-name = "gps-cdcl"
solver-version = 1
+42
View File
@@ -0,0 +1,42 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "mcquay.me/fs"
version = "1.0.0"
[prune]
go-tests = true
unused-packages = true
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"bufio"
"fmt"
"os"
"mcquay.me/pm/keyring"
)
const usage = `pm: simple, cross-platform system package manager
subcommands:
environ (env) -- print environment information
keyring (key) -- interact with pm's OpenPGP keyring
`
const keyUsage = `pm keyring: interact with pm's OpenPGP keyring
subcommands:
create (c) -- create a fresh keypair
export (e) -- export a public key to stdout
import (i) -- import a public key from stdin
list (ls) -- list configured key info
remove (rm) -- remove a key from the keyring
sign (s) -- sign a file
verify (v) -- verify a detached signature
`
func main() {
if len(os.Args) < 2 {
fatalf("pm: missing subcommand\n\n%v", usage)
}
cmd := os.Args[1]
root := os.Getenv("PM_ROOT")
if root == "" {
root = "/usr/local"
}
signID := os.Getenv("PM_PGP_ID")
switch cmd {
case "env", "environ":
fmt.Printf("PM_ROOT=%q\n", root)
fmt.Printf("PM_PGP_ID=%q\n", signID)
case "key", "keyring":
if len(os.Args[1:]) < 2 {
fatalf("pm keyring: insufficient args\n\nusage: %v", keyUsage)
}
sub, args := os.Args[2], os.Args[3:]
switch sub {
case "ls", "list":
if err := keyring.ListKeys(root, os.Stdout); err != nil {
fatalf("listing keypair: %v\n", err)
}
case "c", "create":
var name, email string
s := bufio.NewScanner(os.Stdin)
fmt.Printf("name: ")
s.Scan()
if err := s.Err(); err != nil {
fatalf("reading name: %v\n", err)
}
name = s.Text()
fmt.Printf("email: ")
s.Scan()
if err := s.Err(); err != nil {
fatalf("reading email: %v\n", err)
}
email = s.Text()
if err := os.Stdin.Close(); err != nil {
fatalf("%v\n", err)
}
if err := keyring.NewKeyPair(root, name, email); err != nil {
fatalf("creating keypair: %v\n", err)
}
case "export", "e":
if len(args) != 1 {
fatalf("missing email argument\n")
}
email := args[0]
if err := keyring.Export(root, os.Stdout, email); err != nil {
fatalf("exporting public key for %q: %v\n", email, err)
}
case "sign", "s":
if signID == "" {
fatalf("must set PM_PGP_ID\n")
}
if err := keyring.Sign(root, signID, os.Stdin, os.Stdout); err != nil {
fatalf("signing: %v\n", err)
}
case "verify", "v":
if len(args) != 2 {
fatalf("usage: pm key verify <file> <sig>\n")
}
fn, sn := args[0], args[1]
ff, err := os.Open(fn)
if err != nil {
fatalf("opening %q: %v\n", fn, err)
}
defer ff.Close()
sf, err := os.Open(sn)
if err != nil {
fatalf("opening %q: %v\n", fn, err)
}
defer sf.Close()
if err := keyring.Verify(root, ff, sf); err != nil {
fatalf("detached sig verify: %v\n", err)
}
case "i", "import":
if err := keyring.Import(root, os.Stdin); err != nil {
fatalf("importing key: %v\n", err)
}
case "remove", "rm":
if len(args) != 1 {
fatalf("missing key id\n\nusage: pm key remove <id>\n")
}
id := args[0]
if err := keyring.Remove(root, id); err != nil {
fatalf("removing key for %q: %v\n", id, err)
}
default:
fatalf("unknown keyring subcommand: %q\n\nusage: %v", sub, keyUsage)
}
default:
fatalf("uknown subcommand %q\n\nusage: %v", cmd, usage)
}
}
func fatalf(f string, args ...interface{}) {
fmt.Fprintf(os.Stderr, f, args...)
os.Exit(1)
}
+350
View File
@@ -0,0 +1,350 @@
package keyring
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"mcquay.me/fs"
)
// NewKeyPair creates and adds a new OpenPGP keypair to an existing keyring.
func NewKeyPair(root, name, email string) error {
if name == "" {
return errors.New("name cannot be empty")
}
if email == "" {
return errors.New("email cannot be empty")
}
if strings.ContainsAny(email, "()<>\x00") {
return fmt.Errorf("email %q contains invalid chars", email)
}
if err := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create pgp dir")
}
srn, prn := getNames(root)
secs, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
fresh, err := openpgp.NewEntity(name, "pm", email, nil)
if err != nil {
errors.Wrap(err, "new entity")
}
pr, err := os.Create(prn)
if err != nil {
return errors.Wrap(err, "opening pubring")
}
sr, err := os.Create(srn)
if err != nil {
return errors.Wrap(err, "opening secring")
}
for _, e := range secs {
if err := e.SerializePrivate(sr, nil); err != nil {
return errors.Wrapf(err, "serializing old private key: %v", e.PrimaryKey.KeyIdString())
}
}
// order is critical here; if we don't serialize the private key of fresh
// first, the later steps fail.
if err := fresh.SerializePrivate(sr, nil); err != nil {
return errors.Wrapf(err, "serializing fresh private %v", fresh.PrimaryKey.KeyIdString())
}
if err := sr.Close(); err != nil {
return errors.Wrap(err, "closing secring")
}
for _, e := range pubs {
if err := e.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", e.PrimaryKey.KeyIdString())
}
}
if err := fresh.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", fresh.PrimaryKey.KeyIdString())
}
if err := pr.Close(); err != nil {
return errors.Wrap(err, "closing pubring")
}
return nil
}
// ListKeys prints keyring information to w.
func ListKeys(root string, w io.Writer) error {
if err := checkDir(root); err != nil {
return errors.Wrap(err, "can't find pgp dir")
}
srn, prn := getNames(root)
secs, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
for _, s := range secs {
names := []string{}
for _, v := range s.Identities {
names = append(names, v.Name)
}
fmt.Fprintf(w, "sec: %+v:\t%v\n", s.PrimaryKey.KeyIdShortString(), strings.Join(names, ","))
}
for _, p := range pubs {
names := []string{}
for _, v := range p.Identities {
names = append(names, v.Name)
}
fmt.Fprintf(w, "pub: %+v:\t%v\n", p.PrimaryKey.KeyIdShortString(), strings.Join(names, ","))
}
return nil
}
// Export prints pubkey information associated with email to w.
func Export(root string, w io.Writer, email string) error {
if err := checkDir(root); err != nil {
return errors.Wrap(err, "can't find pgp dir")
}
srn, prn := getNames(root)
_, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
e, err := findKey(pubs, email)
if err != nil {
return errors.Wrap(err, "find key")
}
aw, err := armor.Encode(w, openpgp.PublicKeyType, nil)
if err != nil {
return errors.Wrap(err, "creating armor encoder")
}
if err := e.Serialize(aw); err != nil {
return errors.Wrap(err, "serializing key")
}
if err := aw.Close(); err != nil {
return errors.Wrap(err, "closing armor encoder")
}
fmt.Fprintf(w, "\n")
return nil
}
// Import parses public key information from w and adds it to the public
// keyring.
func Import(root string, w io.Reader) error {
el, err := openpgp.ReadArmoredKeyRing(w)
if err != nil {
return errors.Wrap(err, "reading keyring")
}
if err := checkDir(root); err != nil {
return errors.Wrap(err, "can't find pgp dir")
}
srn, prn := getNames(root)
_, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
foreign := openpgp.EntityList{}
exist := map[uint64]bool{}
for _, p := range pubs {
exist[p.PrimaryKey.KeyId] = true
}
for _, e := range el {
if _, ok := exist[e.PrimaryKey.KeyId]; !ok {
foreign = append(foreign, e)
}
}
if len(foreign) < 1 {
return errors.New("no new key material found")
}
pubs = append(pubs, foreign...)
pr, err := os.Create(prn)
if err != nil {
return errors.Wrap(err, "opening pubring")
}
for _, e := range pubs {
if err := e.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", e.PrimaryKey.KeyIdString())
}
}
if err := pr.Close(); err != nil {
return errors.Wrap(err, "closing pubring")
}
return nil
}
// Sign takes an id and a reader and writes the signature for that id to sig.
func Sign(root, id string, in io.Reader, sig io.Writer) error {
if err := checkDir(root); err != nil {
return errors.Wrap(err, "can't find pgp dir")
}
srn, prn := getNames(root)
secs, _, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
e, err := findKey(secs, id)
if err != nil {
return errors.Wrapf(err, "finding key %q", id)
}
if err := openpgp.ArmoredDetachSign(sig, e, in, nil); err != nil {
return errors.Wrap(err, "armored detach sign")
}
fmt.Fprintf(sig, "\n")
return nil
}
// Verify verifies a file's deatched signature.
func Verify(root string, file, sig io.Reader) error {
if err := checkDir(root); err != nil {
return errors.Wrap(err, "can't find pgp dir")
}
srn, prn := getNames(root)
_, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
if _, err = openpgp.CheckArmoredDetachedSignature(pubs, file, sig); err != nil {
return errors.Wrap(err, "check sig")
}
return nil
}
// Remove removes public key information for a given id.
//
// It skips public keys that have matching secret keys, and does not effect
// private keys.
func Remove(root string, id string) error {
if err := checkDir(root); err != nil {
return errors.Wrap(err, "can't find pgp dir")
}
srn, prn := getNames(root)
secs, pubs, err := getELs(srn, prn)
if err != nil {
return errors.Wrap(err, "getting existing keyrings")
}
victim, err := findKey(pubs, id)
if err != nil {
return errors.Wrapf(err, "finding key %q", id)
}
pr, err := os.Create(prn)
if err != nil {
return errors.Wrap(err, "opening pubring")
}
var rerr error
for _, p := range pubs {
if victim.PrimaryKey.KeyId == p.PrimaryKey.KeyId {
if len(secs.KeysById(victim.PrimaryKey.KeyId)) == 0 {
continue
}
rerr = fmt.Errorf("skipping pubkey with matching privkey: %v", p.PrimaryKey.KeyIdShortString())
}
if err := p.Serialize(pr); err != nil {
return errors.Wrapf(err, "serializing %v", p.PrimaryKey.KeyIdString())
}
}
if err := pr.Close(); err != nil {
return errors.Wrap(err, "closing pubring")
}
return rerr
}
func pGPDir(root string) string {
return filepath.Join(root, "var", "lib", "pm", "pgp")
}
func checkDir(root string) error {
d := pGPDir(root)
if !fs.Exists(d) {
return fmt.Errorf("pgp dir does not exist")
}
return nil
}
func ensureDir(root string) error {
d := pGPDir(root)
if !fs.Exists(d) {
if err := os.MkdirAll(d, 0700); err != nil {
return errors.Wrap(err, "mk pgp dir")
}
}
return nil
}
func getNames(root string) (string, string) {
srn := filepath.Join(pGPDir(root), "secring.gpg")
prn := filepath.Join(pGPDir(root), "pubring.gpg")
return srn, prn
}
func getELs(secring, pubring string) (openpgp.EntityList, openpgp.EntityList, error) {
var sr, pr openpgp.EntityList
if fs.Exists(secring) {
f, err := os.Open(secring)
if err != nil {
return nil, nil, errors.Wrap(err, "opening secring")
}
sr, err = openpgp.ReadKeyRing(f)
if err != nil {
return nil, nil, errors.Wrap(err, "read sec key ring")
}
if err := f.Close(); err != nil {
return nil, nil, errors.Wrap(err, "closing keyring")
}
}
if fs.Exists(pubring) {
f, err := os.Open(pubring)
if err != nil {
return nil, nil, errors.Wrap(err, "opening pubring")
}
pr, err = openpgp.ReadKeyRing(f)
if err != nil {
return nil, nil, errors.Wrap(err, "read pub key ring")
}
if err := f.Close(); err != nil {
return nil, nil, errors.Wrap(err, "closing keyring")
}
}
return sr, pr, nil
}
func findKey(el openpgp.EntityList, id string) (*openpgp.Entity, error) {
var e *openpgp.Entity
if strings.Contains(id, "@") {
es := openpgp.EntityList{}
for _, p := range el {
for _, v := range p.Identities {
if id == v.UserId.Email {
es = append(es, p)
}
}
}
if len(es) == 1 {
return es[0], nil
}
if len(es) > 1 {
return nil, errors.New("too many keys matched; try searching by key id?")
}
} else {
for _, p := range el {
if id == p.PrimaryKey.KeyIdShortString() {
return p, nil
}
}
}
return e, fmt.Errorf("key %q not found", id)
}
+90
View File
@@ -0,0 +1,90 @@
package keyring
import (
"bytes"
"io/ioutil"
"os"
"regexp"
"testing"
)
const (
ExpectedListKeysRegex = "sec: [[:alnum:]]{8}:[[:space:]]*foo[[:space:]]*\\(pm\\)[[:space:]]*<bar>\npub: [[:alnum:]]{8}:[[:space:]]*foo[[:space:]]*\\(pm\\)[[:space:]]*<bar>"
)
func TestNewKeyPair(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Couldn't create tmpdir")
}
defer os.RemoveAll(tmpdir)
var newKeyPair = []struct {
r, n, e string
expected bool
}{
{"", "", "", false},
{"", "foo", "", false},
{"", "foo", "bad<>email", false},
{tmpdir, "foo", "bar", true},
}
for _, rt := range newKeyPair {
actual := NewKeyPair(rt.r, rt.n, rt.e)
if (actual == nil) != rt.expected {
t.Errorf(
"failed NewKeyPair with an error: %v\n\troot: %s\n\tname: %s\n\temail:%s\n\texpected: %t\n\t actual: %t",
actual,
rt.r,
rt.n,
rt.e,
(actual == nil),
rt.expected,
)
}
}
}
func TestListKeys(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("Couldn't create tmpdir")
}
defer os.RemoveAll(tmpdir)
err = NewKeyPair(tmpdir, "foo", "bar")
if err != nil {
t.Fatalf("Couldn't create New Key Pair")
}
var newKeyPair = []struct {
r string
regex string
w *bytes.Buffer
expected bool
}{
{"", "", bytes.NewBuffer(nil), false},
{tmpdir, ExpectedListKeysRegex, bytes.NewBuffer(nil), true},
}
for _, rt := range newKeyPair {
actual := ListKeys(rt.r, rt.w)
if (actual == nil) != rt.expected {
t.Errorf(
"failed ListKeys with an error: %v\n\troot: %s\n\texpected: %t\n\t actual: %t",
actual,
rt.r,
(actual == nil),
rt.expected,
)
}
if rt.expected {
matched, err := regexp.MatchString(rt.regex, rt.w.String())
if err != nil {
t.Fatalf("error%v, ", err)
}
if !matched {
t.Errorf("did not match")
t.Errorf("ListKeys did not match expected regex; wanted: [%q], got: [%s]", rt.regex, rt.w.String())
}
}
}
}