├── .gitchangelog.rc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── aws_iam.go ├── aws_iam_test.go ├── bash ├── credulous.bash_completion └── credulous.sh ├── credentials.go ├── credentials_test.go ├── credulous.go ├── credulous_test.go ├── crypto.go ├── crypto_test.go ├── debian-pkg └── DEBIAN │ └── control ├── doc ├── DEVELOP-OSX.md └── credulous.md ├── git.go ├── git_test.go ├── osx-pkg ├── osx-distribution-template.xml ├── resources │ ├── conclusion.html │ └── credulous-security.png └── scripts │ ├── postinstall │ └── preinstall ├── pkgs └── README.md ├── rpm └── credulous.spec.tmpl ├── scripts ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── Vagrantfile ├── build_latest_libgit2 ├── generate-linux-pkgs ├── generate-osx-pkgs ├── generate-pkgs ├── github-release.sh ├── libgit2.pc-rhel └── libgit2.pc-ubuntu ├── testdata ├── credential.json ├── newcreds.json ├── saved_creds.json ├── testkey └── testkey.pub └── utils.go /.gitchangelog.rc: -------------------------------------------------------------------------------- 1 | ## 2 | ## Format 3 | ## 4 | ## ACTION: [AUDIENCE:] COMMIT_MSG [@TAG ...] 5 | ## 6 | ## Description 7 | ## 8 | ## ACTION is one of 'chg', 'fix', 'new' 9 | ## 10 | ## Is WHAT the change is about. 11 | ## 12 | ## 'chg' is for refactor, small improvement, cosmetic changes... 13 | ## 'fix' is for bug fixes 14 | ## 'new' is for new features, big improvement 15 | ## 16 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 17 | ## 18 | ## Is WHO is concerned by the change. 19 | ## 20 | ## 'dev' is for developpers (API changes, refactors...) 21 | ## 'usr' is for final users (UI changes) 22 | ## 'pkg' is for packagers (packaging changes) 23 | ## 'test' is for testers (test only related changes) 24 | ## 'doc' is for doc guys (doc only changes) 25 | ## 26 | ## COMMIT_MSG is ... well ... the commit message itself. 27 | ## 28 | ## TAGs are additional adjective as 'refactor' 'minor' 'cosmetic' 29 | ## 30 | ## 'refactor' is obviously for refactoring code only 31 | ## 'minor' is for a very meaningless change (a typo, adding a comment) 32 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 33 | ## 'wip' is for partial functionality but complete subfunctionality. 34 | ## 35 | ## Example: 36 | ## 37 | ## new: usr: support of bazaar implemented 38 | ## chg: re-indentend some lines @cosmetic 39 | ## new: dev: updated code to be compatible with last version of killer lib. 40 | ## fix: pkg: updated year of licence coverage. 41 | ## new: test: added a bunch of test around user usability of feature X. 42 | ## fix: typo in spelling my name in comment. @minor 43 | ## 44 | ## Please note that multi-line commit message are supported, and only the 45 | ## first line will be considered as the "summary" of the commit message. So 46 | ## tags, and other rules only applies to the summary. The body of the commit 47 | ## message will be displayed in the changelog with minor reformating. 48 | 49 | 50 | ## 51 | ## ``ignore_regexps`` is a line of regexps 52 | ## 53 | ## Any commit having its full commit message matching any regexp listed here 54 | ## will be ignored and won't be reported in the changelog. 55 | ## 56 | ignore_regexps = [ 57 | r'@minor', r'!minor', 58 | r'@cosmetic', r'!cosmetic', 59 | r'@refactor', r'!refactor', 60 | r'@wip', r'!wip', 61 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', 62 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', 63 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', 64 | ] 65 | 66 | 67 | ## 68 | ## ``replace_regexps`` is a dict associating a regexp pattern and its replacement 69 | ## 70 | ## It will be applied to get the summary line from the full commit message. 71 | ## 72 | ## Note that you can provide multiple replacement patterns, they will be all 73 | ## tried. If None matches, the summary line will be the full commit message. 74 | ## 75 | replace_regexps = { 76 | ## current format (ie: 'chg: dev: my commit msg @tag1 @tag2') 77 | 78 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$': 79 | r'\4', 80 | } 81 | 82 | 83 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a 84 | ## list of regexp 85 | ## 86 | ## Commit messages will be classified in sections thanks to this. Section 87 | ## titles are the label, and a commit is classified under this section if any 88 | ## of the regexps associated is matching. 89 | ## 90 | section_regexps = [ 91 | ('New', [ 92 | r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 93 | ]), 94 | ('Changes', [ 95 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 96 | ]), 97 | ('Fix', [ 98 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 99 | ]), 100 | 101 | ('Other', None ## Match all lines 102 | ), 103 | 104 | ] 105 | 106 | 107 | ## ``body_split_regexp`` is a regexp 108 | ## 109 | ## Commit message body (not the summary) if existing will be split 110 | ## (new line) on this regexp 111 | ## 112 | body_split_regexp = r'\n(?=\w+\s*:)' 113 | 114 | 115 | ## ``tag_filter_regexp`` is a regexp 116 | ## 117 | ## Tags that will be used for the changelog must match this regexp. 118 | ## 119 | tag_filter_regexp = r'^[0-9]+\.[0-9]+(\.[0-9]+)?$' 120 | 121 | 122 | ## ``unreleased_version_label`` is a string 123 | ## 124 | ## This label will be used as the changelog Title of the last set of changes 125 | ## between last valid tag and HEAD if any. 126 | unreleased_version_label = "%%version%% (unreleased)" 127 | 128 | 129 | ## ``output_engine`` is a callable 130 | ## 131 | ## This will change the output format of the generated changelog file 132 | ## 133 | ## Available choices are: 134 | ## 135 | ## - rest_py 136 | ## 137 | ## Legacy pure python engine, outputs ReSTructured text. 138 | ## This is the default. 139 | ## 140 | ## - mustache() 141 | ## 142 | ## Template name could be any of the available templates in 143 | ## ``templates/mustache/*.tpl``. 144 | ## Requires python package ``pystache``. 145 | ## Examples: 146 | ## - mustache("markdown") 147 | ## - mustache("restructuredtext") 148 | ## 149 | ## - makotemplate() 150 | ## 151 | ## Template name could be any of the available templates in 152 | ## ``templates/mako/*.tpl``. 153 | ## Requires python package ``mako``. 154 | ## Examples: 155 | ## - makotemplate("restructuredtext") 156 | ## 157 | output_engine = rest_py 158 | #output_engine = mustache("restructuredtext") 159 | #output_engine = mustache("markdown") 160 | #output_engine = makotemplate("restructuredtext") 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tags 3 | credulous 4 | *.rpm 5 | *.tar.gz 6 | *.swp 7 | .idea/ 8 | *.tgz 9 | *.1 10 | *.mde 11 | credulous.md-e 12 | travis_build_number 13 | pkg/ 14 | *.spec 15 | .vagrant/ 16 | s3/ 17 | *.pkg 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | install: 5 | - mkdir $HOME/gopath/src/github.com/libgit2 && cd $HOME/gopath/src/github.com/libgit2 && git clone https://github.com/libgit2/git2go.git 6 | - cd git2go && git submodule update --init 7 | - make install 8 | - cd $TRAVIS_BUILD_DIR 9 | - go get -v -t ./... 10 | - mkdir -p $HOME/gopath/bin 11 | - go install 12 | script: 13 | - export PATH=$HOME/gopath/bin:$PATH 14 | - go test ./... && scripts/generate-pkgs 15 | env: 16 | global: 17 | - PKG_CONFIG_PATH=/tmp/libgit2-0.21.0/lib/pkgconfig 18 | - secure: "atByW9YBuj/QUIxp0UtIM3hrvHN2mxReTs08VCIbeQCulct8FVU+/MRKTDAsP9gGT1jbKj7EXn77EoWWZBChOUhLK3Nl87UZEN78wDejkG1/vvMDwAcLmRXgAbEoJ0Zqzf/4kspdh3w7jb97TS+5Zf/PSlc7tvl2SUASvnz/jYE=" 19 | - secure: "EQH4LDyIFla+HcoLw7jLyRjk3WK4Nnp2QFxk/T5KScaVTW5f4kzfgkko2twSqxC8gIZS6k05fnaRnOFBXQ1wOWHuttguejXK2KOv5pYYICHg48a6feisdpYpRmwz9ral6PH43X4kNRxxShbMWZMAbmQRZ5trHfQXSh+H1ZMY4wU=" 20 | - secure: "aiym0bgK1A6dQ34E0ws9v761nJfSgjEgVnK7oLn5juffa+EiWruXz8E9gdRphJd59t44PeKglIMtwApptpPpvS5eZxxibeezm9PAdAs1MxJd3h8GQs7OR54HSzOBkw1+Kj6H+lgFyocbfhyyFNir7yQlK/ZChzT8+Kgord3B4aQ=" 21 | after_success: 22 | - scripts/github-release.sh "$TRAVIS_REPO_SLUG" "`git describe`" true *.rpm *.deb 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # I need to be able to replicate the Travis build environment in order 2 | # to troubleshoot some problems with it in a reasonable fast way. 3 | # Their build systems are Ubuntu Server 12.04 LTS (aka Precise Pangolin) 4 | FROM ubuntu:precise 5 | MAINTAINER Colin.Panisset 6 | RUN apt-get update 7 | RUN apt-get install -y python-software-properties 8 | RUN add-apt-repository ppa:pdoes/ppa 9 | RUN apt-get update 10 | RUN apt-get install -y git mercurial subversion \ 11 | curl wget clang gcc openssl rsync 12 | ADD http://golang.org/dl/go1.2.2.linux-amd64.tar.gz /tmp/go1.2.2.linux-amd64.tar.gz 13 | RUN tar -C /usr/local -xzf /tmp/go1.2.2.linux-amd64.tar.gz 14 | RUN ln -s /usr/local/go/bin/go /usr/bin/go 15 | RUN ln -s /usr/local/go/bin/gofmt /usr/bin/gofmt 16 | RUN ln -s /usr/local/go/bin/godoc /usr/bin/godoc 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 REA Group Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This option defines which mock configuration to use -- see /etc/mock for 2 | # the available configuration files for your system. 3 | MOCK_CONFIG=epel-6-x86_64 4 | SHELL=/bin/bash 5 | DIST=$(shell grep "config_opts.*dist.*" /etc/mock/$(MOCK_CONFIG).cfg | awk '{ print $$3 }' | cut -f2 -d\' ) 6 | 7 | SRCS=$(shell ls -1 *.go | grep -v _test.go ) bash/credulous.bash_completion \ 8 | doc/credulous.md bash/credulous.sh scripts/libgit2.pc-rhel 9 | TESTS=credulous_test.go credentials_test.go crypto_test.go git_test.go \ 10 | testdata/testkey testdata/testkey.pub testdata/credential.json testdata/newcreds.json 11 | 12 | DOC=doc/credulous.md 13 | MAN=doc/credulous.1 14 | SPEC=rpm/credulous.spec 15 | SPEC_TMPL=rpm/credulous.spec.tmpl 16 | NAME=$(shell grep '^Name:' $(SPEC_TMPL) | awk '{ print $$2 }' ) 17 | # Because we run under sudo, environment variables don't make it through 18 | BUILD_NR=$(shell cat travis_build_number) 19 | ifeq ($(strip $(BUILD_NR)), ) 20 | BUILD_NR=unknown 21 | endif 22 | VERS=$(shell cat VERSION 2>/dev/null ) 23 | VERSION=$(VERS).$(BUILD_NR) 24 | RELEASE=$(shell grep '^Release:' $(SPEC_TMPL) | awk '{ print $$2 }' | sed -e 's/%{?dist}/.$(DIST)/' ) 25 | 26 | MOCK_RESULT=/var/lib/mock/$(MOCK_CONFIG)/result 27 | 28 | NVR=$(NAME)-$(VERSION)-$(RELEASE) 29 | MOCK_SRPM=$(NVR).src.rpm 30 | RPM=$(NVR).x86_64.rpm 31 | TGZ=$(NAME)-$(VERSION).tar.gz 32 | 33 | INSTALLABLES=credulous bash/credulous.bash_completion doc/credulous.1 bash/credulous.sh 34 | 35 | .DEFAULT: all 36 | .PHONY: debianpkg 37 | 38 | all: mock 39 | 40 | man: $(DOC) 41 | sed -e 's/==VERSION==/$(VERSION)/' $(DOC) | pandoc -s -w man - -o $(MAN) 42 | 43 | osx_binaries: $(SRCS) $(TESTS) 44 | @echo "Building for OSX" 45 | go get -t 46 | go test 47 | go build 48 | 49 | osx: man osx_binaries 50 | tar zcvf credulous-$(VERSION)-osx.tgz $(INSTALLABLES) 51 | 52 | # This is a dirty hack for building on ubuntu build agents in Travis. 53 | rpmbuild: sources 54 | @mkdir -p $(HOME)/rpmbuild/SOURCES \ 55 | $(HOME)/rpmbuild/SRPMS \ 56 | $(HOME)/rpmbuild/RPMS \ 57 | $(HOME)/rpmbuild/SPECS \ 58 | $(HOME)/rpmbuild/BUILD \ 59 | $(HOME)/rpmbuild/BUILDROOT 60 | cp $(NAME)-$(VERSION).tar.gz $(HOME)/rpmbuild/SOURCES 61 | rpmbuild -bs --target x86_64 --nodeps rpm/credulous.spec 62 | rpmbuild -bb --target x86_64 --nodeps rpm/credulous.spec 63 | 64 | # Create the source tarball with N-V prefix to match what the specfile expects 65 | sources: 66 | @echo "Building for version '$(VERSION)'" 67 | sed -i -e 's/==VERSION==/$(VERSION)/' $(DOC) 68 | tar czvf $(TGZ) --transform='s|^|src/github.com/realestate-com-au/credulous/|' $(SRCS) $(TESTS) 69 | 70 | debianpkg: 71 | @echo Build Debian packages 72 | sed -i -e 's/==VERSION==/$(VERSION)/' debian-pkg/DEBIAN/control 73 | sed -i -e 's/==VERSION==/$(VERSION)/' $(DOC) 74 | mkdir -p debian-pkg/usr/bin \ 75 | debian-pkg/usr/share/man/man1 \ 76 | debian-pkg/etc/bash_completion.d \ 77 | debian-pkg/etc/profile.d 78 | cp $(HOME)/gopath/bin/credulous debian-pkg/usr/bin 79 | cp bash/credulous.sh debian-pkg/etc/profile.d 80 | cp bash/credulous.bash_completion debian-pkg/etc/bash_completion.d 81 | chmod 0755 debian-pkg/usr/bin/credulous 82 | pandoc -s -w man $(DOC) -o debian-pkg/usr/share/man/man1/credulous.1 83 | dpkg-deb --build debian-pkg 84 | mv debian-pkg.deb $(NAME)_$(VERSION)_amd64.deb 85 | 86 | mock: mock-rpm 87 | @echo "BUILD COMPLETE; RPMS are in ." 88 | 89 | mock-rpm: mock-srpm 90 | mock -r $(MOCK_CONFIG) --rebuild $(MOCK_SRPM) 91 | cp $(MOCK_RESULT)/$(RPM) . 92 | 93 | mock-srpm: sources 94 | @echo "DIST is $(DIST)" 95 | @echo "RELEASE is $(RELEASE)" 96 | # mock -r $(MOCK_CONFIG) --init 97 | sed -e 's/==VERSION==/$(VERSION)/' $(SPEC_TMPL) > $(SPEC) 98 | mock -r $(MOCK_CONFIG) --buildsrpm --spec $(SPEC) --sources . 99 | rm -f $(SPEC) 100 | cp $(MOCK_RESULT)/$(MOCK_SRPM) . 101 | 102 | clean: 103 | rm -f $(MOCK_SRPM) $(RPM) $(TGZ) 104 | 105 | allclean: 106 | mock -r $(MOCK_CONFIG) --clean 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # this software is archived 2 | 3 | As at 2021-03, REA Group no longer directly uses nor supports this software - as such, we have archived it. 4 | 5 | You will note that the LICENSE file _already_ describes a lack of any warranty. 6 | 7 | We leave the software visible as an example of our technology journey through the years, and hope it's useful for such. 8 | 9 | (you might consider https://github.com/99designs/aws-vault) 10 | 11 | ----- 12 | 13 | # Credulous 14 | 15 | **credulous** is a command line tool that manages **AWS (IAM) Credentials 16 | securely**. The aim is to encrypt the credentials using a user's **public 17 | SSH Key** so that only the user who has the corresponding **private SSH 18 | key** is able to see and use them. Furthermore the tool will also enable 19 | the user to **easily rotate** their current credentials without breaking 20 | the user's current workflow. 21 | 22 | ## Main Features 23 | 24 | * Your IAM Credentials are securely encrypted on disk. 25 | * Easy switching of Credentials between Accounts/Users. 26 | * Painless Credential rotation. 27 | * Enables rotation of Credentials by external application/service. 28 | * No external runtime dependencies beyond minimal platform-specific 29 | shared libraries 30 | 31 | ## Installation 32 | 33 | ### For Linux (.RPM or .DEB packages) 34 | 35 | Download your [Linux package](https://github.com/realestate-com-au/credulous/releases) 36 | 37 | 38 | ### For OSX 39 | 40 | If you are using *[Homebrew](http://brew.sh/)* you can follow these steps to install Credulous 41 | 42 | 1. ```localhost$ brew install bash-completion``` 43 | 1. Add the following lines to your ~/.bash_profile: 44 | ``` 45 | if [ -f $(brew --prefix)/etc/bash_completion ]; then 46 | . $(brew --prefix)/etc/bash_completion 47 | fi 48 | ``` 49 | 1. ```localhost$ brew install https://raw.githubusercontent.com/realestate-com-au/credulous-brew/master/credulous.rb``` 50 | 1. Add the following lines to your ~/.bash_profile: 51 | ``` 52 | if [ -f $(brew --prefix)/etc/profile.d/credulous.sh ]; then 53 | . $(brew --prefix)/etc/profile.d/credulous.sh 54 | fi 55 | ``` 56 | 57 | ### Command completion 58 | 59 | Command completion makes credulous much more convenient to use. 60 | 61 | OSX: `brew install bash-completion` 62 | 63 | Centos: [Enable EPEL repo and install bash-completion](http://unix.stackexchange.com/questions/21135/package-bash-completion-missing-from-yum-in-centos-6) 64 | 65 | Debian/Ubuntu: bash-completion is installed and enabled by default. Enjoy! 66 | 67 | 68 | 69 | ## Usage 70 | 71 | Credentials need to have the right to inspect the account alias, 72 | list access keys and examine the username of the user for whom they 73 | exist. An IAM policy snippet like this will grant sufficient 74 | permissions: 75 | 76 | ```json 77 | { 78 | "Version": "2012-10-17", 79 | "Statement": [ 80 | { 81 | "Sid": "PermitViewAliases", 82 | "Effect": "Allow", 83 | "Action": [ "iam:ListAccountAliases" ], 84 | "Resource": "*" 85 | }, 86 | { 87 | "Sid": "PermitViewOwnDetails", 88 | "Effect": "Allow", 89 | "Action": [ 90 | "iam:ListAccessKeys", 91 | "iam:GetUser" 92 | ], 93 | "Resource": "arn:aws:iam::*:user/${aws:username}" 94 | } 95 | ] 96 | } 97 | ``` 98 | 99 | You can have a [look at the manual 100 | page](https://github.com/realestate-com-au/credulous/blob/master/credulous.md), if that's your thing. 101 | 102 | Storing your current credentials in Credulous 103 | 104 | $ export AWS_ACCESS_KEY_ID=YOUR_AWS_ID 105 | $ export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXX 106 | $ credulous save # Will ask credulous to store these credentials 107 | # saving credentials for user@account 108 | 109 | Displaying a set of credentials from Credulous 110 | 111 | $ credulous source -a account -u user 112 | export AWS_ACCESS_KEY_ID=YOUR_AWS_ID 113 | export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXX 114 | 115 | 116 | ## Development 117 | 118 | [![Build Status](https://travis-ci.org/realestate-com-au/credulous.svg)](https://travis-ci.org/realestate-com-au/credulous) 119 | 120 | Required tools: 121 | * [go](http://golang.org) 122 | * [git](http://git-scm.com) 123 | * [bzr](http://bazaar.canonical.com) 124 | * [mercurial](http://mercurial.selenic.com) 125 | 126 | Make sure you have [GOPATH](http://golang.org/doc/code.html#GOPATH) set in your environment 127 | 128 | Download the dependencies 129 | 130 | $ go get -u # -u will update existing dependencies 131 | 132 | Install [git2go](https://github.com/libgit2/git2go) (Optional if you already have it installed correctly in your environment) 133 | 134 | $ go get github.com/libgit2/git2go 135 | $ cd $GOPATH/src/github.com/libgit2/git2go && rm -rf vendor/libgit2 136 | $ git submodule update --init 137 | $ mkdir -p $GOPATH/src/github.com/libgit2/git2go/vendor/libgit2/install/lib 138 | $ make install 139 | # Run dependency update again for credulous 140 | $ cd $GOPATH/src/github.com/realestate-com-au/credulous && go get -u 141 | 142 | Install the binary in your $GOBIN 143 | 144 | $ go install 145 | 146 | ## Tests 147 | 148 | First we make sure we have our dependencies 149 | 150 | go get -t 151 | 152 | Make sure goconvey is installed, else use 153 | 154 | go get -t github.com/smartystreets/goconvey 155 | 156 | Just go into this directory and either 157 | 158 | goconvey 159 | < Go to localhost:8080 in your browser > 160 | 161 | Or just run 162 | 163 | go test ./... 164 | 165 | ## Roadmap 166 | See [here](https://github.com/realestate-com-au/credulous/wiki/Roadmap) 167 | 168 | ![Credulous Security](https://github.com/realestate-com-au/credulous/raw/master/site/credulous-security.png) 169 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.2 2 | -------------------------------------------------------------------------------- /aws_iam.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/realestate-com-au/goamz/aws" 9 | "github.com/realestate-com-au/goamz/iam" 10 | ) 11 | 12 | type Instancer interface { 13 | GetUser(string) (*iam.GetUserResp, error) 14 | AccessKeys(string) (*iam.AccessKeysResp, error) 15 | ListAccountAliases() (*iam.AccountAliasesResp, error) 16 | } 17 | 18 | func getAWSUsernameAndAlias(cred Credential) (username, alias string, err error) { 19 | auth := aws.Auth{ 20 | AccessKey: cred.KeyId, 21 | SecretKey: cred.SecretKey, 22 | } 23 | // Note: the region is irrelevant for IAM 24 | instance := iam.New(auth, aws.APSoutheast2) 25 | username, err = getAWSUsername(instance) 26 | if err != nil { 27 | return "", "", err 28 | } 29 | 30 | alias, err = getAWSAccountAlias(instance) 31 | if err != nil { 32 | return "", "", err 33 | } 34 | 35 | return username, alias, nil 36 | } 37 | 38 | func getAWSUsername(instance Instancer) (string, error) { 39 | response, err := instance.GetUser("") 40 | if err != nil { 41 | return "", err 42 | } 43 | return response.User.Name, nil 44 | } 45 | 46 | func getKeyCreateDate(instance Instancer) (string, error) { 47 | response, err := instance.AccessKeys("") 48 | panic_the_err(err) 49 | // This mess is because iam.IAM and TestIamInstance are structs 50 | elem := reflect.ValueOf(instance).Elem() 51 | auth := elem.FieldByName("Auth") 52 | accessKey := auth.FieldByName("AccessKey").String() 53 | for _, key := range response.AccessKeys { 54 | if key.Id == accessKey { 55 | return key.CreateDate, nil 56 | } 57 | } 58 | return "", errors.New("Couldn't find this key") 59 | } 60 | 61 | func getAWSAccountAlias(instance Instancer) (string, error) { 62 | response, err := instance.ListAccountAliases() 63 | if err != nil { 64 | return "", err 65 | } 66 | // There really is only one alias 67 | if len(response.Aliases) == 0 { 68 | // we have to do a getuser instead and parse out the 69 | // account ID from the ARN 70 | response, err := instance.GetUser("") 71 | if err != nil { 72 | return "", err 73 | } 74 | id := strings.Split(response.User.Arn, ":") 75 | return id[4], nil 76 | } 77 | return response.Aliases[0], nil 78 | } 79 | 80 | func verify_account(alias string, instance Instancer) error { 81 | acct_alias, err := getAWSAccountAlias(instance) 82 | if err != nil { 83 | return err 84 | } 85 | if acct_alias == alias { 86 | return nil 87 | } 88 | err = errors.New("Cannot verify account: does not match alias " + alias) 89 | return err 90 | } 91 | 92 | func verify_user(username string, instance Instancer) error { 93 | response, err := instance.AccessKeys(username) 94 | if err != nil { 95 | return err 96 | } 97 | // This mess is because iam.IAM and TestIamInstance are structs 98 | elem := reflect.ValueOf(instance).Elem() 99 | auth := elem.FieldByName("Auth") 100 | accessKey := auth.FieldByName("AccessKey").String() 101 | for _, key := range response.AccessKeys { 102 | if key.Id == accessKey { 103 | return nil 104 | } 105 | } 106 | err = errors.New("Cannot verify user: access keys are not for user " + username) 107 | return err 108 | } 109 | -------------------------------------------------------------------------------- /aws_iam_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/realestate-com-au/goamz/aws" 8 | "github.com/realestate-com-au/goamz/iam" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | type TestIamInstance struct { 13 | Auth aws.Auth 14 | getUserResp iam.GetUserResp 15 | accessKeysResp iam.AccessKeysResp 16 | accountAliasesResp iam.AccountAliasesResp 17 | } 18 | 19 | func (t *TestIamInstance) GetUser(username string) (*iam.GetUserResp, error) { 20 | if t.getUserResp.User.Name == "" { 21 | return &iam.GetUserResp{}, errors.New("No user of that name") 22 | } 23 | return &t.getUserResp, nil 24 | } 25 | 26 | func (t *TestIamInstance) AccessKeys(username string) (*iam.AccessKeysResp, error) { 27 | if len(t.accessKeysResp.AccessKeys) == 0 { 28 | return &iam.AccessKeysResp{}, errors.New("No keys for that user") 29 | } 30 | return &t.accessKeysResp, nil 31 | } 32 | 33 | func (t *TestIamInstance) ListAccountAliases() (*iam.AccountAliasesResp, error) { 34 | return &t.accountAliasesResp, nil 35 | } 36 | 37 | func TestGetAWSUsername(t *testing.T) { 38 | Convey("Test getAWSUsername", t, func() { 39 | tstInst := TestIamInstance{ 40 | getUserResp: iam.GetUserResp{ 41 | RequestId: "abc123", 42 | User: iam.User{ 43 | Arn: "some-arn", 44 | Path: "/", 45 | Id: "userid", 46 | Name: "foonly", 47 | }, 48 | }, 49 | } 50 | resp, _ := getAWSUsername(&tstInst) 51 | So(resp, ShouldEqual, "foonly") 52 | }) 53 | } 54 | 55 | func TestGetKeyCreateDate(t *testing.T) { 56 | Convey("Test getKeyCreateDate", t, func() { 57 | tstKey := []iam.AccessKey{} 58 | tstKey = append(tstKey, iam.AccessKey{ 59 | UserName: "bob", 60 | Id: "AKIAtest", 61 | Secret: "sooper-seekrit", 62 | Status: "happy", 63 | CreateDate: "1970-01-01T00:00:00:00Z", 64 | }) 65 | tstInst := TestIamInstance{ 66 | accessKeysResp: iam.AccessKeysResp{ 67 | RequestId: "abc123", 68 | AccessKeys: tstKey, 69 | }, 70 | Auth: aws.Auth{ 71 | AccessKey: "AKIAtest", 72 | SecretKey: "sooper-seekrit", 73 | }, 74 | } 75 | date, err := getKeyCreateDate(&tstInst) 76 | So(date, ShouldEqual, tstKey[0].CreateDate) 77 | So(err, ShouldEqual, nil) 78 | }) 79 | } 80 | 81 | func TestListAccountAliases(t *testing.T) { 82 | Convey("Test listAccountAliases", t, func() { 83 | Convey("when the alias matches", func() { 84 | tstResp := iam.AccountAliasesResp{ 85 | RequestId: "abc123", 86 | Aliases: []string{"test-alias"}, 87 | } 88 | tstInst := TestIamInstance{accountAliasesResp: tstResp} 89 | alias, _ := getAWSAccountAlias(&tstInst) 90 | So(alias, ShouldEqual, "test-alias") 91 | }) 92 | Convey("when there is no account alias", func() { 93 | tstResp := iam.AccountAliasesResp{} 94 | tstInst := TestIamInstance{ 95 | accountAliasesResp: tstResp, 96 | getUserResp: iam.GetUserResp{ 97 | RequestId: "abc123", 98 | User: iam.User{ 99 | Arn: "arn:aws:iam::123456789012:user/foonly", 100 | Path: "/", 101 | Id: "userid", 102 | Name: "foonly", 103 | }, 104 | }, 105 | } 106 | alias, err := getAWSAccountAlias(&tstInst) 107 | So(err, ShouldEqual, nil) 108 | So(alias, ShouldNotEqual, "") 109 | }) 110 | Convey("when there is somehow more than one alias", func() { 111 | tstResp := iam.AccountAliasesResp{ 112 | RequestId: "abc123", 113 | Aliases: []string{"test-alias", "second-alias"}, 114 | } 115 | tstInst := TestIamInstance{accountAliasesResp: tstResp} 116 | alias, _ := getAWSAccountAlias(&tstInst) 117 | So(alias, ShouldEqual, "test-alias") 118 | }) 119 | }) 120 | } 121 | 122 | func TestVerifyAccount(t *testing.T) { 123 | Convey("Test verifyAccount", t, func() { 124 | Convey("when there is no account alias but we think there is", func() { 125 | tstResp := iam.AccountAliasesResp{} 126 | tstInst := TestIamInstance{ 127 | accountAliasesResp: tstResp, 128 | getUserResp: iam.GetUserResp{ 129 | RequestId: "abc123", 130 | User: iam.User{ 131 | Arn: "arn:aws:iam::123456789012:user/foonly", 132 | Path: "/", 133 | Id: "userid", 134 | Name: "foonly", 135 | }, 136 | }, 137 | } 138 | err := verify_account("test-alias", &tstInst) 139 | So(err, ShouldNotEqual, nil) 140 | So(err.Error(), ShouldEqual, "Cannot verify account: does not match alias test-alias") 141 | }) 142 | Convey("when returned alias matches expected alias", func() { 143 | tstResp := iam.AccountAliasesResp{ 144 | RequestId: "abc123", 145 | Aliases: []string{"test-alias"}, 146 | } 147 | tstInst := TestIamInstance{accountAliasesResp: tstResp} 148 | err := verify_account("test-alias", &tstInst) 149 | So(err, ShouldEqual, nil) 150 | }) 151 | Convey("when returned alias does not match expected alias", func() { 152 | tstResp := iam.AccountAliasesResp{ 153 | RequestId: "abc123", 154 | Aliases: []string{"test-alias"}, 155 | } 156 | tstInst := TestIamInstance{accountAliasesResp: tstResp} 157 | err := verify_account("nomatch-alias", &tstInst) 158 | So(err.Error(), ShouldEqual, "Cannot verify account: does not match alias nomatch-alias") 159 | }) 160 | Convey("when there is no alias and we expect that", func() { 161 | tstResp := iam.AccountAliasesResp{} 162 | tstInst := TestIamInstance{ 163 | accountAliasesResp: tstResp, 164 | getUserResp: iam.GetUserResp{ 165 | RequestId: "abc123", 166 | User: iam.User{ 167 | Arn: "arn:aws:iam::123456789012:user/foonly", 168 | Path: "/", 169 | Id: "userid", 170 | Name: "foonly", 171 | }, 172 | }, 173 | } 174 | err := verify_account("123456789012", &tstInst) 175 | So(err, ShouldEqual, nil) 176 | }) 177 | 178 | }) 179 | } 180 | 181 | func TestVerifyUser(t *testing.T) { 182 | Convey("Test verify_user", t, func() { 183 | Convey("when username not found", func() { 184 | tstInst := TestIamInstance{ 185 | Auth: aws.Auth{ 186 | AccessKey: "AKIAtest", 187 | SecretKey: "sooper-seekrit", 188 | }, 189 | } 190 | err := verify_user("bob", &tstInst) 191 | So(err, ShouldNotEqual, nil) 192 | }) 193 | Convey("when returned user matches expected user", func() { 194 | tstKey := []iam.AccessKey{} 195 | // this is the set of responses 196 | tstKey = append(tstKey, iam.AccessKey{ 197 | UserName: "bob", 198 | Id: "AKIAtest", 199 | Secret: "sooper-seekrit", 200 | Status: "happy", 201 | CreateDate: "1970-01-01T00:00:00:00Z", 202 | }) 203 | tstInst := TestIamInstance{ 204 | accessKeysResp: iam.AccessKeysResp{ 205 | RequestId: "abc123", 206 | AccessKeys: tstKey, 207 | }, 208 | Auth: aws.Auth{ 209 | AccessKey: "AKIAtest", 210 | SecretKey: "sooper-seekrit", 211 | }, 212 | } 213 | err := verify_user("bob", &tstInst) 214 | So(err, ShouldEqual, nil) 215 | }) 216 | Convey("when returned user does not match expected user", func() { 217 | tstKey := []iam.AccessKey{} 218 | // this is the set of responses 219 | tstKey = append(tstKey, iam.AccessKey{ 220 | UserName: "fred", 221 | Id: "AKIAcheese", 222 | Secret: "notso-seekrit", 223 | Status: "indifferent", 224 | CreateDate: "1970-01-01T00:00:00:00Z", 225 | }) 226 | tstInst := TestIamInstance{ 227 | accessKeysResp: iam.AccessKeysResp{ 228 | RequestId: "abc123", 229 | AccessKeys: tstKey, 230 | }, 231 | Auth: aws.Auth{ 232 | AccessKey: "AKIAtest", 233 | SecretKey: "sooper-seekrit", 234 | }, 235 | } 236 | err := verify_user("bob", &tstInst) 237 | So(err, ShouldNotEqual, nil) 238 | So(err.Error(), ShouldEqual, "Cannot verify user: access keys are not for user bob") 239 | }) 240 | 241 | }) 242 | } 243 | -------------------------------------------------------------------------------- /bash/credulous.bash_completion: -------------------------------------------------------------------------------- 1 | _credulous() 2 | { 3 | local cur prev opts base 4 | COMPREPLY=() 5 | cur="${COMP_WORDS[COMP_CWORD]}" 6 | prev="${COMP_WORDS[COMP_CWORD-1]}" 7 | 8 | # 9 | # Commands we'll complete 10 | # 11 | commands="display save source list current rotate" 12 | 13 | # 14 | # Complete the arguments to some (well, one!) of the commands. 15 | # 16 | case "${prev}" in 17 | source) 18 | local creds=$(credulous list) 19 | COMPREPLY=( $(compgen -W "${creds}" -- ${cur}) ) 20 | return 0 21 | ;; 22 | *) 23 | COMPREPLY=($(compgen -W "${commands}" -- ${cur})) 24 | return 0 25 | ;; 26 | esac 27 | 28 | } 29 | complete -F _credulous credulous 30 | -------------------------------------------------------------------------------- /bash/credulous.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # credulous.sh 4 | # 5 | # Wrapper that sources output of 'credulous source' command into 6 | # current environment 7 | # 8 | # Source this from your ~/.bash_profile 9 | 10 | credulous () { 11 | BINARY=$( type -P credulous ) 12 | RES=$( $BINARY $@ ) 13 | RET=$? 14 | if [ $RET -eq 0 -a "x$1" = "xsource" ]; then 15 | echo -n "Loading AWS creds into current environment..." 16 | eval "$RES" 17 | if [ $? -eq 0 ]; then 18 | echo "OK" 19 | else 20 | echo "FAIL" 21 | fi 22 | elif [ $RET -eq 0 ]; then 23 | echo "$RES" 24 | else 25 | echo "Failed to source credentials" 26 | return 1 27 | fi 28 | } 29 | -------------------------------------------------------------------------------- /credentials.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | "time" 15 | 16 | "github.com/realestate-com-au/goamz/aws" 17 | "github.com/realestate-com-au/goamz/iam" 18 | 19 | "code.google.com/p/go.crypto/ssh" 20 | ) 21 | 22 | const FORMAT_VERSION string = "2014-06-12" 23 | 24 | // How long to retry after rotating credentials for 25 | // new credentials to become active (in seconds) 26 | const ROTATE_TIMEOUT int = 30 27 | 28 | type Credentials struct { 29 | Version string 30 | IamUsername string 31 | AccountAliasOrId string 32 | CreateTime string 33 | LifeTime int 34 | Encryptions []Encryption 35 | } 36 | 37 | type Encryption struct { 38 | Fingerprint string 39 | Ciphertext string 40 | // we can do this because the field isn't exported 41 | // and so won't be included when we call Marshal to 42 | // save the encrypted credentials 43 | decoded Credential 44 | } 45 | 46 | type Credential struct { 47 | KeyId string 48 | SecretKey string 49 | EnvVars map[string]string 50 | } 51 | 52 | type OldCredential struct { 53 | CreateTime string 54 | LifeTime int 55 | KeyId string 56 | SecretKey string 57 | Salt string 58 | AccountAliasOrId string 59 | IamUsername string 60 | FingerPrint string 61 | } 62 | 63 | type SaveData struct { 64 | cred Credential 65 | username string 66 | alias string 67 | pubkeys []ssh.PublicKey 68 | lifetime int 69 | force bool 70 | repo string 71 | isRepo bool 72 | } 73 | 74 | func decodeOldCredential(data []byte, keyfile string) (*OldCredential, error) { 75 | var credential OldCredential 76 | err := json.Unmarshal(data, &credential) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | privKey, err := loadPrivateKey(keyfile) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | decoded, err := CredulousDecodeWithSalt(credential.KeyId, credential.Salt, privKey) 87 | if err != nil { 88 | return nil, err 89 | } 90 | credential.KeyId = decoded 91 | 92 | decoded, err = CredulousDecodeWithSalt(credential.SecretKey, credential.Salt, privKey) 93 | if err != nil { 94 | return nil, err 95 | } 96 | credential.SecretKey = decoded 97 | 98 | if credential.CreateTime == "" { 99 | credential.CreateTime = "0" 100 | } 101 | 102 | return &credential, nil 103 | } 104 | 105 | func parseOldCredential(data []byte, keyfile string) (*Credentials, error) { 106 | oldCred, err := decodeOldCredential(data, keyfile) 107 | if err != nil { 108 | return nil, err 109 | } 110 | // build a new Credentials structure out of the old 111 | cred := Credential{ 112 | KeyId: oldCred.KeyId, 113 | SecretKey: oldCred.SecretKey, 114 | } 115 | enc := []Encryption{} 116 | enc = append(enc, Encryption{ 117 | decoded: cred, 118 | }) 119 | creds := Credentials{ 120 | Version: "noversion", 121 | IamUsername: oldCred.IamUsername, 122 | AccountAliasOrId: oldCred.AccountAliasOrId, 123 | CreateTime: oldCred.CreateTime, 124 | LifeTime: oldCred.LifeTime, 125 | Encryptions: enc, 126 | } 127 | 128 | return &creds, nil 129 | } 130 | 131 | func parseCredential(data []byte, keyfile string) (*Credentials, error) { 132 | var creds Credentials 133 | err := json.Unmarshal(data, &creds) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | privKey, err := loadPrivateKey(keyfile) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | fp, err := SSHPrivateFingerprint(*privKey) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | var offset int = -1 149 | for i, enc := range creds.Encryptions { 150 | if enc.Fingerprint == fp { 151 | offset = i 152 | break 153 | } 154 | } 155 | 156 | if offset < 0 { 157 | err := errors.New("The SSH key specified cannot decrypt those credentials") 158 | return nil, err 159 | } 160 | 161 | var tmp string 162 | switch { 163 | case creds.Version == "2014-05-31": 164 | log.Print("INFO: These credentials are in the old format; re-run 'credulous save' now to remove this warning") 165 | tmp, err = CredulousDecodePureRSA(creds.Encryptions[offset].Ciphertext, privKey) 166 | case creds.Version == "2014-06-12": 167 | tmp, err = CredulousDecodeAES(creds.Encryptions[offset].Ciphertext, privKey) 168 | } 169 | 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | var cred Credential 175 | err = json.Unmarshal([]byte(tmp), &cred) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | creds.Encryptions[0].decoded = cred 181 | return &creds, nil 182 | } 183 | 184 | func readCredentialFile(fileName string, keyfile string) (*Credentials, error) { 185 | b, err := ioutil.ReadFile(fileName) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | if !strings.Contains(string(b), "Version") { 191 | log.Print("INFO: These credentials are in the old format; re-run 'credulous save' now to remove this warning") 192 | creds, err := parseOldCredential(b, keyfile) 193 | if err != nil { 194 | return nil, err 195 | } 196 | return creds, nil 197 | } 198 | 199 | creds, err := parseCredential(b, keyfile) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | return creds, nil 205 | } 206 | 207 | func (cred Credentials) WriteToDisk(repo, filename string) (err error) { 208 | b, err := json.Marshal(cred) 209 | if err != nil { 210 | return err 211 | } 212 | path := filepath.Join(repo, cred.AccountAliasOrId, cred.IamUsername) 213 | os.MkdirAll(path, 0700) 214 | err = ioutil.WriteFile(filepath.Join(path, filename), b, 0600) 215 | if err != nil { 216 | return err 217 | } 218 | isrepo, err := isGitRepo(repo) 219 | if err != nil { 220 | return err 221 | } 222 | if !isrepo { 223 | return nil 224 | } 225 | relpath := filepath.Join(cred.AccountAliasOrId, cred.IamUsername, filename) 226 | _, err = gitAddCommitFile(repo, relpath, "Added by Credulous") 227 | if err != nil { 228 | return err 229 | } 230 | return nil 231 | } 232 | 233 | func (cred OldCredential) Display(output io.Writer) { 234 | fmt.Fprintf(output, "export AWS_ACCESS_KEY_ID=\"%v\"\nexport AWS_SECRET_ACCESS_KEY=\"%v\"\n", cred.KeyId, cred.SecretKey) 235 | } 236 | 237 | func (cred Credentials) Display(output io.Writer) { 238 | fmt.Fprintf(output, "export AWS_ACCESS_KEY_ID=\"%v\"\nexport AWS_SECRET_ACCESS_KEY=\"%v\"\n", 239 | cred.Encryptions[0].decoded.KeyId, cred.Encryptions[0].decoded.SecretKey) 240 | for key, val := range cred.Encryptions[0].decoded.EnvVars { 241 | fmt.Fprintf(output, "export %s=\"%s\"\n", key, val) 242 | } 243 | } 244 | 245 | func (creds Credentials) verifyUserAndAccount() error { 246 | // need to check both the username and the account alias for the 247 | // supplied creds match the passed-in username and account alias 248 | auth := aws.Auth{ 249 | AccessKey: creds.Encryptions[0].decoded.KeyId, 250 | SecretKey: creds.Encryptions[0].decoded.SecretKey, 251 | } 252 | // Note: the region is irrelevant for IAM 253 | instance := iam.New(auth, aws.APSoutheast2) 254 | 255 | // Make sure the account is who we expect 256 | err := verify_account(creds.AccountAliasOrId, instance) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | // Make sure the user is who we expect 262 | // If the username is the same as the account name, then it's the root user 263 | // and there's actually no username at all (oddly) 264 | if creds.IamUsername == creds.AccountAliasOrId { 265 | err = verify_user("", instance) 266 | } else { 267 | err = verify_user(creds.IamUsername, instance) 268 | } 269 | if err != nil { 270 | return err 271 | } 272 | 273 | return nil 274 | } 275 | 276 | // Only delete the oldest key *if* the new key is valid; otherwise, 277 | // delete the newest key 278 | func (cred *Credential) deleteOneKey(username string) (err error) { 279 | auth := aws.Auth{ 280 | AccessKey: cred.KeyId, 281 | SecretKey: cred.SecretKey, 282 | } 283 | instance := iam.New(auth, aws.APSoutheast2) 284 | 285 | allKeys, err := instance.AccessKeys(username) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | // wtf? 291 | if len(allKeys.AccessKeys) == 0 { 292 | err = errors.New("Zero access keys found for this account -- cannot rotate") 293 | return err 294 | } 295 | 296 | // only one key 297 | if len(allKeys.AccessKeys) == 1 { 298 | return nil 299 | } 300 | 301 | // Find out which key to delete. 302 | var oldestId string 303 | var oldest int64 304 | 305 | for _, key := range allKeys.AccessKeys { 306 | t, err := time.Parse("2006-01-02T15:04:05Z", key.CreateDate) 307 | key_create_date := t.Unix() 308 | if err != nil { 309 | return err 310 | } 311 | // If we find an inactive one, just delete it 312 | if key.Status == "Inactive" { 313 | oldestId = key.Id 314 | break 315 | } 316 | if oldest == 0 || key_create_date < oldest { 317 | oldest = key_create_date 318 | oldestId = key.Id 319 | } 320 | } 321 | 322 | if oldestId == "" { 323 | err = errors.New("Cannot find oldest key for this account, will not rotate") 324 | return err 325 | } 326 | 327 | _, err = instance.DeleteAccessKey(oldestId, username) 328 | if err != nil { 329 | return err 330 | } 331 | 332 | return nil 333 | } 334 | 335 | func (cred *Credential) createNewAccessKey(username string) (err error) { 336 | auth := aws.Auth{ 337 | AccessKey: cred.KeyId, 338 | SecretKey: cred.SecretKey, 339 | } 340 | instance := iam.New(auth, aws.APSoutheast2) 341 | 342 | resp, err := instance.CreateAccessKey(username) 343 | if err != nil { 344 | return err 345 | } 346 | 347 | cred.KeyId = resp.AccessKey.Id 348 | cred.SecretKey = resp.AccessKey.Secret 349 | return nil 350 | } 351 | 352 | // Potential conditions to handle here: 353 | // * AWS has one key 354 | // * only generate a new key, do not delete the old one 355 | // * AWS has two keys 356 | // * both are active and valid 357 | // * new one is inactive 358 | // * old one is inactive 359 | // * We successfully delete the oldest key, but fail in creating the new key (eg network, permission issues) 360 | func (cred *Credential) rotateCredentials(username string) (err error) { 361 | err = cred.deleteOneKey(username) 362 | if err != nil { 363 | return err 364 | } 365 | err = cred.createNewAccessKey(username) 366 | if err != nil { 367 | return err 368 | } 369 | // Loop until the credentials are active 370 | count := 0 371 | for _, _, err = getAWSUsernameAndAlias(*cred); err != nil && count < ROTATE_TIMEOUT; _, _, err = getAWSUsernameAndAlias(*cred) { 372 | time.Sleep(1 * time.Second) 373 | count += 1 374 | } 375 | if err != nil { 376 | err = errors.New("Timed out waiting for new credentials to become active") 377 | return err 378 | } 379 | return nil 380 | } 381 | 382 | func SaveCredentials(data SaveData) (err error) { 383 | 384 | var key_create_date int64 385 | 386 | if data.force { 387 | key_create_date = time.Now().Unix() 388 | } else { 389 | auth := aws.Auth{AccessKey: data.cred.KeyId, SecretKey: data.cred.SecretKey} 390 | instance := iam.New(auth, aws.APSoutheast2) 391 | if data.username == "" { 392 | data.username, err = getAWSUsername(instance) 393 | if err != nil { 394 | return err 395 | } 396 | } 397 | if data.alias == "" { 398 | data.alias, err = getAWSAccountAlias(instance) 399 | if err != nil { 400 | return err 401 | } 402 | } 403 | 404 | date, _ := getKeyCreateDate(instance) 405 | t, err := time.Parse("2006-01-02T15:04:05Z", date) 406 | key_create_date = t.Unix() 407 | if err != nil { 408 | return err 409 | } 410 | } 411 | 412 | fmt.Printf("saving credentials for %s@%s\n", data.username, data.alias) 413 | plaintext, err := json.Marshal(data.cred) 414 | if err != nil { 415 | return err 416 | } 417 | 418 | enc_slice := []Encryption{} 419 | for _, pubkey := range data.pubkeys { 420 | encoded, err := CredulousEncode(string(plaintext), pubkey) 421 | if err != nil { 422 | return err 423 | } 424 | 425 | enc_slice = append(enc_slice, Encryption{ 426 | Ciphertext: encoded, 427 | Fingerprint: SSHFingerprint(pubkey), 428 | }) 429 | } 430 | creds := Credentials{ 431 | Version: FORMAT_VERSION, 432 | AccountAliasOrId: data.alias, 433 | IamUsername: data.username, 434 | CreateTime: fmt.Sprintf("%d", key_create_date), 435 | Encryptions: enc_slice, 436 | LifeTime: data.lifetime, 437 | } 438 | 439 | filename := fmt.Sprintf("%v-%v.json", key_create_date, data.cred.KeyId[12:]) 440 | err = creds.WriteToDisk(data.repo, filename) 441 | return err 442 | } 443 | 444 | type FileLister interface { 445 | Readdir(int) ([]os.FileInfo, error) 446 | Name() string 447 | } 448 | 449 | func getDirs(fl FileLister) ([]os.FileInfo, error) { 450 | dirents, err := fl.Readdir(0) // get all the entries 451 | if err != nil { 452 | return nil, err 453 | } 454 | 455 | dirs := []os.FileInfo{} 456 | for _, dirent := range dirents { 457 | if dirent.IsDir() { 458 | dirs = append(dirs, dirent) 459 | } 460 | } 461 | 462 | return dirs, nil 463 | } 464 | 465 | func findDefaultDir(fl FileLister) (string, error) { 466 | dirs, err := getDirs(fl) 467 | if err != nil { 468 | return "", err 469 | } 470 | 471 | switch { 472 | case len(dirs) == 0: 473 | return "", errors.New("No saved credentials found; please run 'credulous save' first") 474 | case len(dirs) > 1: 475 | return "", errors.New("More than one account found; please specify account and user") 476 | } 477 | 478 | return dirs[0].Name(), nil 479 | } 480 | 481 | func (cred Credentials) ValidateCredentials(alias string, username string) error { 482 | if cred.IamUsername != username { 483 | err := errors.New("FATAL: username in credential does not match requested username") 484 | return err 485 | } 486 | if cred.AccountAliasOrId != alias { 487 | err := errors.New("FATAL: account alias in credential does not match requested alias") 488 | return err 489 | } 490 | 491 | err := cred.verifyUserAndAccount() 492 | if err != nil { 493 | return err 494 | } 495 | return nil 496 | } 497 | 498 | func RetrieveCredentials(rootPath string, alias string, username string, keyfile string) (Credentials, error) { 499 | rootDir, err := os.Open(rootPath) 500 | if err != nil { 501 | panic_the_err(err) 502 | } 503 | 504 | if alias == "" { 505 | if alias, err = findDefaultDir(rootDir); err != nil { 506 | panic_the_err(err) 507 | } 508 | } 509 | 510 | if username == "" { 511 | aliasDir, err := os.Open(filepath.Join(rootPath, alias)) 512 | if err != nil { 513 | panic_the_err(err) 514 | } 515 | username, err = findDefaultDir(aliasDir) 516 | if err != nil { 517 | panic_the_err(err) 518 | } 519 | } 520 | 521 | fullPath := filepath.Join(rootPath, alias, username) 522 | latest, err := latestFileInDir(fullPath) 523 | if err != nil { 524 | return Credentials{}, err 525 | } 526 | filePath := filepath.Join(fullPath, latest.Name()) 527 | cred, err := readCredentialFile(filePath, keyfile) 528 | if err != nil { 529 | return Credentials{}, err 530 | } 531 | 532 | return *cred, nil 533 | } 534 | 535 | func latestFileInDir(dir string) (os.FileInfo, error) { 536 | entries, err := ioutil.ReadDir(dir) 537 | panic_the_err(err) 538 | if len(entries) == 0 { 539 | return nil, errors.New("No credentials have been saved for that user and account; please run 'credulous save' first") 540 | } 541 | return entries[len(entries)-1], nil 542 | } 543 | 544 | func listAvailableCredentials(rootDir FileLister) ([]string, error) { 545 | creds := make(map[string]int) 546 | 547 | repo_dirs, err := getDirs(rootDir) // get just the directories 548 | if err != nil { 549 | return []string{}, err 550 | } 551 | 552 | if len(repo_dirs) == 0 { 553 | return []string{}, errors.New("No saved credentials found; please run 'credulous save' first") 554 | } 555 | 556 | for _, repo_dirent := range repo_dirs { 557 | repo_path := filepath.Join(rootDir.Name(), repo_dirent.Name()) 558 | repo_dir, err := os.Open(repo_path) 559 | if err != nil { 560 | return []string{}, err 561 | } 562 | 563 | alias_dirs, err := getDirs(repo_dir) 564 | if err != nil { 565 | return []string{}, err 566 | } 567 | 568 | for _, alias_dirent := range alias_dirs { 569 | if alias_dirent.Name() == ".git" { 570 | continue 571 | } 572 | alias_path := filepath.Join(repo_path, alias_dirent.Name()) 573 | alias_dir, err := os.Open(alias_path) 574 | if err != nil { 575 | return []string{}, err 576 | } 577 | 578 | user_dirs, err := getDirs(alias_dir) 579 | if err != nil { 580 | return []string{}, err 581 | } 582 | 583 | for _, user_dirent := range user_dirs { 584 | user_path := filepath.Join(alias_path, user_dirent.Name()) 585 | latest, err := latestFileInDir(user_path) 586 | if err != nil { 587 | return []string{}, err 588 | } 589 | if latest.Name() != "" { 590 | creds[user_dirent.Name()+"@"+alias_dirent.Name()] += 1 591 | } 592 | } 593 | } 594 | } 595 | 596 | names := make([]string, len(creds)) 597 | i := 0 598 | for k, _ := range creds { 599 | names[i] = k 600 | i++ 601 | } 602 | sort.Strings(names) 603 | return names, nil 604 | } 605 | -------------------------------------------------------------------------------- /credentials_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | . "github.com/smartystreets/goconvey/convey" 8 | // "io/ioutil" 9 | // "fmt" 10 | ) 11 | 12 | type TestWriter struct { 13 | Written []byte 14 | } 15 | 16 | func (t *TestWriter) Write(p []byte) (n int, err error) { 17 | t.Written = p 18 | return 0, nil 19 | } 20 | 21 | type TestFileList struct { 22 | testList []os.FileInfo 23 | } 24 | 25 | func (t *TestFileList) Readdir(n int) ([]os.FileInfo, error) { 26 | return t.testList, nil 27 | } 28 | 29 | func (t *TestFileList) Name() string { 30 | return "foo" 31 | } 32 | 33 | type TestFileInfo struct { 34 | isDir bool 35 | name string 36 | } 37 | 38 | func (t *TestFileInfo) IsDir() bool { 39 | return t.isDir 40 | } 41 | 42 | func (t *TestFileInfo) Name() string { 43 | return t.name 44 | } 45 | 46 | func (t *TestFileInfo) Size() int64 { 47 | return 0 48 | } 49 | 50 | func (t *TestFileInfo) Mode() os.FileMode { 51 | return 0 52 | } 53 | 54 | func (t *TestFileInfo) ModTime() time.Time { 55 | return time.Now() 56 | } 57 | 58 | func (t *TestFileInfo) Sys() interface{} { 59 | return nil 60 | } 61 | 62 | func TestGetDirs(t *testing.T) { 63 | Convey("Test finding all dirs", t, func() { 64 | Convey("Test with nothing", func() { 65 | t := TestFileList{} 66 | ents, err := getDirs(&t) 67 | So(err, ShouldEqual, nil) 68 | So(len(ents), ShouldEqual, 0) 69 | }) 70 | Convey("Test with files only", func() { 71 | i := []os.FileInfo{} 72 | i = append(i, &TestFileInfo{isDir: false}) 73 | i = append(i, &TestFileInfo{isDir: false}) 74 | i = append(i, &TestFileInfo{isDir: false}) 75 | t := TestFileList{testList: i} 76 | ents, err := getDirs(&t) 77 | So(err, ShouldEqual, nil) 78 | So(len(ents), ShouldEqual, 0) 79 | }) 80 | Convey("Test with one dir", func() { 81 | i := []os.FileInfo{} 82 | i = append(i, &TestFileInfo{isDir: true}) 83 | t := TestFileList{testList: i} 84 | ents, err := getDirs(&t) 85 | So(err, ShouldEqual, nil) 86 | So(len(ents), ShouldEqual, 1) 87 | }) 88 | Convey("Test with multiple dirs", func() { 89 | i := []os.FileInfo{} 90 | i = append(i, &TestFileInfo{isDir: true}) 91 | i = append(i, &TestFileInfo{isDir: true}) 92 | i = append(i, &TestFileInfo{isDir: true}) 93 | i = append(i, &TestFileInfo{isDir: true}) 94 | t := TestFileList{testList: i} 95 | ents, err := getDirs(&t) 96 | So(err, ShouldEqual, nil) 97 | So(len(ents), ShouldEqual, 4) 98 | }) 99 | }) 100 | } 101 | 102 | func TestFindDefaultDir(t *testing.T) { 103 | Convey("Test Finding Default Dirs", t, func() { 104 | Convey("With no files or directories", func() { 105 | t := TestFileList{} 106 | _, err := findDefaultDir(&t) 107 | So(err.Error(), ShouldEqual, "No saved credentials found; please run 'credulous save' first") 108 | }) 109 | Convey("With one file and no directories", func() { 110 | i := []os.FileInfo{} 111 | i = append(i, &TestFileInfo{isDir: false}) 112 | t := TestFileList{testList: i} 113 | _, err := findDefaultDir(&t) 114 | So(err, ShouldNotEqual, nil) 115 | So(err.Error(), ShouldEqual, "No saved credentials found; please run 'credulous save' first") 116 | }) 117 | Convey("With one file and one directory", func() { 118 | i := []os.FileInfo{} 119 | i = append(i, &TestFileInfo{isDir: false}) 120 | i = append(i, &TestFileInfo{isDir: true, name: "foo"}) 121 | t := TestFileList{testList: i} 122 | name, err := findDefaultDir(&t) 123 | So(err, ShouldEqual, nil) 124 | So(name, ShouldEqual, "foo") 125 | }) 126 | Convey("With no files and more than one directory", func() { 127 | i := []os.FileInfo{} 128 | i = append(i, &TestFileInfo{isDir: true, name: "foo"}) 129 | i = append(i, &TestFileInfo{isDir: true, name: "bar"}) 130 | i = append(i, &TestFileInfo{isDir: true, name: "baz"}) 131 | t := TestFileList{testList: i} 132 | _, err := findDefaultDir(&t) 133 | So(err, ShouldNotEqual, nil) 134 | So(err.Error(), ShouldEqual, "More than one account found; please specify account and user") 135 | }) 136 | }) 137 | } 138 | 139 | func TestValidateCredentials(t *testing.T) { 140 | Convey("Test credential validation", t, func() { 141 | // we can't really test ValidateCredentials directly, 142 | // because it calls verifyUserAndAccount, which 143 | // creates its own IAM connection. This is probably not 144 | // the best way to have implemented that function. 145 | // goamz provides an iamtest package, and we should 146 | // use that. 147 | }) 148 | } 149 | 150 | func TestReadFile(t *testing.T) { 151 | Convey("Test Read File", t, func() { 152 | Convey("Valid old Json returns Credential", func() { 153 | cred, _ := readCredentialFile("testdata/credential.json", "testdata/testkey") 154 | So(cred.LifeTime, ShouldEqual, 22) 155 | So(cred.Encryptions[0].decoded.KeyId, ShouldEqual, "some plaintext") 156 | }) 157 | Convey("Old credentials display correctly", func() { 158 | cred, _ := readCredentialFile("testdata/credential.json", "testdata/testkey") 159 | testWriter := TestWriter{} 160 | cred.Display(&testWriter) 161 | So(string(testWriter.Written), ShouldEqual, "export AWS_ACCESS_KEY_ID=\"some plaintext\"\nexport AWS_SECRET_ACCESS_KEY=\"some plaintext\"\n") 162 | }) 163 | 164 | Convey("Valid new Json returns Credentials", func() { 165 | cred, err := readCredentialFile("testdata/newcreds.json", "testdata/testkey") 166 | So(err, ShouldEqual, nil) 167 | So(cred.LifeTime, ShouldEqual, 0) 168 | So(cred.CreateTime, ShouldEqual, "1401515273") 169 | So(cred.Encryptions[0].Fingerprint, ShouldEqual, "c0:61:84:fc:e8:c9:52:dc:cd:a9:8e:82:a2:70:0a:30") 170 | So(cred.Encryptions[0].decoded.KeyId, ShouldEqual, "plaintextkeyid") 171 | So(cred.Encryptions[0].decoded.SecretKey, ShouldEqual, "plaintextsecret") 172 | }) 173 | Convey("New credentials display correctly", func() { 174 | cred, err := readCredentialFile("testdata/newcreds.json", "testdata/testkey") 175 | testWriter := TestWriter{} 176 | cred.Display(&testWriter) 177 | So(string(testWriter.Written), ShouldEqual, "export AWS_ACCESS_KEY_ID=\"plaintextkeyid\"\nexport AWS_SECRET_ACCESS_KEY=\"plaintextsecret\"\n") 178 | So(err, ShouldEqual, nil) 179 | }) 180 | }) 181 | } 182 | 183 | func TestListAvailableCreds(t *testing.T) { 184 | Convey("Test listing available credentials", t, func() { 185 | Convey("Test with no credentials", func() { 186 | tmp := TestFileList{} 187 | creds, err := listAvailableCredentials(&tmp) 188 | So(len(creds), ShouldEqual, 0) 189 | So(err.Error(), ShouldEqual, "No saved credentials found; please run 'credulous save' first") 190 | }) 191 | }) 192 | } 193 | -------------------------------------------------------------------------------- /credulous.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | 16 | "code.google.com/p/go.crypto/ssh" 17 | 18 | "code.google.com/p/gopass" 19 | "github.com/codegangsta/cli" 20 | ) 21 | 22 | const ENV_PATTERN string = "^[A-Za-z_][A-Za-z0-9_]*=.*" 23 | 24 | func decryptPEM(pemblock *pem.Block, filename string) ([]byte, error) { 25 | var err error 26 | if _, err = fmt.Fprintf(os.Stderr, "Enter passphrase for %s: ", filename); err != nil { 27 | return []byte(""), err 28 | } 29 | 30 | // we already emit the prompt to stderr; GetPass only emits to stdout 31 | var passwd string 32 | passwd, err = gopass.GetPass("") 33 | fmt.Fprintln(os.Stderr, "") 34 | if err != nil { 35 | return []byte(""), err 36 | } 37 | 38 | var decryptedBytes []byte 39 | if decryptedBytes, err = x509.DecryptPEMBlock(pemblock, []byte(passwd)); err != nil { 40 | return []byte(""), err 41 | } 42 | 43 | pemBytes := pem.Block{ 44 | Type: "RSA PRIVATE KEY", 45 | Bytes: decryptedBytes, 46 | } 47 | decryptedPEM := pem.EncodeToMemory(&pemBytes) 48 | return decryptedPEM, nil 49 | } 50 | 51 | func getPrivateKey(c *cli.Context) (filename string) { 52 | if c.String("key") == "" { 53 | filename = filepath.Join(os.Getenv("HOME"), "/.ssh/id_rsa") 54 | } else { 55 | filename = c.String("key") 56 | } 57 | return filename 58 | } 59 | 60 | func splitUserAndAccount(arg string) (string, string, error) { 61 | atpos := strings.LastIndex(arg, "@") 62 | if atpos < 1 { 63 | err := errors.New("Invalid account format; please specify @") 64 | return "", "", err 65 | } 66 | // pull off everything before the last '@' 67 | return arg[atpos+1:], arg[0:atpos], nil 68 | } 69 | 70 | func getAccountAndUserName(c *cli.Context) (string, string, error) { 71 | if len(c.Args()) > 0 { 72 | user, acct, err := splitUserAndAccount(c.Args()[0]) 73 | if err != nil { 74 | return "", "", err 75 | } 76 | return user, acct, nil 77 | } 78 | if c.String("credentials") != "" { 79 | user, acct, err := splitUserAndAccount(c.String("credentials")) 80 | if err != nil { 81 | return "", "", err 82 | } 83 | return user, acct, nil 84 | } else { 85 | return c.String("account"), c.String("username"), nil 86 | } 87 | } 88 | 89 | func parseUserAndAccount(c *cli.Context) (username string, account string, err error) { 90 | if (c.String("username") == "" || c.String("account") == "") && c.Bool("force") { 91 | err = errors.New("Must specify both username and account with force") 92 | return "", "", err 93 | } 94 | 95 | // if username OR account were specified, but not both, complain 96 | if (c.String("username") != "" && c.String("account") == "") || 97 | (c.String("username") == "" && c.String("account") != "") { 98 | if c.Bool("force") { 99 | err = errors.New("Must specify both username and account for force save") 100 | } else { 101 | err = errors.New("Must use force save when specifying username or account") 102 | } 103 | return "", "", err 104 | } 105 | 106 | // if username/account were specified, but force wasn't set, complain 107 | if c.String("username") != "" && c.String("account") != "" { 108 | if !c.Bool("force") { 109 | err = errors.New("Cannot specify username and/or account without force") 110 | return "", "", err 111 | } else { 112 | log.Print("WARNING: saving credentials without verifying username or account alias") 113 | username = c.String("username") 114 | account = c.String("account") 115 | } 116 | } 117 | return username, account, nil 118 | } 119 | 120 | func parseEnvironmentArgs(c *cli.Context) (map[string]string, error) { 121 | if len(c.StringSlice("env")) == 0 { 122 | return nil, nil 123 | } 124 | 125 | envMap := make(map[string]string) 126 | for _, arg := range c.StringSlice("env") { 127 | match, err := regexp.Match(ENV_PATTERN, []byte(arg)) 128 | if err != nil { 129 | return nil, err 130 | } 131 | if !match { 132 | log.Print("WARNING: Skipping env argument " + arg + " -- not in NAME=value format") 133 | continue 134 | } 135 | parts := strings.SplitN(arg, "=", 2) 136 | envMap[parts[0]] = parts[1] 137 | } 138 | return envMap, nil 139 | } 140 | 141 | func readSSHPubkeyFile(filename string) (pubkey ssh.PublicKey, err error) { 142 | pubkeyString, err := ioutil.ReadFile(filename) 143 | if err != nil { 144 | return nil, err 145 | } 146 | pubkey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(pubkeyString)) 147 | if err != nil { 148 | return nil, err 149 | } 150 | return pubkey, nil 151 | } 152 | 153 | func parseKeyArgs(c *cli.Context) (pubkeys []ssh.PublicKey, err error) { 154 | // no args, so just use the default 155 | if len(c.StringSlice("key")) == 0 { 156 | pubkey, err := readSSHPubkeyFile(filepath.Join(os.Getenv("HOME"), "/.ssh/id_rsa.pub")) 157 | if err != nil { 158 | return nil, err 159 | } 160 | pubkeys = append(pubkeys, pubkey) 161 | return pubkeys, nil 162 | } 163 | 164 | for _, arg := range c.StringSlice("key") { 165 | pubkey, err := readSSHPubkeyFile(arg) 166 | if err != nil { 167 | return nil, err 168 | } 169 | pubkeys = append(pubkeys, pubkey) 170 | } 171 | return pubkeys, nil 172 | } 173 | 174 | // parseLifetimeArgs attempts to be a little clever in determining what credential 175 | // lifetime you've chosen. It returns a number of hours and an error. It assumes that 176 | // the argument was passed in as hours. 177 | func parseLifetimeArgs(c *cli.Context) (lifetime int, err error) { 178 | // the default is zero, which is our default 179 | if c.Int("lifetime") < 0 { 180 | return 0, nil 181 | } 182 | 183 | return c.Int("lifetime"), nil 184 | } 185 | 186 | func parseRepoArgs(c *cli.Context) (repo string, err error) { 187 | // the default is 'local' which is set below, so not much to do here 188 | if c.String("repo") == "local" { 189 | repo = path.Join(getRootPath(), "local") 190 | } else { 191 | repo = c.String("repo") 192 | } 193 | return repo, nil 194 | } 195 | 196 | func parseSaveArgs(c *cli.Context) (cred Credential, username, account string, pubkeys []ssh.PublicKey, lifetime int, repo string, err error) { 197 | pubkeys, err = parseKeyArgs(c) 198 | if err != nil { 199 | return Credential{}, "", "", nil, 0, "", err 200 | } 201 | 202 | username, account, err = parseUserAndAccount(c) 203 | if err != nil { 204 | return Credential{}, "", "", nil, 0, "", err 205 | } 206 | 207 | envmap, err := parseEnvironmentArgs(c) 208 | if err != nil { 209 | return Credential{}, "", "", nil, 0, "", err 210 | } 211 | 212 | lifetime, err = parseLifetimeArgs(c) 213 | if err != nil { 214 | return Credential{}, "", "", nil, 0, "", err 215 | } 216 | 217 | repo, err = parseRepoArgs(c) 218 | if err != nil { 219 | return Credential{}, "", "", nil, 0, "", err 220 | } 221 | 222 | AWSAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID") 223 | AWSSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") 224 | if AWSAccessKeyId == "" || AWSSecretAccessKey == "" { 225 | err := errors.New("Can't save, no credentials in the environment") 226 | if err != nil { 227 | return Credential{}, "", "", nil, 0, "", err 228 | } 229 | } 230 | cred = Credential{ 231 | KeyId: AWSAccessKeyId, 232 | SecretKey: AWSSecretAccessKey, 233 | EnvVars: envmap, 234 | } 235 | 236 | return cred, username, account, pubkeys, lifetime, repo, nil 237 | } 238 | 239 | func main() { 240 | app := cli.NewApp() 241 | app.Name = "credulous" 242 | app.Usage = "Secure AWS Credential Management" 243 | app.Version = "0.2.2" 244 | 245 | app.Commands = []cli.Command{ 246 | { 247 | Name: "save", 248 | Usage: "Save AWS credentials", 249 | Flags: []cli.Flag{ 250 | cli.StringSliceFlag{ 251 | Name: "key, k", 252 | Value: &cli.StringSlice{}, 253 | Usage: "\n SSH public keys for encryption", 254 | }, 255 | cli.StringSliceFlag{ 256 | Name: "env, e", 257 | Value: &cli.StringSlice{}, 258 | Usage: "\n Environment variables to set in the form VAR=value", 259 | }, 260 | cli.IntFlag{ 261 | Name: "lifetime, l", 262 | Value: 0, 263 | Usage: "\n Credential lifetime in seconds (0 means forever)", 264 | }, 265 | cli.BoolFlag{ 266 | Name: "force, f", 267 | Usage: "\n Force saving without validating username or account." + 268 | "\n You MUST specify -u username -a account", 269 | }, 270 | cli.StringFlag{ 271 | Name: "username, u", 272 | Value: "", 273 | Usage: "\n Username (for use with '--force')", 274 | }, 275 | cli.StringFlag{ 276 | Name: "account, a", 277 | Value: "", 278 | Usage: "\n Account alias (for use with '--force')", 279 | }, 280 | cli.StringFlag{ 281 | Name: "repo, r", 282 | Value: "local", 283 | Usage: "\n Repository location ('local' by default)", 284 | }, 285 | }, 286 | Action: func(c *cli.Context) { 287 | cred, username, account, pubkeys, lifetime, repo, err := parseSaveArgs(c) 288 | panic_the_err(err) 289 | err = SaveCredentials(SaveData{ 290 | cred: cred, 291 | username: username, 292 | alias: account, 293 | pubkeys: pubkeys, 294 | lifetime: lifetime, 295 | force: c.Bool("force"), 296 | repo: repo, 297 | }) 298 | panic_the_err(err) 299 | }, 300 | }, 301 | 302 | { 303 | Name: "source", 304 | Usage: "Source AWS credentials", 305 | Flags: []cli.Flag{ 306 | cli.StringFlag{ 307 | Name: "account, a", 308 | Value: "", 309 | Usage: "\n AWS Account alias or id", 310 | }, 311 | cli.StringFlag{ 312 | Name: "key, k", 313 | Value: "", 314 | Usage: "\n SSH private key", 315 | }, 316 | cli.StringFlag{ 317 | Name: "username, u", 318 | Value: "", 319 | Usage: "\n IAM User", 320 | }, 321 | cli.StringFlag{ 322 | Name: "credentials, c", 323 | Value: "", 324 | Usage: "\n Credentials, for example username@account", 325 | }, 326 | cli.BoolFlag{ 327 | Name: "force, f", 328 | Usage: "\n Force sourcing of credentials without validating username or account", 329 | }, 330 | cli.StringFlag{ 331 | Name: "repo, r", 332 | Value: "local", 333 | Usage: "\n Repository location ('local' by default)", 334 | }, 335 | }, 336 | Action: func(c *cli.Context) { 337 | keyfile := getPrivateKey(c) 338 | account, username, err := getAccountAndUserName(c) 339 | if err != nil { 340 | panic_the_err(err) 341 | } 342 | repo, err := parseRepoArgs(c) 343 | if err != nil { 344 | panic_the_err(err) 345 | } 346 | creds, err := RetrieveCredentials(repo, account, username, keyfile) 347 | if err != nil { 348 | panic_the_err(err) 349 | } 350 | 351 | if !c.Bool("force") { 352 | err = creds.ValidateCredentials(account, username) 353 | if err != nil { 354 | panic_the_err(err) 355 | } 356 | } 357 | creds.Display(os.Stdout) 358 | }, 359 | }, 360 | 361 | { 362 | Name: "current", 363 | Usage: "Show the username and alias of the currently-loaded credentials", 364 | Action: func(c *cli.Context) { 365 | AWSAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID") 366 | AWSSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") 367 | if AWSAccessKeyId == "" || AWSSecretAccessKey == "" { 368 | err := errors.New("No amazon credentials are currently in your environment") 369 | panic_the_err(err) 370 | } 371 | cred := Credential{ 372 | KeyId: AWSAccessKeyId, 373 | SecretKey: AWSSecretAccessKey, 374 | } 375 | username, alias, err := getAWSUsernameAndAlias(cred) 376 | if err != nil { 377 | panic_the_err(err) 378 | } 379 | fmt.Printf("%s@%s\n", username, alias) 380 | }, 381 | }, 382 | 383 | { 384 | Name: "display", 385 | Usage: "Display loaded AWS credentials", 386 | Action: func(c *cli.Context) { 387 | AWSAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID") 388 | AWSSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") 389 | fmt.Printf("AWS_ACCESS_KEY_ID: %s\n", AWSAccessKeyId) 390 | fmt.Printf("AWS_SECRET_ACCESS_KEY: %s\n", AWSSecretAccessKey) 391 | }, 392 | }, 393 | 394 | { 395 | Name: "list", 396 | Usage: "List available AWS credentials", 397 | Action: func(c *cli.Context) { 398 | rootDir, err := os.Open(getRootPath()) 399 | if err != nil { 400 | panic_the_err(err) 401 | } 402 | set, err := listAvailableCredentials(rootDir) 403 | if err != nil { 404 | panic_the_err(err) 405 | } 406 | for _, cred := range set { 407 | fmt.Println(cred) 408 | } 409 | }, 410 | }, 411 | 412 | { 413 | Name: "rotate", 414 | Usage: "Rotate current AWS credentials, deleting the oldest", 415 | Flags: []cli.Flag{ 416 | cli.IntFlag{ 417 | Name: "lifetime, l", 418 | Value: 0, 419 | Usage: "\n New credential lifetime in seconds (0 means forever)", 420 | }, 421 | cli.StringSliceFlag{ 422 | Name: "key, k", 423 | Value: &cli.StringSlice{}, 424 | Usage: "\n SSH public keys for encryption", 425 | }, 426 | cli.StringSliceFlag{ 427 | Name: "env, e", 428 | Value: &cli.StringSlice{}, 429 | Usage: "\n Environment variables to set in the form VAR=value", 430 | }, 431 | cli.StringFlag{ 432 | Name: "repo, r", 433 | Value: "local", 434 | Usage: "\n Repository location ('local' by default)", 435 | }, 436 | }, 437 | Action: func(c *cli.Context) { 438 | cred, _, _, pubkeys, lifetime, repo, err := parseSaveArgs(c) 439 | panic_the_err(err) 440 | username, account, err := getAWSUsernameAndAlias(cred) 441 | panic_the_err(err) 442 | err = (&cred).rotateCredentials(username) 443 | panic_the_err(err) 444 | err = SaveCredentials(SaveData{ 445 | cred: cred, 446 | username: username, 447 | alias: account, 448 | pubkeys: pubkeys, 449 | lifetime: lifetime, 450 | force: c.Bool("force"), 451 | repo: repo, 452 | }) 453 | panic_the_err(err) 454 | }, 455 | }, 456 | } 457 | 458 | app.Run(os.Args) 459 | } 460 | 461 | func rotate(cred Credential) (err error) { 462 | return nil 463 | } 464 | -------------------------------------------------------------------------------- /credulous_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestHelloWorld(t *testing.T) { 9 | Convey("Testing", t, func() { 10 | So(1, ShouldEqual, 1) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/md5" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/sha1" 10 | "crypto/x509" 11 | "encoding/base64" 12 | "encoding/json" 13 | "encoding/pem" 14 | "fmt" 15 | "io" 16 | "io/ioutil" 17 | "log" 18 | "math/big" 19 | "reflect" 20 | "strings" 21 | 22 | "code.google.com/p/go.crypto/ssh" 23 | ) 24 | 25 | type Salter interface { 26 | GenerateSalt() (string, error) 27 | } 28 | 29 | type RandomSaltGenerator struct { 30 | } 31 | 32 | type StaticSaltGenerator struct { 33 | salt string 34 | } 35 | 36 | func (ssg *StaticSaltGenerator) GenerateSalt() (string, error) { 37 | return ssg.salt, nil 38 | } 39 | 40 | func (sg *RandomSaltGenerator) GenerateSalt() (string, error) { 41 | const SALT_LENGTH = 8 42 | b := make([]byte, SALT_LENGTH) 43 | _, err := rand.Read(b) 44 | encoder := base64.StdEncoding 45 | encoded := make([]byte, encoder.EncodedLen(len(b))) 46 | encoder.Encode(encoded, b) 47 | if err != nil { 48 | return "", err 49 | } 50 | return string(encoded), nil 51 | } 52 | 53 | func sshPubkeyToRsaPubkey(pubkey ssh.PublicKey) rsa.PublicKey { 54 | s := reflect.ValueOf(pubkey).Elem() 55 | rsaKey := rsa.PublicKey{ 56 | N: s.Field(0).Interface().(*big.Int), 57 | E: s.Field(1).Interface().(int), 58 | } 59 | return rsaKey 60 | } 61 | 62 | func rsaPubkeyToSSHPubkey(rsakey rsa.PublicKey) (sshkey ssh.PublicKey, err error) { 63 | sshkey, err = ssh.NewPublicKey(&rsakey) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return sshkey, nil 68 | } 69 | 70 | type AESEncryption struct { 71 | EncodedKey string 72 | Ciphertext string 73 | } 74 | 75 | func encodeAES(key []byte, plaintext string) (ciphertext string, err error) { 76 | cipherBlock, err := aes.NewCipher(key) 77 | if err != nil { 78 | return "", err 79 | } 80 | 81 | // We need an unique IV to go at the front of the ciphertext 82 | out := make([]byte, aes.BlockSize+len(plaintext)) 83 | iv := out[:aes.BlockSize] 84 | _, err = io.ReadFull(rand.Reader, iv) 85 | if err != nil { 86 | return "", err 87 | } 88 | 89 | stream := cipher.NewCFBEncrypter(cipherBlock, iv) 90 | stream.XORKeyStream(out[aes.BlockSize:], []byte(plaintext)) 91 | encoded := base64.StdEncoding.EncodeToString(out) 92 | return encoded, nil 93 | } 94 | 95 | // takes a base64-encoded AES-encrypted ciphertext 96 | func decodeAES(key []byte, ciphertext string) (string, error) { 97 | encrypted, err := base64.StdEncoding.DecodeString(ciphertext) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | decrypter, err := aes.NewCipher(key) 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | iv := encrypted[:aes.BlockSize] 108 | msg := encrypted[aes.BlockSize:] 109 | aesDecrypter := cipher.NewCFBDecrypter(decrypter, iv) 110 | aesDecrypter.XORKeyStream(msg, msg) 111 | return string(msg), nil 112 | } 113 | 114 | // returns a base64 encoded ciphertext. 115 | // OAEP can only encrypt plaintexts that are smaller than the key length; for 116 | // a 1024-bit key, about 117 bytes. So instead, this function: 117 | // * generates a random 32-byte symmetric key (randKey) 118 | // * encrypts the plaintext with AES256 using that random symmetric key -> cipherText 119 | // * encrypts the random symmetric key with the ssh PublicKey -> cipherKey 120 | // * returns the base64-encoded marshalled JSON for the ciphertext and key 121 | func CredulousEncode(plaintext string, pubkey ssh.PublicKey) (ciphertext string, err error) { 122 | rsaKey := sshPubkeyToRsaPubkey(pubkey) 123 | randKey := make([]byte, 32) 124 | _, err = rand.Read(randKey) 125 | if err != nil { 126 | return "", err 127 | } 128 | 129 | encoded, err := encodeAES(randKey, plaintext) 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | out, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, &rsaKey, []byte(randKey), []byte("Credulous")) 135 | if err != nil { 136 | return "", err 137 | } 138 | cipherKey := base64.StdEncoding.EncodeToString(out) 139 | 140 | cipherStruct := AESEncryption{ 141 | EncodedKey: cipherKey, 142 | Ciphertext: encoded, 143 | } 144 | 145 | tmp, err := json.Marshal(cipherStruct) 146 | if err != nil { 147 | return "", err 148 | } 149 | 150 | ciphertext = base64.StdEncoding.EncodeToString(tmp) 151 | 152 | return ciphertext, nil 153 | } 154 | 155 | func CredulousDecodeAES(ciphertext string, privkey *rsa.PrivateKey) (plaintext string, err error) { 156 | in, err := base64.StdEncoding.DecodeString(ciphertext) 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | // pull apart the layers of base64-encoded JSON 162 | var encrypted AESEncryption 163 | err = json.Unmarshal(in, &encrypted) 164 | if err != nil { 165 | return "", err 166 | } 167 | 168 | encryptedKey, err := base64.StdEncoding.DecodeString(encrypted.EncodedKey) 169 | if err != nil { 170 | return "", err 171 | } 172 | 173 | // decrypt the AES key using the ssh private key 174 | aesKey, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, privkey, encryptedKey, []byte("Credulous")) 175 | if err != nil { 176 | return "", err 177 | } 178 | 179 | plaintext, err = decodeAES(aesKey, encrypted.Ciphertext) 180 | 181 | return plaintext, nil 182 | } 183 | 184 | func CredulousDecodePureRSA(ciphertext string, privkey *rsa.PrivateKey) (plaintext string, err error) { 185 | in, err := base64.StdEncoding.DecodeString(ciphertext) 186 | if err != nil { 187 | return "", err 188 | } 189 | out, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, privkey, in, []byte("Credulous")) 190 | if err != nil { 191 | return "", err 192 | } 193 | plaintext = string(out) 194 | return plaintext, nil 195 | } 196 | 197 | func CredulousDecodeWithSalt(ciphertext string, salt string, privkey *rsa.PrivateKey) (plaintext string, err error) { 198 | in, err := base64.StdEncoding.DecodeString(ciphertext) 199 | if err != nil { 200 | return "", err 201 | } 202 | out, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, privkey, in, []byte("Credulous")) 203 | if err != nil { 204 | return "", err 205 | } 206 | plaintext = strings.Replace(string(out), salt, "", 1) 207 | return plaintext, nil 208 | } 209 | 210 | func loadPrivateKey(filename string) (privateKey *rsa.PrivateKey, err error) { 211 | var tmp []byte 212 | 213 | if tmp, err = ioutil.ReadFile(filename); err != nil { 214 | return &rsa.PrivateKey{}, err 215 | } 216 | 217 | pemblock, _ := pem.Decode([]byte(tmp)) 218 | if x509.IsEncryptedPEMBlock(pemblock) { 219 | if tmp, err = decryptPEM(pemblock, filename); err != nil { 220 | return &rsa.PrivateKey{}, err 221 | } 222 | } else { 223 | log.Print("WARNING: Your private SSH key has no passphrase!") 224 | } 225 | 226 | key, err := ssh.ParseRawPrivateKey(tmp) 227 | if err != nil { 228 | return &rsa.PrivateKey{}, err 229 | } 230 | privateKey = key.(*rsa.PrivateKey) 231 | return privateKey, nil 232 | } 233 | 234 | func SSHFingerprint(pubkey ssh.PublicKey) (fingerprint string) { 235 | binary := pubkey.Marshal() 236 | hash := md5.Sum(binary) 237 | // now add the colons 238 | fingerprint = fmt.Sprintf("%02x", (hash[0])) 239 | for i := 1; i < len(hash); i += 1 { 240 | fingerprint += ":" + fmt.Sprintf("%02x", (hash[i])) 241 | } 242 | return fingerprint 243 | } 244 | 245 | func SSHPrivateFingerprint(privkey rsa.PrivateKey) (fingerprint string, err error) { 246 | sshPubkey, err := rsaPubkeyToSSHPubkey(privkey.PublicKey) 247 | if err != nil { 248 | return "", err 249 | } 250 | fingerprint = SSHFingerprint(sshPubkey) 251 | return fingerprint, nil 252 | } 253 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "crypto/rsa" 8 | 9 | "code.google.com/p/go.crypto/ssh" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func TestEncode(t *testing.T) { 14 | Convey("Test Encode A String", t, func() { 15 | pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXg9Vmhy9YSB8BcN3yHgQjdX9lN3j2KRpv7kVDXSiIana2WbKP7IiTS0uJcJWUM3vlHjdL9KOO0jCWWzVFIcmLhiVVG+Fy2tothBp/NhjR8WWG/6Jg/6tXvVkLG6bDgfbDaLWdE5xzjL0YG8TrIluqnu0J5GHKrQcXF650PlqkGo+whpXrS8wOG+eUmsHX9L1w/Z3TkQlMjQNJEoRbqqSrp7yGj4JqzbtLpsglPRlobD7LHp+5ZDxzpk9i+6hoMxp2muDFxnEtZyED6IMQlNNEGkc3sdmGPOo26oW2+ePkBcjpOpdVif/Iya/jDLuLFHAOol6G34Tr4IdTgaL0qCCr TEST KEY")) 16 | panic_the_err(err) 17 | plaintext := "some plaintext" 18 | ciphertext, err := CredulousEncode(plaintext, pubkey) 19 | So(err, ShouldEqual, nil) 20 | So(len(ciphertext), ShouldEqual, 556) 21 | }) 22 | } 23 | 24 | func TestEncodeAES(t *testing.T) { 25 | Convey("Test encoding with AES", t, func() { 26 | plaintext := "some plaintext" 27 | key := "12345678901234567890123456789012" 28 | ciphertext, err := encodeAES([]byte(key), plaintext) 29 | So(err, ShouldEqual, nil) 30 | So(len(ciphertext), ShouldEqual, 40) 31 | }) 32 | } 33 | 34 | func TestDecodeAES(t *testing.T) { 35 | Convey("Test encoding with AES", t, func() { 36 | ciphertext := "ghRydcBg7LR66v8pF6PvaXZ67gHk8toOtveDE+dP" 37 | key := "12345678901234567890123456789012" 38 | plaintext, err := decodeAES([]byte(key), ciphertext) 39 | So(err, ShouldEqual, nil) 40 | So(plaintext, ShouldEqual, "some plaintext") 41 | }) 42 | } 43 | 44 | func TestDecodeAESCredential(t *testing.T) { 45 | Convey("Test decoding an AES-encrypted ciphertext", t, func() { 46 | ciphertext := "eyJFbmNvZGVkS2V5IjoicDI5R3NmSmhIVjYvRGd3cmd1d040aDhKTmErTGJkZ0VHcU5vaVB6c1Rnb3IrOEJsQnJTVW1rWGZQTlFvRnY4NHdlcGkvYmd4ZmNyYlpDWm5iMEx4bW9pVjhjMERZYlE5M3F1d0ptK2VBNVhSVlZzTFZodUk1RG9rOENMbkwxOEl5aXc4OENWMXR6ZkJOUWNnQVdBckpsNHBMdzZEbkVFS21NOHRabCtNRUVnTlFjVStybUprKytZbU1ubW44KzVEU1Q5TWtLQ0lxeHl2eVNCRGYxVGkrS2ZHNTlXajkybGQycGZ1Q3k5YWREYlQ2azc0ZG1MbFkvOTlZMWVDZkREMmJWZjNueWJrUkg2UTM3bXNQVHpnbGRaWE56cjBoeStTUERTZHozU0lBSmZGZGw1dy9ka3pYTms2TXcwaHMxbjhRR1BsdnBMOFI1MzF1Rit5a3c5STh3PT0iLCJDaXBoZXJ0ZXh0Ijoiem5Cc2ZxbmJwYTFtdEF6Q09GMVZpU3VsUlRQSGZIblE1UEREZzluYyJ9" 47 | tmp, err := ioutil.ReadFile("testdata/testkey") 48 | panic_the_err(err) 49 | key, err := ssh.ParseRawPrivateKey(tmp) 50 | privkey := key.(*rsa.PrivateKey) 51 | panic_the_err(err) 52 | plaintext, err := CredulousDecodeAES(ciphertext, privkey) 53 | So(err, ShouldEqual, nil) 54 | So(plaintext, ShouldEqual, "some plaintext") 55 | }) 56 | } 57 | 58 | func TestDecodeWithSalt(t *testing.T) { 59 | Convey("Test Decode a string", t, func() { 60 | ciphertext := "sGhPCj9OCe0hv9PvWQvsu289sMsVNfqpyQDRCgXo+PwDMlXmRVXa5ErkkHNwyuYWFr9u1gkytiue7Dol4duvPycUYqpdeOOrfAMWkLWKGrO6tgTYtxMjVYBtp3negl2OeJqHFs6h/UwmNaO6IP2z2R8vPctmMmpwrkdzokiiPx6WKLDP17eoC+Q+zvDUqSTgqnSiwbjb+gFGFt7NTH65gHHHtwbm2wr45Oce4+LfddGo8V7A52ZjVlTHHdK+OiJzHmN8KMTAUi1d0ULI7oW+BfAX7iyA1SyvFx0oJHJ/dDidxPUm7i2vEeKtXU5BS8THv5dk01BwByJU+kl3qenCTA==" 61 | tmp, err := ioutil.ReadFile("testdata/testkey") 62 | panic_the_err(err) 63 | key, err := ssh.ParseRawPrivateKey(tmp) 64 | privkey := key.(*rsa.PrivateKey) 65 | panic_the_err(err) 66 | salt := "pepper" 67 | plaintext, err := CredulousDecodeWithSalt(ciphertext, salt, privkey) 68 | panic_the_err(err) 69 | So(plaintext, ShouldEqual, "some plaintext") 70 | }) 71 | } 72 | 73 | func TestSSHFingerprint(t *testing.T) { 74 | Convey("Test generating SSH fingerprint", t, func() { 75 | pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXg9Vmhy9YSB8BcN3yHgQjdX9lN3j2KRpv7kVDXSiIana2WbKP7IiTS0uJcJWUM3vlHjdL9KOO0jCWWzVFIcmLhiVVG+Fy2tothBp/NhjR8WWG/6Jg/6tXvVkLG6bDgfbDaLWdE5xzjL0YG8TrIluqnu0J5GHKrQcXF650PlqkGo+whpXrS8wOG+eUmsHX9L1w/Z3TkQlMjQNJEoRbqqSrp7yGj4JqzbtLpsglPRlobD7LHp+5ZDxzpk9i+6hoMxp2muDFxnEtZyED6IMQlNNEGkc3sdmGPOo26oW2+ePkBcjpOpdVif/Iya/jDLuLFHAOol6G34Tr4IdTgaL0qCCr TEST KEY")) 76 | panic_the_err(err) 77 | fingerprint := SSHFingerprint(pubkey) 78 | So(fingerprint, ShouldEqual, "c0:61:84:fc:e8:c9:52:dc:cd:a9:8e:82:a2:70:0a:30") 79 | }) 80 | } 81 | 82 | func TestSSHPrivateFingerprint(t *testing.T) { 83 | Convey("Test generating SSH private key fingerprint", t, func() { 84 | tmp, err := ioutil.ReadFile("testdata/testkey") 85 | panic_the_err(err) 86 | key, err := ssh.ParseRawPrivateKey(tmp) 87 | privkey := key.(*rsa.PrivateKey) 88 | panic_the_err(err) 89 | fingerprint, err := SSHPrivateFingerprint(*privkey) 90 | So(err, ShouldEqual, nil) 91 | So(fingerprint, ShouldEqual, "c0:61:84:fc:e8:c9:52:dc:cd:a9:8e:82:a2:70:0a:30") 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /debian-pkg/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: credulous 2 | Depends: bash-completion 3 | Architecture: amd64 4 | Maintainer: Colin Panisset 5 | Priority: optional 6 | Version: ==VERSION== 7 | Description: Credulous securely manages AWS credentials 8 | -------------------------------------------------------------------------------- /doc/DEVELOP-OSX.md: -------------------------------------------------------------------------------- 1 | # Things needed to build on OSX 2 | 3 | ## Mavericks 4 | 5 | * developer command-line tools 6 | * can get just by trying to run `git` and then following the 7 | prompts to download command-line tools -- do not need full XCode 8 | * go 9 | * download from 10 | http://golang.org/dl/go1.2.2.darwin-amd64-osx10.8.pkg and run the 11 | installer 12 | * hg 13 | * download from http://mercurial.selenic.com/mac/binaries/Mercurial-3.0.1-py2.7-macosx10.9.zip 14 | * it's an unsigned package, so will have to go to System Preferences 15 | -> Security & Privacy and allow "Anywhere" for downloaded apps 16 | * pkg-config 17 | * yeah, you need [Homebrew](http://brew.sh) 18 | * `brew install pkg-config` 19 | * libgit2 20 | * `brew install https://raw.githubusercontent.com/realestate-com-au/credulous-brew/master/libgit2-0.21.0.rb` 21 | 22 | -------------------------------------------------------------------------------- /doc/credulous.md: -------------------------------------------------------------------------------- 1 | % credulous(1) | Version ==VERSION== 2 | % Colin Panisset, Mike Bailey et. al., REA Group 3 | % Jun 2, 2014 4 | 5 | # NAME 6 | 7 | `credulous` - securely store and retrieve AWS credentials 8 | 9 | # SYNOPSIS 10 | 11 | `credulous []` 12 | 13 | # DESCRIPTION 14 | 15 | Credulous manages AWS credentials for you, storing them securely 16 | and retrieving them for placement in your shell runtime environment on 17 | demand so that they can be used by other tools. 18 | 19 | Credulous makes use of SSH RSA public keys to encrypt credentials at 20 | rest, and the corresponding private keys to decrypt them. It supports 21 | multiple AWS IAM users in multiple accounts, and provides the 22 | capability to store custom environment variables encrypted along with 23 | each set of credentials. 24 | 25 | # COMMANDS 26 | 27 | **save** Encrypt AWS credentials from the current environment 28 | variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` with an SSH 29 | RSA public key, and store them securely. 30 | 31 | **source** Decrypt a set of AWS credentials for a given username and 32 | account alias and make them available in a form suitable for eval'ing 33 | into the current shell runtime environment. 34 | 35 | **current** Query the AWS APIs using the current credentials and 36 | display the username and account alias. 37 | 38 | **rotate** Force a key rotation to occur -- credulous will delete one 39 | key and create a new one, saving the new credentials into the repository. 40 | 41 | **display** Show the currently loaded AWS credentials 42 | 43 | **list** Show a list of all stored `username@alias` credentials. 44 | 45 | # OPTIONS 46 | 47 | **-h** 48 | **--help** 49 | 50 | > All commands take the `-h` or `--help` option to describe the command 51 | > and its available full option set. 52 | 53 | ## Options for the save subcommand 54 | 55 | **-k \** 56 | **--key \** 57 | 58 | > Specify the SSH public key to use in saving the current credentials. 59 | > If more than one key is specified, the credentials will be saved 60 | > multiple times, encrypted with each different public key. 61 | 62 | **-e \=\** 63 | **--env \=\** 64 | 65 | > Save the environment variable `VAR` with the value `value` along with 66 | > the encrypted credentials. The option can be used multiple times to 67 | > save multiple different environment variables. All specified 68 | > environment variables are encrypted alongside the credentials. 69 | 70 | **-u \** 71 | **--username \** 72 | 73 | > Specify the AWS IAM username for the current credentials. Note that 74 | > this is unnecessary if you have an active Internet connection, as 75 | > credulous will query AWS for the username from the current 76 | > credentials when `save` is called. If `username` is specified, you 77 | > __must__ also specify **--account** and **--force** to prevent 78 | > credulous querying AWS. 79 | 80 | **-a \** 81 | **--account \** 82 | 83 | > Specify the AWS account alias. If you specify this option, you 84 | > __must__ also specify **--username** and **--force**, otherwise 85 | > credulous will query AWS for the account alias. If no account alias 86 | > has been defined, credulous will use the numeric account ID instead. 87 | 88 | **-f** 89 | **--force** 90 | 91 | > Do not attempt to verify the username or account alias/ID with AWS 92 | > when saving. This is useful if you don't have an Internet connection 93 | > when saving the credentials, but you __must__ specify both the 94 | > username and account alias at the same time. 95 | 96 | ## Options for the source subcommand 97 | 98 | If no options are specified, and no credential is specified on the 99 | command-line, and only a single set of credentials have been saved, 100 | credulous will read those credentials. If multiple credentials have 101 | been saved, you will have to specify the credentials to source. 102 | 103 | Note that if the SSH private key used to decrypt the credentials is not 104 | protected with a passphrase, credulous will issue a warning. 105 | 106 | **-k \** 107 | **--key \** 108 | 109 | > Use the specified SSH RSA private key to decrypt the credentials. 110 | 111 | **-a \** 112 | **--account \** 113 | 114 | > Load credentials for the named account 115 | 116 | **-u \** 117 | **--username \** 118 | 119 | > Load credentials for the named username. 120 | 121 | **-f** 122 | **--force** 123 | 124 | > Do not attempt to verify that the loaded credentials match the 125 | > username and account specified on the command-line. This is required 126 | > if you have no Internet connection, BUT __represents a security 127 | > risk__ in that a third party could substitute different credentials 128 | > and you would have no way of knowing that this had happened until AWS 129 | > API calls were made using those credentials, leading to a potential 130 | > leakage of information, and financial or operational damage. 131 | 132 | **-c \@\** 133 | **--credentials \@\** 134 | 135 | > Load the specified credentials. This is the default action for the 136 | > `source` subcommand, so invocations like `credulous source foo@bar` 137 | > are perfectly acceptable. 138 | 139 | ## Options for the current subcommand 140 | 141 | There are no options for the `current` subcommand. 142 | 143 | ## Options for the rotate subcommand 144 | 145 | **-k \** 146 | **--key \** 147 | 148 | > Specify the SSH public key to use in saving the new credentials. 149 | > If more than one key is specified, the credentials will be saved 150 | > multiple times, encrypted with each different public key. 151 | 152 | **-e \=\** 153 | **--env \=\** 154 | 155 | > Save the environment variable `VAR` with the value `value` along with 156 | > the encrypted credentials. The option can be used multiple times to 157 | > save multiple different environment variables. All specified 158 | > environment variables are encrypted alongside the credentials. 159 | 160 | ## Options for the display subcommand 161 | 162 | There are no options for the `display` subcommand. 163 | 164 | ## Options for the list subcommand 165 | 166 | There are no options for the `list` subcommand. 167 | 168 | # EXAMPLES 169 | 170 | ## Save a set of AWS credentials from the current environment 171 | 172 | host$ env | grep AWS 173 | AWS_ACCESS_KEY_ID=AKIAJRETNBUEIZ3S6VU2 174 | AWS_SECRET_ACCESS_KEY=ffLbUThxWlKvR/Wp/qanXlgpthqipyDsUxHBUrN2 175 | host$ credulous save 176 | Saving credentials for hoopy@frood 177 | 178 | ## Save a set of AWS credentials using a specific SSH public key 179 | 180 | host$ credulous save -k /path/to/ssh/key.pub 181 | 182 | ## Save a set of environment variables along with the AWS credentials 183 | 184 | host$ credulous save -e AWS_DEFAULT_REGION=us-west-2 \ 185 | -e FOO=bar -e BACON=yummy 186 | 187 | ## Load a particular set of credentials 188 | 189 | host$ credulous source hoopy@frood 190 | Enter passphrase for /path/to/my/ssh/privkey_rsa: ******** 191 | export AWS_ACCESS_KEY_ID=AKIAJRETNBUEIZ3S6VU2 192 | export AWS_SECRET_ACCESS_KEY=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 193 | 194 | ## Place the sourced credentials into the runtime environment 195 | 196 | host$ eval $( credulous source hoopy@frood ) 197 | Enter passphrase for /path/to/my/ssh/privkey_rsa: ******** 198 | host$ env | grep AWS 199 | AWS_ACCESS_KEY_ID=AKIAJRETNBUEIZ3S6VU2 200 | AWS_SECRET_ACCESS_KEY=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 201 | 202 | # AUTHORS 203 | 204 | Colin Panisset, Mike Bailey, Greg Dziemidowicz, Paul van de Vreede, 205 | Mujtaba Hussain, Stephen Moore. 206 | 207 | # BUGS 208 | 209 | Please report bugs via the GitHub page at 210 | https://github.com/realestate-com-au/credulous/issues 211 | 212 | # COPYRIGHT 213 | 214 | Copyright (c) 2014 REA Group, Pty Ltd. `credulous` is distributed 215 | under the MIT license: http://opensource.org/licenses/MIT 216 | There is NO WARRANTY, to the extent permitted by law. 217 | -------------------------------------------------------------------------------- /git.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "path" 6 | "time" 7 | 8 | "github.com/libgit2/git2go" 9 | ) 10 | 11 | type RepoConfig struct { 12 | Name string 13 | Email string 14 | } 15 | 16 | func isGitRepo(checkpath string) (bool, error) { 17 | ceiling := []string{checkpath} 18 | 19 | repopath, err := git.Discover(checkpath, false, ceiling) 20 | nonRepoErr := errors.New("Could not find repository from '" + checkpath + "'") 21 | if err != nil && err.Error() != nonRepoErr.Error() { 22 | return false, err 23 | } 24 | if err != nil && err.Error() == nonRepoErr.Error() { 25 | return false, nil 26 | } 27 | // the path is the parent of the repo, which appends '.git' 28 | // to the path 29 | dirpath := path.Dir(path.Clean(repopath)) 30 | if dirpath == checkpath { 31 | return true, nil 32 | } 33 | return false, nil 34 | } 35 | 36 | func getRepoConfig(repo *git.Repository) (RepoConfig, error) { 37 | config, err := repo.Config() 38 | if err != nil { 39 | return RepoConfig{}, err 40 | } 41 | name, err := config.LookupString("user.name") 42 | if err != nil { 43 | return RepoConfig{}, err 44 | } 45 | email, err := config.LookupString("user.email") 46 | if err != nil { 47 | return RepoConfig{}, err 48 | } 49 | repoconf := RepoConfig{ 50 | Name: name, 51 | Email: email, 52 | } 53 | return repoconf, nil 54 | } 55 | 56 | func gitAddCommitFile(repopath, filename, message string) (commitId string, err error) { 57 | repo, err := git.OpenRepository(repopath) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | config, err := getRepoConfig(repo) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | index, err := repo.Index() 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | err = index.AddByPath(filename) 73 | if err != nil { 74 | return "", err 75 | } 76 | 77 | err = index.Write() 78 | if err != nil { 79 | return "", err 80 | } 81 | 82 | treeId, err := index.WriteTree() 83 | if err != nil { 84 | return "", err 85 | } 86 | 87 | // new file is now staged, so we have to create a commit 88 | sig := &git.Signature{ 89 | Name: config.Name, 90 | Email: config.Email, 91 | When: time.Now(), 92 | } 93 | 94 | tree, err := repo.LookupTree(treeId) 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | var commit *git.Oid 100 | haslog, err := repo.HasLog("HEAD") 101 | if err != nil { 102 | return "", err 103 | } 104 | if !haslog { 105 | // In this case, the repo has been initialized, but nothing has ever been committed 106 | commit, err = repo.CreateCommit("HEAD", sig, sig, message, tree) 107 | if err != nil { 108 | return "", err 109 | } 110 | } else { 111 | // In this case, the repo has commits 112 | currentBranch, err := repo.Head() 113 | if err != nil { 114 | return "", err 115 | } 116 | currentTip, err := repo.LookupCommit(currentBranch.Target()) 117 | if err != nil { 118 | return "", err 119 | } 120 | commit, err = repo.CreateCommit("HEAD", sig, sig, message, tree, currentTip) 121 | if err != nil { 122 | return "", err 123 | } 124 | } 125 | 126 | return commit.String(), nil 127 | } 128 | -------------------------------------------------------------------------------- /git_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/libgit2/git2go" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestGitAdd(t *testing.T) { 15 | 16 | repopath := path.Join("testrepo." + fmt.Sprintf("%d", os.Getpid())) 17 | 18 | // need to create a new repo first 19 | repo, err := git.InitRepository(repopath, false) 20 | panic_the_err(err) 21 | defer os.RemoveAll(path.Clean(path.Join(repo.Path(), ".."))) 22 | 23 | // Need to add some basic config so that tests will pass 24 | config, _ := repo.Config() 25 | _ = config.SetString("user.name", "Test User") 26 | _ = config.SetString("user.email", "test.user@nowhere") 27 | 28 | Convey("Testing gitAdd", t, func() { 29 | Convey("Test add to non-existent repository", func() { 30 | _, err := gitAddCommitFile("/no/such/repo", "testdata/newcreds.json", "message") 31 | So(err, ShouldNotEqual, nil) 32 | }) 33 | 34 | Convey("Test add non-existent file to a repo", func() { 35 | _, err := gitAddCommitFile(repo.Path(), "/no/such/file", "message") 36 | // fmt.Println("gitAdd returned " + fmt.Sprintf("%s", err)) 37 | So(err, ShouldNotEqual, nil) 38 | }) 39 | 40 | Convey("Test add an initial file to the repo", func() { 41 | fp, _ := os.Create(path.Join(repopath, "testfile")) 42 | _, _ = fp.WriteString("A test string") 43 | _ = fp.Close() 44 | commitId, err := gitAddCommitFile(repo.Path(), "testfile", "first commit") 45 | So(err, ShouldEqual, nil) 46 | So(commitId, ShouldNotEqual, nil) 47 | So(commitId, ShouldNotBeBlank) 48 | }) 49 | 50 | Convey("Test add a second file to the repo", func() { 51 | fp, _ := os.Create(path.Join(repopath, "testfile")) 52 | _, _ = fp.WriteString("A second test string") 53 | _ = fp.Close() 54 | commitId, err := gitAddCommitFile(repo.Path(), "testfile", "second commit") 55 | So(err, ShouldEqual, nil) 56 | So(commitId, ShouldNotEqual, nil) 57 | So(commitId, ShouldNotBeBlank) 58 | }) 59 | 60 | Convey("Test checking whether a repo is a repo", func() { 61 | fullpath, _ := filepath.Abs(repopath) 62 | isrepo, err := isGitRepo(fullpath) 63 | So(err, ShouldEqual, nil) 64 | So(isrepo, ShouldEqual, true) 65 | }) 66 | 67 | Convey("Test checking whether a plain dir is a repo", func() { 68 | isrepo, err := isGitRepo("/tmp") 69 | So(err, ShouldEqual, nil) 70 | So(isrepo, ShouldEqual, false) 71 | }) 72 | 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /osx-pkg/osx-distribution-template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ARTIFACT 15 | Credulous 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /osx-pkg/resources/conclusion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Credulous 4 | 9 | 10 | 11 |

Thanks for installing Credulous

12 | 13 |

Credulous is a command-line tool to manage your AWS credentials. In order to get the proper functionality, please add the following text to your $HOME/.bashrc:

14 | 15 |

16 |   if [ -f /etc/profile.d/credulous.sh ]; then
17 |       . /etc/profile.d/credulous.sh
18 |   fi
19 | 

20 | 21 |

... and then restart your Terminal sessions.

22 | 23 |

To keep up with credulous development and status, check out http://credulous.io and https://github.com/realestate-com-au/credulous

24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /osx-pkg/resources/credulous-security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realestate-com-au/credulous/7482d59c7b8e61b1cc75393059c40df37d979cfc/osx-pkg/resources/credulous-security.png -------------------------------------------------------------------------------- /osx-pkg/scripts/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f /usr/local/etc/bash_completion.d ]; then 4 | cp /tmp/credulous.bash_completion /usr/local/etc/bash_completion.d/credulous 5 | else 6 | echo "You need to install bash-completion via homebrew in order to get" 7 | echo "bash completion for credulous. Once you've done so, re-install" 8 | echo "the credulous package to get the completer installed" 9 | fi 10 | 11 | echo "Finished at `date`" > /tmp/time_finished.txt 12 | -------------------------------------------------------------------------------- /osx-pkg/scripts/preinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GO=https://go.googlecode.com/files/go1.2.1.darwin-amd64-osx10.8.pkg 4 | BZR=https://launchpad.net/bzr/2.6/2.6.0/+download/Bazaar-2.6.0-OSX-10.6-2.dmg 5 | GIT=http://downloads.sourceforge.net/project/git-osx-installer/git-1.9.2-intel-universal-snow-leopard.dmg?r=&ts=1400407606&use_mirror=aarnet 6 | -------------------------------------------------------------------------------- /pkgs/README.md: -------------------------------------------------------------------------------- 1 | # Packages 2 | 3 | A placeholder directory for packages to be dumped into 4 | -------------------------------------------------------------------------------- /rpm/credulous.spec.tmpl: -------------------------------------------------------------------------------- 1 | # don't build debug versions 2 | %define debug_package %{nil} 3 | 4 | Name: credulous 5 | Version: ==VERSION== 6 | Release: 1%{?dist} 7 | Summary: Secure AWS credential storage, rotation and redistribution 8 | 9 | Group: Applications/System 10 | License: MIT 11 | URL: https://github.com/realestate-com-au/credulous 12 | Source0: credulous-%{version}.tar.gz 13 | 14 | BuildRequires: golang, mercurial, bzr, git, pandoc, wget, cmake, openssl-devel 15 | Requires: bash-completion, zlib, openssl, http-parser 16 | 17 | %description 18 | Credulous securely saves and retrieves AWS credentials, storing 19 | them in an encrypted local repository. 20 | 21 | %prep 22 | %setup -n src/github.com/realestate-com-au/credulous 23 | 24 | %build 25 | export GOPATH=$RPM_BUILD_DIR 26 | export HERE=`pwd` 27 | mkdir -p $GOPATH/src/github.com/libgit2 28 | cd $GOPATH/src/github.com/libgit2 29 | git clone https://github.com/libgit2/git2go.git 30 | cd git2go 31 | git submodule update --init 32 | sh -x ./script/build-libgit2-static.sh 33 | cd vendor/libgit2/build 34 | make 35 | cd ../../.. 36 | sh -x ./script/with-static.sh go install ./... 37 | 38 | cd $HERE 39 | go get -v -t ./... 40 | go test 41 | go build 42 | pandoc -s -w man doc/credulous.md -o credulous.1 43 | 44 | %install 45 | rm -rf $RPM_BUILD_ROOT 46 | mkdir -p $RPM_BUILD_ROOT/%{_bindir} \ 47 | $RPM_BUILD_ROOT/%{_sysconfdir}/bash_completion.d \ 48 | $RPM_BUILD_ROOT/%{_sysconfdir}/profile.d \ 49 | $RPM_BUILD_ROOT/%{_mandir}/man1 50 | 51 | cp credulous $RPM_BUILD_ROOT/%{_bindir} 52 | cp bash/credulous.bash_completion $RPM_BUILD_ROOT/%{_sysconfdir}/bash_completion.d/credulous.bash_completion 53 | cp bash/credulous.sh $RPM_BUILD_ROOT/%{_sysconfdir}/profile.d/credulous.sh 54 | chmod 0755 $RPM_BUILD_ROOT/%{_bindir}/credulous 55 | cp credulous.1 $RPM_BUILD_ROOT/%{_mandir}/man1/credulous.1 56 | 57 | %clean 58 | rm -rf $RPM_BUILD_ROOT 59 | 60 | %files 61 | %defattr(-,root,root,-) 62 | %attr(0755,root,root) %{_bindir}/credulous 63 | %attr(0644,root,root) %{_mandir}/man1/credulous.1.gz 64 | %attr(0644,root,root) %{_sysconfdir}/bash_completion.d/credulous.bash_completion 65 | %attr(0644,root,root) %{_sysconfdir}/profile.d/credulous.sh 66 | 67 | %changelog 68 | -------------------------------------------------------------------------------- /scripts/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake" 4 | -------------------------------------------------------------------------------- /scripts/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rake (10.3.2) 5 | 6 | PLATFORMS 7 | ruby 8 | 9 | DEPENDENCIES 10 | rake 11 | -------------------------------------------------------------------------------- /scripts/Rakefile: -------------------------------------------------------------------------------- 1 | desc "Get PKGs through s3 sync" 2 | task :sync do 3 | sh "aws s3 sync s3://credulous s3/ ../pkgs" 4 | end 5 | -------------------------------------------------------------------------------- /scripts/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | 9 | config.vm.define "centos" do |centos| 10 | centos.vm.box = "chef/centos-6.5" 11 | end 12 | 13 | config.vm.define "ubuntu" do |ubuntu| 14 | ubuntu.vm.box = "ubuntu/trusty64" 15 | end 16 | 17 | # All Vagrant configuration is done here. The most common configuration 18 | # options are documented and commented below. For a complete reference, 19 | # please see the online documentation at vagrantup.com. 20 | 21 | # Every Vagrant virtual environment requires a box to build off of. 22 | 23 | # Disable automatic box update checking. If you disable this, then 24 | # boxes will only be checked for updates when the user runs 25 | # `vagrant box outdated`. This is not recommended. 26 | # config.vm.box_check_update = false 27 | 28 | # Create a forwarded port mapping which allows access to a specific port 29 | # within the machine from a port on the host machine. In the example below, 30 | # accessing "localhost:8080" will access port 80 on the guest machine. 31 | # config.vm.network "forwarded_port", guest: 80, host: 8080 32 | 33 | # Create a private network, which allows host-only access to the machine 34 | # using a specific IP. 35 | # config.vm.network "private_network", ip: "192.168.33.10" 36 | 37 | # Create a public network, which generally matched to bridged network. 38 | # Bridged networks make the machine appear as another physical device on 39 | # your network. 40 | # config.vm.network "public_network" 41 | 42 | # If true, then any SSH connections made will enable agent forwarding. 43 | # Default value: false 44 | # config.ssh.forward_agent = true 45 | 46 | # Share an additional folder to the guest VM. The first argument is 47 | # the path on the host to the actual folder. The second argument is 48 | # the path on the guest to mount the folder. And the optional third 49 | # argument is a set of non-required options. 50 | # config.vm.synced_folder "../data", "/vagrant_data" 51 | 52 | # Provider-specific configuration so you can fine-tune various 53 | # backing providers for Vagrant. These expose provider-specific options. 54 | # Example for VirtualBox: 55 | # 56 | # config.vm.provider "virtualbox" do |vb| 57 | # # Don't boot with headless mode 58 | # vb.gui = true 59 | # 60 | # # Use VBoxManage to customize the VM. For example to change memory: 61 | # vb.customize ["modifyvm", :id, "--memory", "1024"] 62 | # end 63 | # 64 | # View the documentation for the provider you're using for more 65 | # information on available options. 66 | 67 | # Enable provisioning with CFEngine. CFEngine Community packages are 68 | # automatically installed. For example, configure the host as a 69 | # policy server and optionally a policy file to run: 70 | # 71 | # config.vm.provision "cfengine" do |cf| 72 | # cf.am_policy_hub = true 73 | # # cf.run_file = "motd.cf" 74 | # end 75 | # 76 | # You can also configure and bootstrap a client to an existing 77 | # policy server: 78 | # 79 | # config.vm.provision "cfengine" do |cf| 80 | # cf.policy_server_address = "10.0.2.15" 81 | # end 82 | 83 | # Enable provisioning with Puppet stand alone. Puppet manifests 84 | # are contained in a directory path relative to this Vagrantfile. 85 | # You will need to create the manifests directory and a manifest in 86 | # the file default.pp in the manifests_path directory. 87 | # 88 | # config.vm.provision "puppet" do |puppet| 89 | # puppet.manifests_path = "manifests" 90 | # puppet.manifest_file = "site.pp" 91 | # end 92 | 93 | # Enable provisioning with chef solo, specifying a cookbooks path, roles 94 | # path, and data_bags path (all relative to this Vagrantfile), and adding 95 | # some recipes and/or roles. 96 | # 97 | # config.vm.provision "chef_solo" do |chef| 98 | # chef.cookbooks_path = "../my-recipes/cookbooks" 99 | # chef.roles_path = "../my-recipes/roles" 100 | # chef.data_bags_path = "../my-recipes/data_bags" 101 | # chef.add_recipe "mysql" 102 | # chef.add_role "web" 103 | # 104 | # # You may also specify custom JSON attributes: 105 | # chef.json = { :mysql_password => "foo" } 106 | # end 107 | 108 | # Enable provisioning with chef server, specifying the chef server URL, 109 | # and the path to the validation key (relative to this Vagrantfile). 110 | # 111 | # The Opscode Platform uses HTTPS. Substitute your organization for 112 | # ORGNAME in the URL and validation key. 113 | # 114 | # If you have your own Chef Server, use the appropriate URL, which may be 115 | # HTTP instead of HTTPS depending on your configuration. Also change the 116 | # validation key to validation.pem. 117 | # 118 | # config.vm.provision "chef_client" do |chef| 119 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" 120 | # chef.validation_key_path = "ORGNAME-validator.pem" 121 | # end 122 | # 123 | # If you're using the Opscode platform, your validator client is 124 | # ORGNAME-validator, replacing ORGNAME with your organization name. 125 | # 126 | # If you have your own Chef Server, the default validation client name is 127 | # chef-validator, unless you changed the configuration. 128 | # 129 | # chef.validation_client_name = "ORGNAME-validator" 130 | end 131 | -------------------------------------------------------------------------------- /scripts/build_latest_libgit2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Oh, further horror: git2go, while most excellent, tracks HEAD of the 4 | # development branch of libgit2. This means that pretty much every 5 | # libgit2 packaged installation is too old. 6 | # 7 | 8 | set -ex 9 | 10 | [ -z "$TRAVIS_BUILD_DIR" ] && TRAVIS_BUILD_DIR=$(pwd) 11 | 12 | if [ -f /etc/redhat-release ]; then 13 | sudo yum install -y cmake openssl http-parser zlib 14 | SUFFIX="rhel" 15 | elif [ -f /etc/debian_version ]; then 16 | echo "precedence ::ffff:0:0/96 100" | sudo tee /etc/gai.conf 17 | # sudo apt-get install -y cmake openssl zlib1g 18 | SUFFIX="ubuntu" 19 | else 20 | echo "I can't build on this OS (yet) -- how about a pull request?" 21 | exit 1 22 | fi 23 | 24 | LIBGIT2_VERSION="0.21.0" 25 | TARBALL="v${LIBGIT2_VERSION}.tar.gz" 26 | SOURCE="https://github.com/libgit2/libgit2/archive/${TARBALL}" 27 | 28 | INSTALLDIR=/tmp/libgit2-${LIBGIT2_VERSION} 29 | cd /tmp 30 | wget "$SOURCE" 31 | tar xvf $TARBALL 32 | 33 | cd libgit2-${LIBGIT2_VERSION} 34 | rm -rf build && mkdir build && cd build 35 | cmake .. -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=$INSTALLDIR 36 | cmake --build . 37 | cmake --build . --target install 38 | 39 | cp $TRAVIS_BUILD_DIR/scripts/libgit2.pc-$SUFFIX \ 40 | $INSTALLDIR/lib/pkgconfig/libgit2.pc 41 | -------------------------------------------------------------------------------- /scripts/generate-linux-pkgs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | if [ -f /etc/redhat-release ]; then 6 | sudo yum install -y mock python-pip 7 | sudo pip-python install --upgrade awscli 8 | elif [ -f /etc/debian_version ]; then 9 | # Ooh, this is nasty 10 | # Manually pre-install dependencies 11 | sudo apt-get -o Acquire::ForceIPv4=true \ 12 | install \ 13 | adduser python python-decoratortools rpm usermode python-iniparse \ 14 | python-libxml2 python-rpm python-sqlite python-sqlitecachec python-urlgrabber \ 15 | pandoc python-pip 16 | # ... and now install mock from lucid (winces) 17 | wget http://mirrors.kernel.org/ubuntu/pool/universe/y/yum-utils/yum-utils_1.1.31-2_all.deb 18 | wget http://mirrors.kernel.org/ubuntu/pool/universe/m/mock/mock_1.1.33-1_all.deb 19 | wget http://mirrors.kernel.org/ubuntu/pool/universe/y/yum/yum_3.4.3-2ubuntu1_all.deb 20 | # (sorry) 21 | sudo dpkg -i yum_3.4.3-2ubuntu1_all.deb 22 | sudo dpkg -i yum-utils_1.1.31-2_all.deb 23 | sudo dpkg -i mock_1.1.33-1_all.deb 24 | # oh, the horror 25 | sudo groupadd mock 26 | sudo mkdir /var/lib/mock 27 | sudo chgrp -R mock /var/lib/mock 28 | sudo chmod 2755 /var/lib/mock 29 | # clean up so we don't release these .debs as well 30 | sudo pip install --upgrade awscli 31 | rm *.deb 32 | else 33 | echo "I don't know what distro I'm on" 34 | exit 1 35 | fi 36 | 37 | # This is annoying; Travis-CI uses ubuntu 12.04 for its build agents 38 | # and 12.04 doesn't have mock. 10.04 did, and 14.04 does, but not 39 | # anything in between. So the nastiness above ... 40 | echo $TRAVIS_BUILD_NUMBER > travis_build_number 41 | sudo make mock 42 | 43 | if [ $? -ne 0 ]; then 44 | cat /var/lib/mock/epel-6-x86_64/result/*.log 45 | exit 1 46 | fi 47 | 48 | # This should go fairly smoothly, give we're building a debian pkg on a debian box 49 | sudo make debianpkg 50 | 51 | if [ -z "$AWS_ACCESS_KEY_ID" ]; then 52 | echo "NO AWS CREDENTIALS -- NOT UPLOADING TO S3" 53 | exit 0 54 | fi 55 | 56 | for pkg in *.rpm *.deb; do 57 | aws s3 cp $pkg s3://credulous 58 | done 59 | -------------------------------------------------------------------------------- /scripts/generate-osx-pkgs: -------------------------------------------------------------------------------- 1 | #!/bin/bash +ex 2 | 3 | PACKAGER=/usr/bin/pkgbuild 4 | BUILDER=/usr/bin/productbuild 5 | 6 | if [ ! -f $PACKAGER ]; then 7 | echo "You need 'pkgbuild' binary to create os-x installer packages" 8 | exit 1 9 | fi 10 | 11 | if [ ! -f $BUILDER ]; then 12 | echo "You need 'productbuild' binary to create os-x installer packages" 13 | exit 1 14 | fi 15 | 16 | SOURCE="$( dirname $0 )/.." 17 | NAME="credulous" 18 | VERSION=$( cat VERSION ) 19 | IDENTIFIER="com.realestate-com-au.${NAME}" 20 | RELEASE=1 21 | NVR="$NAME-$VERSION-$RELEASE" 22 | DESTINATION="/" 23 | TMP_PKG_PATH="/tmp/credulous" 24 | TEMP_ARTIFACT="$TMP_PKG_PATH/credulous.pkg" 25 | ARTIFACT="$SOURCE/${NVR}.pkg" 26 | DIST_FILE="/tmp/os-x-distribution.xml" 27 | DIST_TEMPLATE="$SOURCE/osx-pkg/osx-distribution-template.xml" 28 | RESOURCES="$SOURCE/osx-pkg/resources" 29 | SCRIPTS="$SOURCE/osx-pkg/scripts" 30 | 31 | TMPROOT=$( mktemp -d -t pkg ) 32 | mkdir -p $TMPROOT/usr/bin \ 33 | $TMPROOT/etc/profile.d \ 34 | $TMPROOT/tmp # In case homebrew's bash-completion is installed 35 | 36 | cp credulous $TMPROOT/usr/bin 37 | cp bash/credulous.bash_completion $TMPROOT/tmp 38 | cp bash/credulous.sh $TMPROOT/etc/profile.d 39 | 40 | echo "Removing old artifact $ARTIFACT" 41 | rm -f $ARTIFACT 42 | 43 | cp $DIST_TEMPLATE $DIST_FILE 44 | 45 | sed -i.bak -e s/IDENTIFIER/$IDENTIFIER/g $DIST_FILE 46 | sed -i.bak -e s/NVR/$NVR/g $DIST_FILE 47 | sed -i.bak -e s/ARTIFACT/"$NAME.pkg"/g $DIST_FILE 48 | 49 | mkdir -p $TMP_PKG_PATH 50 | 51 | $PACKAGER --root $TMPROOT \ 52 | --identifier $IDENTIFIER \ 53 | --scripts $SCRIPTS \ 54 | --version $NVR \ 55 | --filter ".DS_Store" --filter ".git" --filter "swp" \ 56 | --filter "os-x" --filter "osx" \ 57 | --filter "debian" --filter "rpm" --filter "brew" \ 58 | --install-location $DESTINATION $TEMP_ARTIFACT 59 | 60 | $BUILDER --distribution $DIST_FILE \ 61 | --resources $RESOURCES \ 62 | --package-path $TMP_PKG_PATH $ARTIFACT 63 | 64 | rm $DIST_FILE 65 | rm $TEMP_ARTIFACT 66 | rm -rf $TMPROOT 67 | -------------------------------------------------------------------------------- /scripts/generate-pkgs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | env | egrep -v "AWS_|TOKEN" 4 | 5 | case "x$TRAVIS_OS_NAME" in 6 | xlinux) scripts/generate-linux-pkgs; exit $? ;; 7 | xosx) scripts/generate-osx-pkgs; exit $? ;; 8 | *) echo "TRAVIS_OS_NAME not set" ;; 9 | esac 10 | 11 | case "x$_system_type" in 12 | x[Ll]inux) scripts/generate-linux-pkgs; exit $? ;; 13 | x[Oo][Ss][Xx]) scripts/generate-osx-pkgs; exit $? ;; 14 | *) echo "_system_type not set" ;; 15 | esac 16 | 17 | echo "I can't work out what build environment this is" 18 | echo "Here's the runtime environment, if it helps:" 19 | env 20 | exit 1 21 | -------------------------------------------------------------------------------- /scripts/github-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2014 Terry Burton 4 | # 5 | # https://github.com/terryburton/travis-github-release 6 | # 7 | # Permission is hereby granted, free of charge, to any 8 | # person obtaining a copy of this software and associated 9 | # documentation files (the "Software"), to deal in the 10 | # Software without restriction, including without 11 | # limitation the rights to use, copy, modify, merge, 12 | # publish, distribute, sublicense, and/or sell copies of 13 | # the Software, and to permit persons to whom the Software 14 | # is furnished to do so, subject to the following 15 | # conditions: 16 | # 17 | # The above copyright notice and this permission notice 18 | # shall be included in all copies or substantial portions 19 | # of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 22 | # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 23 | # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 24 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 25 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 26 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 27 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 28 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 29 | # IN THE SOFTWARE. 30 | 31 | # This script provides a simple continuous deployment 32 | # solution that allows Travis CI to publish a new GitHub 33 | # release and upload assets to it whenever an annotated or 34 | # signed tag is pushed (git tag -a/-s; git push --tags). 35 | # 36 | # It is created as a temporary solution whilst we wait for 37 | # Travis DPL to support GitHub: 38 | # 39 | # https://github.com/travis-ci/dpl 40 | # 41 | # Place this script somewhere like .travis/github-release.sh 42 | # then add something like this to your .travis.yml: 43 | # 44 | # after_success: .travis/github-release.sh "$TRAVIS_REPO_SLUG" "`head -1 src/VERSION`" build/release/* 45 | # 46 | # The first argument is your repository in the format 47 | # "username/repository", which Travis provides in the 48 | # TRAVIS_REPO_SLUG environment variable. 49 | # 50 | # The second argument is the release version which as a 51 | # sanity check should match the tag that you are releasing. 52 | # You could pass "`git describe`" to satisfy this check. 53 | # 54 | # The third argument indicates whether this is a prerelease or not 55 | # 56 | # The remaining arguments are a list of asset files that you 57 | # want to publish along with the release. 58 | # 59 | # The script requires that you create a GitHub OAuth access 60 | # token to facilitate the upload: 61 | # 62 | # https://help.github.com/articles/creating-an-access-token-for-command-line-use 63 | # 64 | # You must pass this securely in the GITHUBTOKEN environment 65 | # variable: 66 | # 67 | # http://docs.travis-ci.com/user/build-configuration/#Secure-environment-variables 68 | # 69 | # For testing purposes you can create a local convenience 70 | # file in the script directory called GITHUBTOKEN that sets 71 | # the GITHUBTOKEN environment variable. If you do so you MUST 72 | # ensure that this doesn't get pushed to your repository, 73 | # perhaps by adding it to a .gitignore file. 74 | # 75 | # Should you get stuck then look at a working example. This 76 | # code is being used by Barcode Writer in Pure PostScript 77 | # for automated deployment: 78 | # 79 | # https://github.com/terryburton/postscriptbarcode 80 | 81 | set -e 82 | 83 | REPO=$1 && shift 84 | RELEASE=$1 && shift 85 | PRERELEASE=$1 && shift 86 | EXISTS=$1 && shift 87 | RELEASEFILES=$@ 88 | 89 | if ! TAG=`git describe --exact-match 2>/dev/null`; then 90 | echo "This commit is not a tag so not creating a release" 91 | exit 0 92 | fi 93 | 94 | if [ "$TAG" != "$RELEASE" ]; then 95 | echo "Error: The tag ($TAG) does not match the indicated release ($RELEASE)" 96 | exit 1 97 | fi 98 | 99 | if [[ -z "$RELEASEFILES" ]]; then 100 | echo "Error: No release files provided" 101 | exit 1 102 | fi 103 | 104 | if [ -z "$PRERELEASE" ]; then 105 | echo "Error: you need to indicate whether this is a prerelease" 106 | exit 1 107 | fi 108 | 109 | if [ -z "$EXISTS" ]; then 110 | echo "Error: you need to indicate whether or not the tag already exists" 111 | exit 1 112 | fi 113 | 114 | case $PRERELEASE in 115 | true|false) : ;; 116 | *) echo "Error: PRERELEASE must be true or false"; exit 1 ;; 117 | esac 118 | 119 | SCRIPTDIR=`dirname $0` 120 | [ -e "$SCRIPTDIR/GITHUBTOKEN" ] && . "$SCRIPTDIR/GITHUBTOKEN" 121 | if [[ -z "$GITHUBTOKEN" ]]; then 122 | echo "Error: GITHUBTOKEN is not set" 123 | exit 1 124 | fi 125 | 126 | echo "Creating GitHub release for $RELEASE" 127 | 128 | NAME=$( git tag -l $TAG -n1 | cut -f2- -d" " ) 129 | 130 | echo -n "Create draft release... " 131 | JSON=$(cat <