diff --git a/.travis.yml b/.travis.yml index fdef53859..e5cd6fbbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: go +osx_image: xcode8.3 install: ./scripts/ci_install.sh go: diff --git a/Makefile b/Makefile index 82c73e42b..facf12df3 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ $(BIN)-windows-386.exe: $(SRC) GOOS=windows GOARCH=386 go build -o $@ -ldflags="$(FLAGS)" . release: $(BIN)-linux-amd64 $(BIN)-darwin-amd64 $(BIN)-windows-386.exe - codesign -s $(CERT) $(BIN)-darwin-amd64 + codesign -s "$(CERT)" $(BIN)-darwin-amd64 clean: rm -f $(BIN)-*-* diff --git a/README.md b/README.md index d8338b807..f89c96c6b 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,19 @@ Developed with golang, to install run: go get github.com/99designs/aws-vault ``` +## Self-signing your binary + +Binaries that call Keychain need to be signed, otherwise they always show the "allow access" prompt. Releases are signed by 99designs certificates, but if you are actively developing and want to mimic the behaviour of a signed release you can generate a self-signed code signing certificate. + +Check out Apple's guide on it [here](http://web.archive.org/web/20090119080759/http://developer.apple.com/documentation/Security/Conceptual/CodeSigningGuide/Procedures/chapter_3_section_2.html), or find it in `Keychain Access > Certificate Assistant > Create Certificate > Code Signing Certificate`. + +You can then sign your binary like this: + +```bash +make build +codesign -s "Name of my certificate" ./aws-vault +``` + ## References and Inspiration * https://github.com/pda/aws-keychain diff --git a/cli/exec.go b/cli/exec.go index 8d4577957..c84b12b5e 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -112,6 +112,7 @@ func ExecCommand(app *kingpin.Application, input ExecCommandInput) { val, err := creds.Get() if err != nil { app.Fatalf(vault.FormatCredentialError(input.Profile, profiles, err)) + return } if input.StartServer { diff --git a/cli/global.go b/cli/global.go index 0a62bfdb6..85e6ed5e5 100644 --- a/cli/global.go +++ b/cli/global.go @@ -26,6 +26,7 @@ var GlobalFlags struct { Debug bool Backend string PromptDriver string + Biometrics bool } func ConfigureGlobals(app *kingpin.Application) { @@ -42,6 +43,10 @@ func ConfigureGlobals(app *kingpin.Application) { OverrideDefaultFromEnvar("AWS_VAULT_PROMPT"). EnumVar(&GlobalFlags.PromptDriver, promptsAvailable...) + app.Flag("biometrics", "Use biometric authentication if supported"). + OverrideDefaultFromEnvar("AWS_VAULT_BIOMETRICS"). + BoolVar(&GlobalFlags.Biometrics) + app.PreAction(func(c *kingpin.ParseContext) (err error) { if !GlobalFlags.Debug { log.SetOutput(ioutil.Discard) @@ -49,10 +54,12 @@ func ConfigureGlobals(app *kingpin.Application) { if keyringImpl == nil { keyringImpl, err = keyring.Open(KeyringName, GlobalFlags.Backend) } + if globals.Biometrics { + keyring.Config.UseBiometrics = true + } if awsConfigFile == nil { awsConfigFile, err = vault.NewConfigFromEnv() } return err }) - } diff --git a/vendor/github.com/99designs/keyring/keychain.go b/vendor/github.com/99designs/keyring/keychain.go index 910d3f486..17d88005c 100644 --- a/vendor/github.com/99designs/keyring/keychain.go +++ b/vendor/github.com/99designs/keyring/keychain.go @@ -3,16 +3,28 @@ package keyring import ( + "errors" "fmt" "log" + "os" + + "golang.org/x/crypto/ssh/terminal" gokeychain "github.com/keybase/go-keychain" + touchid "github.com/lox/go-touchid" +) + +const ( + biometricsAccount = "com.99designs.aws-vault.biometrics" + biometricsService = "aws-vault" + biometricsLabel = "Passphrase for %s" ) type keychain struct { - path string - service string - passphrase string + path string + service string + passphrase string + authenticated bool } func init() { @@ -45,6 +57,7 @@ func (k *keychain) Get(key string) (Item, error) { query.SetReturnData(true) query.SetMatchSearchList(kc) + log.Printf("Querying service=%q, account=%q in osx keychain %s", k.service, key, k.path) results, err := gokeychain.QueryItem(query) if err == gokeychain.ErrorItemNotFound || len(results) == 0 { return Item{}, ErrKeyNotFound @@ -145,11 +158,92 @@ func (k *keychain) Keys() ([]string, error) { return accountNames, nil } +func (k *keychain) setupBiometrics() error { + fmt.Println("\nTo use biometrics for authentication, your keychain password needs to be stored in your login keychain.\n" + + "You will be prompted for your password.\n") + + fmt.Printf("Password for %q: ", k.path) + passphrase, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return err + } + + fmt.Println() + + // needs to be locked first in-case it's already unlocked. if so, an incorrect password can be stored + log.Printf("Locking keychain %s", k.path) + gokeychain.LockAtPath(k.path) + + log.Printf("Unlocking keychain %s", k.path) + if err := gokeychain.UnlockAtPath(k.path, string(passphrase)); err != nil { + return err + } + + k.passphrase = string(passphrase) + + item := gokeychain.NewItem() + item.SetSecClass(gokeychain.SecClassGenericPassword) + item.SetService(biometricsService) + item.SetAccount(biometricsAccount) + item.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + item.SetData(passphrase) + item.SetSynchronizable(gokeychain.SynchronizableNo) + item.SetAccessible(gokeychain.AccessibleWhenUnlocked) + + log.Printf("Adding service=%q, account=%q to osx keychain %s", biometricsService, biometricsAccount, k.path) + return gokeychain.AddItem(item) +} + +func (k *keychain) openWithBiometrics() (gokeychain.Keychain, error) { + if !k.authenticated { + log.Printf("Checking touchid") + ok, err := touchid.Authenticate("unlock " + k.path) + if !ok || err != nil { + return gokeychain.Keychain{}, errors.New("Authentication with biometrics failed") + } + + k.authenticated = true + + log.Printf("Looking up password stored in login.keychain") + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(biometricsService) + query.SetAccount(biometricsAccount) + query.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnData(true) + + results, err := gokeychain.QueryItem(query) + if err != nil { + return gokeychain.Keychain{}, err + } + + if len(results) != 1 { + err := k.setupBiometrics() + if err != nil { + return gokeychain.Keychain{}, err + } + } else { + log.Printf("Found passphrase in login.keychain, unlocking %s with stored password", k.path) + if err = gokeychain.UnlockAtPath(k.path, string(results[0].Data)); err != nil { + return gokeychain.Keychain{}, err + } + k.passphrase = string(results[0].Data) + } + } + + return gokeychain.NewWithPath(k.path), nil +} + func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { kc := gokeychain.NewWithPath(k.path) err := kc.Status() if err == nil { + if Config.UseBiometrics { + log.Printf("Opening %s with biometrics", k.path) + return k.openWithBiometrics() + } return kc, nil } diff --git a/vendor/github.com/99designs/keyring/keyring.go b/vendor/github.com/99designs/keyring/keyring.go index 2a52c72be..90f318923 100644 --- a/vendor/github.com/99designs/keyring/keyring.go +++ b/vendor/github.com/99designs/keyring/keyring.go @@ -3,12 +3,16 @@ package keyring import "errors" const ( - SecretServiceBackend string = "secret-service" - KeychainBackend string = "keychain" - KWalletBackend string = "kwallet" - FileBackend string = "file" + SecretServiceBackend string = "secret-service" + KeychainBackend string = "keychain" + KWalletBackend string = "kwallet" + FileBackend string = "file" ) +var Config struct { + UseBiometrics bool +} + var DefaultBackend = FileBackend var supportedBackends = map[string]opener{} diff --git a/vendor/github.com/lox/go-touchid/README.md b/vendor/github.com/lox/go-touchid/README.md new file mode 100644 index 000000000..e5a7d0a8b --- /dev/null +++ b/vendor/github.com/lox/go-touchid/README.md @@ -0,0 +1,26 @@ +# Authenticating with TouchID + +```golang +package main + +import ( + "log" + + touchid "github.com/lox/go-touchid" +) + +func main() { + ok, err := touchid.Authenticate("access llamas") + if err != nil { + log.Fatal(err) + } + + if ok { + log.Printf("Authenticated") + } else { + log.Fatal("Failed to authenticate") + } +} +``` + +![Screenshot](https://lachlan.me/s/9TMZWTYGikXoeCHm8RBgi8Bb0o4R1Bz6uI.png) diff --git a/vendor/github.com/lox/go-touchid/touchid.go b/vendor/github.com/lox/go-touchid/touchid.go new file mode 100644 index 000000000..19caad419 --- /dev/null +++ b/vendor/github.com/lox/go-touchid/touchid.go @@ -0,0 +1,56 @@ +package touchid + +/* +#cgo CFLAGS: -x objective-c -fmodules -fblocks +#cgo LDFLAGS: -framework CoreFoundation -framework LocalAuthentication -framework Foundation +#include +#include +#import + +int Authenticate(char const* reason) { + LAContext *myContext = [[LAContext alloc] init]; + NSError *authError = nil; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + NSString *nsReason = [NSString stringWithUTF8String:reason]; + __block int result = 0; + + if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { + [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:nsReason + reply:^(BOOL success, NSError *error) { + if (success) { + result = 1; + } else { + result = 2; + } + dispatch_semaphore_signal(sema); + }]; + } + + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_release(sema); + return result; +} +*/ +import ( + "C" +) +import ( + "errors" + "unsafe" +) + +func Authenticate(reason string) (bool, error) { + reasonStr := C.CString(reason) + defer C.free(unsafe.Pointer(reasonStr)) + + result := C.Authenticate(reasonStr) + switch result { + case 1: + return true, nil + case 2: + return false, nil + } + + return false, errors.New("Error occurred accessing biometrics") +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 190b0ce5a..f61d20d3a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -257,6 +257,12 @@ "revision": "efeae48c0b272ac15a6c0b53129285b2b0ed828d", "revisionTime": "2017-08-10T04:31:23Z" }, + { + "checksumSHA1": "kEyCzFEzVLWWRauNn0Nzxg2pg6Q=", + "path": "github.com/lox/go-touchid", + "revision": "619cc8e578d0ef916aa29c806117c370f9d621cb", + "revisionTime": "2017-07-12T10:52:33Z" + }, { "checksumSHA1": "AXacfEchaUqT5RGmPmMXsOWRhv8=", "path": "github.com/mitchellh/go-homedir",