├── .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 | [](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 | 
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 <