Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read shell scripts from STDIN #8

Merged
merged 13 commits into from
Mar 19, 2016
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ improve your life)
$ pipethis --no-verify --inspect https://get.rvm.io
```

or even

```
$ curl -sSL https://get.rvm.io | pipethis --no-verify | bash
```


## Install

```
Expand Down Expand Up @@ -59,9 +66,13 @@ OPTIONS
local
Use your local GnuPG public keyring

If you're piping a script from `stdin`, the service will be forced to
`local`.

--inspect

If set, open the script in an editor before checking the author.
If set, open the script in an editor before checking the author. Ignored if
you're piping a script from `stdin`.

--editor <editor>

Expand All @@ -75,11 +86,20 @@ OPTIONS

--signature <signature file>

The detached signature to verify <script> against. You'll only need this if
you've already downloaded the detached signature, or it's hosted in a
non-standard location (i.e. it's not <script>.sig).
The detached signature to verify <script> against. You'll only need this in
a couple scenarios:

- You've already downloaded the detached signature and you want to use your
downloaded copy, or
- the signature is hosted in a non-standard location (i.e. it's not
<script>.sig), or
- you're piping a script with a detached signature from `stdin`.
```

If you're piping scripts into `pipethis` directly from `curl`, you'll need
to have the script authors' PGP keys already stored in your local keyring.
Don't worry, they'll have instructions!

### People writing the installers

You can add one line to your installer script to make it support `pipethis`,
Expand All @@ -99,7 +119,7 @@ but there's other stuff to do as well:
# // ; '' PIPETHIS_AUTHOR your_name_or_your_key_fingerprint
```

3. Create a detached signature for the script. With Keybase, that's:
3. Create a signature for the script. With Keybase, that's:

```
$ keybase pgp sign -i yourscript.sh -d -o yourscript.sh.sig
Expand All @@ -111,9 +131,20 @@ but there's other stuff to do as well:
$ gpg --detach-sign -a -o yourscript.sh.sig yourscript.sh
```

Both those commands create ASCII-armored signatures. Binary signatures work
too.
4. Pop both the script and the signature up on your web server.
Both those commands create ASCII-armored signatures. Binary signatures work
too.

Alternatively, you can clearsign the script with an attached signature::

```
$ keybase pgp sign -i yourscript.unsigned.sh -c -o yourscript.sh
```

```
$ gpg --clearsign -a -o yourscript.sh yourscript.unsigned.sh
```

4. Pop the script (and the signature, if it's detached) up on your web server.
5. Replace your copy-paste-able installation instructions!

## What's all this noise
Expand Down Expand Up @@ -225,8 +256,6 @@ that you almost pwned yourself.

`pipethis` works, but it can be better!

- If there were a non-interactive version, it could be inserted into a pipe
chain like `curl -Ss http://pwn.me/please | pipethis | bash`. That'd be cool.
- There are zillions of other places to get public keys for people, and I want
to support more of them. I think Keybase is stellar and I love what they're
trying to do, but nobody likes to be locked in to one provider.
48 changes: 43 additions & 5 deletions lookup/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ func (u User) String() string {
return s
}

// NewKeyService creates the KeyService implementation requested by name. If
// fromPipe is true, it creates a LocalPGPService type.
func NewKeyService(name string, fromPipe bool) (KeyService, error) {
// force the local keyring when reading the script from a pipe
if fromPipe {
name = "local"
}

switch name {
case "keybase":
return &KeybaseService{}, nil
case "local":
return NewLocalPGPService()
}

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

// chooseMatch prints all the matches provided, prompts for a choice, and
// returns the chosen match.
func chooseMatch(matches []User) (User, error) {
Expand Down Expand Up @@ -101,19 +119,39 @@ func chooseMatch(matches []User) (User, error) {
return matches[n], nil
}

func chooseSingleMatch(matches []User) (User, error) {
if len(matches) != 1 {
return User{}, fmt.Errorf("Found %d author matches; need exactly 1 when reading from STDIN", len(matches))
}

return matches[0], nil
}

// Key looks up an author query in the provided KeyService, and prompts for a
// choice of matches. It returns an error if no matches were found, if no match
// was chosen, or if no PGP public was found.
func Key(service KeyService, query string) (openpgp.KeyRing, error) {
// choice of matches (if single is false) or automatically chooses the matched
// user when there is one and only one match (if single is true). It returns an
// error if no matches were found, if no match was chosen, or if no PGP public
// was found.
func Key(service KeyService, query string, single bool) (openpgp.KeyRing, error) {
// get possible matches from the key service
matches, err := service.Matches(query)
if err != nil {
return nil, err
}

if len(matches) < 1 {
return nil, errors.New("No author matches found for " + query)
}

// verify that the author is who the user was expecting by showing all the
// details (twitter handle, github handle, websites, etc.)
match, err := chooseMatch(matches)
var match User
if single {
match, err = chooseSingleMatch(matches)
} else {
match, err = chooseMatch(matches)
}

if err != nil {
return nil, err
}
Expand All @@ -123,7 +161,7 @@ func Key(service KeyService, query string) (openpgp.KeyRing, error) {
if err != nil {
return nil, err
}
log.Printf("Using %v (%v)", match.Username, ring[0].PrimaryKey.KeyIdShortString())
log.Printf("Verifying your script against\n%v", match)

return ring, nil
}
72 changes: 72 additions & 0 deletions lookup/lookup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
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 (
"os"
"testing"

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

type LookupTest struct {
home string
suite.Suite
}

func (s *LookupTest) SetupSuite() {
s.home = os.Getenv("HOME")
os.Setenv("HOME", os.TempDir())
}

func (s *LookupTest) TeardownSuite() {
os.Setenv("HOME", s.home)
}

func (s *LookupTest) TestNewKeyServiceAcceptsKeybaseWithoutPipe() {
service, err := NewKeyService("keybase", false)

s.NoError(err)
s.IsType(&KeybaseService{}, service)
}

func (s *LookupTest) TestNewKeyServiceForcesLocalWithPipe() {
_, err := NewKeyService("keybase", true)
s.Error(err)

perr, ok := err.(*os.PathError)
s.True(ok)
s.Equal("/tmp/.gnupg/pubring.gpg", perr.Path)
}

func (s *LookupTest) TestChooseSingleMatchBailsWithoutMatches() {
user, err := chooseSingleMatch([]User{})

s.Error(err)
s.Equal(User{}, user)
}

func (s *LookupTest) TestChooseSingleMatchBailsWithMoreThanOneMatch() {
user, err := chooseSingleMatch([]User{User{}, User{}})

s.Error(err)
s.Equal(User{}, user)
}

func (s *LookupTest) TestChooseSingleMatchReturnsSingleMatch() {
user, err := chooseSingleMatch([]User{User{Username: "foo"}})

s.NoError(err)
s.Equal("foo", user.Username)
}

func TestLookupTest(t *testing.T) {
suite.Run(t, new(LookupTest))
}
65 changes: 39 additions & 26 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func main() {
// still happen.
defer func() {
if r := recover(); r != nil {
flag.Usage()
os.Exit(1)
}
}()
Expand All @@ -52,16 +53,6 @@ func main() {
)
flag.Parse()

if len(flag.Args()) < 1 {
flag.Usage()
log.Panic("No script specified")
}

if _, err := os.Stat(*target); os.IsNotExist(err) {
log.Panic("Script executable does not exist")
}
log.Println("Using script executable", *target)

// download the script, store it someplace temporary
script, err := NewScript(flag.Arg(0))
if err != nil {
Expand All @@ -70,6 +61,15 @@ func main() {
defer os.Remove(script.Name())
log.Println("Script saved to", script.Name())

// if we're not reading from a pipe we need a target executable
if !script.IsPiped() {
if _, err := os.Stat(*target); os.IsNotExist(err) {
log.Panic("Script executable does not exist")
}

log.Println("Using script executable", *target)
}

// let the user look at it if they want
if cont := script.Inspect(*inspect, *editor); !cont {
log.Panic("Exiting without running", script.Name())
Expand All @@ -82,12 +82,12 @@ func main() {
log.Panic(err)
}

service, err := makeService(*serviceName)
service, err := lookup.NewKeyService(*serviceName, script.IsPiped())
if err != nil {
log.Panic(err)
}

key, err := lookup.Key(service, author)
key, err := lookup.Key(service, author, script.IsPiped())
if err != nil {
log.Panic(err)
}
Expand All @@ -99,12 +99,18 @@ func main() {
log.Panic(err)
}

log.Println("Signature", signature.Source(), "verified!")
log.Println("Signature verified!")
}

// run the script
log.Println("Running", script.Name(), "with", *target)
script.Run(*target, flag.Args()...)
if script.IsPiped() {
err = script.Echo()
} else {
err = script.Run(*target, flag.Args()...)
}
if err != nil {
log.Panic(err)
}
}

func parseToken(pattern string, reader io.Reader) string {
Expand All @@ -125,6 +131,10 @@ func parseToken(pattern string, reader io.Reader) string {

// getFile tries to find location locally first, then tries remote
func getFile(location string) (io.ReadCloser, error) {
if location == "" {
return getFromStdin()
}

body, err := getLocal(location)
if err == nil {
return body, nil
Expand All @@ -133,6 +143,20 @@ func getFile(location string) (io.ReadCloser, error) {
return getRemote(location)
}

func getFromStdin() (io.ReadCloser, error) {
stat, err := os.Stdin.Stat()
if err != nil {
return nil, err
}

// could also use os.ModeNamedPipe here? not sure if the difference matters
if (stat.Mode() & os.ModeCharDevice) != 0 {
return nil, errors.New("Nothing to read from STDIN and no script given")
}

return os.Stdin, nil
}

func getRemote(location string) (io.ReadCloser, error) {
parsed, err := url.Parse(location)
if err != nil || parsed.Scheme == "" {
Expand All @@ -154,14 +178,3 @@ 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")
}
Loading