Compare commits

...

20 Commits
master ... pkg

Author SHA1 Message Date
Stephen McQuay 17cfd72173
Stub out fetching secret key entity 2018-02-26 20:07:47 -08:00
Stephen McQuay 85e33bbe90
Simple pre-flight checks
More of this to come ...
2018-02-26 20:02:23 -08:00
Stephen McQuay 2ec42dd61c
Stub out pm package create 2018-02-26 19:58:28 -08:00
Stephen McQuay e043384c67
Adds pm key remove <id> 2018-02-26 19:33:45 -08:00
Stephen McQuay 040b438cd1
Consistently align usage 2018-02-26 19:07:33 -08:00
Stephen McQuay 1f49d14e58
Adds dep files 2018-02-25 20:37:30 -08:00
Stephen McQuay 2a69f39e4b
Adds pm keyring verify <file> <sig> 2018-02-25 20:10:18 -08:00
Stephen McQuay ff137c7036
Adds pm keyring sign 2018-02-25 20:10:15 -08:00
Stephen McQuay 72bbf22cbd
Adds pm key import 2018-02-25 20:10:13 -08:00
Stephen McQuay 6f41544659
Adds pm key export 2018-02-25 20:10:10 -08:00
Stephen McQuay 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
Stephen McQuay a00e058178
Adds pm key ls 2018-02-25 20:10:10 -08:00
Stephen McQuay 9f921135f7
Adds pm key create 2018-02-25 20:10:09 -08:00
Stephen McQuay b22d99bf53
Creates pgp dir if it doesn't exist 2018-02-25 20:10:07 -08:00
Stephen McQuay 4d8f3f0e79
Creates openpgp.Entity 2018-02-25 20:10:06 -08:00
Stephen McQuay d333acfdbd
Stubs out passing args to keyring creation 2018-02-25 20:10:03 -08:00
Stephen McQuay ea383b7a34
parse name/email 2018-02-25 20:10:03 -08:00
Stephen McQuay 280ba234c8
Adds env subcommand 2018-02-25 20:10:00 -08:00
Stephen McQuay 5495f69b5a
Stubs out pm key create subcommand 2018-02-25 20:09:58 -08:00
Stephen McQuay 20d0b556ea
Stubs in keyring subcommand 2018-02-25 20:09:49 -08:00
6 changed files with 617 additions and 0 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.swp
vendor

35
Gopkg.lock generated Normal file
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
Gopkg.toml Normal file
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

166
cmd/pm/main.go Normal file
View File

@ -0,0 +1,166 @@
package main
import (
"bufio"
"fmt"
"os"
"mcquay.me/pm/keyring"
"mcquay.me/pm/pkg"
)
const usage = `pm: simple, cross-platform system package manager
subcommands:
environ (env) -- print environment information
keyring (key) -- interact with pm's OpenPGP keyring
package (pkg) -- create packages
`
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
`
const pkgUsage = `pm package: generate pm-compatible packages
subcommands:
create (c) -- create a fresh keypair
`
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)
}
case "package", "pkg":
if len(os.Args[1:]) < 2 {
fatalf("pm package: insufficient args\n\nusage: %v", pkgUsage)
}
sub := os.Args[2]
switch sub {
case "create", "creat", "c":
if signID == "" {
fatalf("must set PM_PGP_ID\n")
}
args := os.Args[3:]
if len(args) != 1 {
fatalf("usage: pm package create <directory>\n")
}
dir := args[0]
if err := pkg.Create(root, signID, dir); err != nil {
fatalf("creating package: %v\n", err)
}
default:
fatalf("unknown package subcommand: %q\n\nusage: %v", sub, pkgUsage)
}
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)
}

347
keyring/keyring.go Normal file
View File

@ -0,0 +1,347 @@
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 := 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")
}
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 := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create 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 := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create 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 := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create 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 := ensureDir(root); err != nil {
return errors.Wrap(err, "can't find or create 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 := 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")
}
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 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)
}
// FindSecretEntity searches for id in the secret keyring.
func FindSecretEntity(root, id string) (*openpgp.Entity, error) {
return nil, errors.New("NYI")
}

26
pkg/pkg.go Normal file
View File

@ -0,0 +1,26 @@
package pkg
import (
"fmt"
"log"
"github.com/pkg/errors"
"mcquay.me/fs"
"mcquay.me/pm/keyring"
)
// Create traverses the contents of dir and emits a valid pkg, signed by id
func Create(root, id, dir string) error {
if !fs.Exists(dir) {
return fmt.Errorf("%q: doesn't exist", dir)
}
if !fs.IsDir(dir) {
return fmt.Errorf("%q: is not a directory", dir)
}
e, err := keyring.FindSecretEntity(dir, id)
if err != nil {
return errors.Wrap(err, "find secret key")
}
log.Printf("found key: %v", e.PrimaryKey.KeyIdShortString())
return fmt.Errorf("creating package from %q for %q: NYI", dir, id)
}