Skip to content

Commit

Permalink
Merge branch 'local-keyring-service'
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
ellotheth committed Feb 7, 2016
2 parents 598c6c6 + c54f194 commit a4ede07
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 12 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,24 @@ drop it in your `$PATH`.
### People piping the installers

```
pipethis [ --target <exe> | --inspect | --editor <editor> | --no-verify | --signature <signature file> ] <script>
pipethis [ OPTIONS ] <script>
OPTIONS
--target <exe>
The shell or other binary that will run the script. Defaults to the SHELL
environment variable.
--lookup-with <keybase,local>
The service you'll use to verify the author's identity:
keybase (default)
Use https://keybase.io
local
Use your local GnuPG public keyring
--inspect
If set, open the script in an editor before checking the author.
Expand Down Expand Up @@ -74,6 +85,10 @@ but there's other stuff to do as well:
1. Get an account on [Keybase](https://keybase.io). I know, Real Crypto Geeks™
hate Keybase because Browser Crypto Is Unsafe™ and They Can Store
Your Private Key®. It's a place to start, yo, just do it.

Alternatively, you can hand out your public key at key signing parties
(because you're a Read Crypto Geek™, remember?), and tell people to import
it into their local public keyrings.
2. Add one line to your installation script to identify yourself. You can throw
it in a comment:

Expand Down
6 changes: 3 additions & 3 deletions lookup/keybase.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,14 @@ func (k KeybaseService) Matches(query string) ([]User, error) {
// Key finds the PGP public key for one Keybase user by Keybase username and
// returns the key ring representation of the key. If the Keybase username is
// invalid, or the key itself is missing or invalid, Key returns an error.
func (k KeybaseService) Key(user string) (openpgp.EntityList, error) {
func (k KeybaseService) Key(user User) (openpgp.EntityList, error) {

// I think I set this up to match Keybase's own username pattern. I think.
if matches, _ := regexp.MatchString(`^[a-zA-Z0-9_\-\.]+$`, user); !matches {
if matches, _ := regexp.MatchString(`^[a-zA-Z0-9_\-\.]+$`, user.Username); !matches {
return nil, errors.New("Invalid user requested")
}

resp, err := http.Get("https://keybase.io/" + user + "/key.asc")
resp, err := http.Get("https://keybase.io/" + user.Username + "/key.asc")
defer resp.Body.Close()
if err != nil {
return nil, err
Expand Down
129 changes: 129 additions & 0 deletions lookup/localpgp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
pipethis: Stop piping the internet into your shell
Copyright 2016 Ellotheth
Use of this source code is governed by the GNU Public License version 2
(GPLv2). You should have received a copy of the GPLv2 along with your copy of
the source. If not, see http://www.gnu.org/licenses/gpl-2.0.html.
*/

package lookup

import (
"errors"
"os"
"path"
"strconv"
"strings"

"golang.org/x/crypto/openpgp"
)

// LocalPGPService implements the KeyService interface for a local GnuPG
// public keyring.
type LocalPGPService struct {
ringfile string
ring openpgp.EntityList
}

// NewLocalPGPService creates a new LocalPGPService if it finds a local
// public keyring; otherwise it bails.
func NewLocalPGPService() (*LocalPGPService, error) {
ringfile := path.Join(os.Getenv("HOME"), ".gnupg", "pubring.gpg")

info, err := os.Stat(ringfile)
if err != nil || info.Size() == 0 {
return nil, err
}

return &LocalPGPService{ringfile: ringfile}, nil
}

// Ring loads the local public keyring so LocalPGPService can use it later. If
// it's // already been loaded, Ring returns the existing version.
func (l *LocalPGPService) Ring() openpgp.EntityList {
if l.ring != nil {
return l.ring
}

reader, err := os.Open(l.ringfile)
if err != nil {
return nil
}
defer reader.Close()

ring, err := openpgp.ReadKeyRing(reader)
if err != nil {
return nil
}

return ring
}

// Matches finds all the public keys that have a fingerprint or identity (name
// and email address) that match query. If no matches are found, Matches
// returns an error.
func (l *LocalPGPService) Matches(query string) ([]User, error) {
users := []User{}

ring := l.Ring()
if ring == nil {
return nil, errors.New("No key ring loaded")
}

// this is why LocalPGPService.ring has to be an EntityList instead of the
// more generic KeyRing: can't iterate through the latter. Botheration.
for _, key := range ring {
user := User{
Fingerprint: key.PrimaryKey.KeyIdString(),
}

for name, _ := range key.Identities {
user.Emails = append(user.Emails, name)
}

if l.isMatch(query, user) {
users = append(users, user)
}
}

if len(users) == 0 {
return nil, errors.New("No matches")
}

return users, nil
}

func (l LocalPGPService) isMatch(query string, user User) bool {
if strings.Contains(strings.ToUpper(user.Fingerprint), strings.ToUpper(query)) {
return true
}

for _, email := range user.Emails {
if strings.Contains(strings.ToUpper(email), strings.ToUpper(query)) {
return true
}
}

return false
}

// Key gets the PGP public key from the local public keyring for a user's
// fingerprint and // returns the keyRing representation. If the fingerprint is
// invalid or more than one public key is found, Key returns an error.
func (l *LocalPGPService) Key(user User) (openpgp.EntityList, error) {
id, err := strconv.ParseUint(user.Fingerprint, 16, 64)
if err != nil {
return nil, err
}

ring := l.Ring()
keys := ring.KeysById(id)
if len(keys) != 1 {
return nil, errors.New("More than one key returned, not sure what to do")
}

list := []*openpgp.Entity{keys[0].Entity}

return list, nil
}
44 changes: 44 additions & 0 deletions lookup/localpgp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package lookup

import (
"testing"

"github.com/stretchr/testify/suite"
)

type LocalPGPTest struct {
suite.Suite
}

func (s *LocalPGPTest) TestIsMatchMatchesOnFingerprint() {
local := LocalPGPService{}
user := User{Fingerprint: "foobar"}

s.True(local.isMatch("oba", user))
s.True(local.isMatch("foo", user))
s.True(local.isMatch("bar", user))
s.True(local.isMatch("FOOBAR", user))
}

func (s *LocalPGPTest) TestIsMatchMatchesOnEmails() {
local := LocalPGPService{}
user := User{Emails: []string{"foobar", "bizbaz", "THINGS"}}

s.True(local.isMatch("oba", user))
s.True(local.isMatch("foo", user))
s.True(local.isMatch("bar", user))
s.True(local.isMatch("FOOBAR", user))
s.True(local.isMatch("zba", user))
s.True(local.isMatch("thin", user))
}

func (s *LocalPGPTest) TestIsMatchFailsWithoutMatches() {
local := LocalPGPService{}
user := User{}

s.False(local.isMatch("foo", user))
}

func TestLocalPGPTest(t *testing.T) {
suite.Run(t, new(LocalPGPTest))
}
9 changes: 7 additions & 2 deletions lookup/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
// Key gets the PGP public key for one user.
type KeyService interface {
Matches(query string) ([]User, error)
Key(user string) (openpgp.EntityList, error)
Key(user User) (openpgp.EntityList, error)
}

// User represents an author's identity.
Expand All @@ -40,6 +40,7 @@ type User struct {
HackerNews string
Reddit string
Sites []string
Emails []string
}

// String returns a representation of all the User's identity details.
Expand All @@ -58,6 +59,10 @@ func (u User) String() string {
s = s + fmt.Sprintf(format, "Site", site)
}

for _, email := range u.Emails {
s = s + fmt.Sprintf(format, "Email", email)
}

return s
}

Expand Down Expand Up @@ -111,7 +116,7 @@ func Key(service KeyService, query string) (openpgp.KeyRing, error) {
}

// get the public key for the selected author
ring, err := service.Key(match.Username)
ring, err := service.Key(match)
if err != nil {
return nil, err
}
Expand Down
29 changes: 23 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ func main() {
}()

var (
target = flag.String("target", os.Getenv("SHELL"), "Executable to run the script")
inspect = flag.Bool("inspect", false, "Open an editor to inspect the file before running it")
editor = flag.String("editor", os.Getenv("EDITOR"), "Editor to inspect the script")
noVerify = flag.Bool("no-verify", false, "Don't verify the author or signature")
sigSource = flag.String("signature", "", `Detached signature to verify. (default "<script location>.sig")`)
target = flag.String("target", os.Getenv("SHELL"), "Executable to run the script")
inspect = flag.Bool("inspect", false, "Open an editor to inspect the file before running it")
editor = flag.String("editor", os.Getenv("EDITOR"), "Editor to inspect the script")
noVerify = flag.Bool("no-verify", false, "Don't verify the author or signature")
sigSource = flag.String("signature", "", `Detached signature to verify. (default "<script location>.sig")`)
serviceName = flag.String("lookup-with", "keybase", "Key lookup service to use. Could be 'keybase' or 'local'.")
)
flag.Parse()

Expand Down Expand Up @@ -76,7 +77,12 @@ func main() {
log.Panic(err)
}

key, err := lookup.Key(lookup.KeybaseService{}, author)
service, err := makeService(*serviceName)
if err != nil {
log.Panic(err)
}

key, err := lookup.Key(service, author)
if err != nil {
log.Panic(err)
}
Expand Down Expand Up @@ -143,3 +149,14 @@ func getLocal(location string) (io.ReadCloser, error) {

return os.Open(location)
}

func makeService(name string) (lookup.KeyService, error) {
switch name {
case "keybase":
return &lookup.KeybaseService{}, nil
case "local":
return lookup.NewLocalPGPService()
}

return nil, errors.New("Unrecognized key service")
}

0 comments on commit a4ede07

Please sign in to comment.