Skip to content
This repository has been archived by the owner on Nov 15, 2024. It is now read-only.

Commit

Permalink
Adding a --priv-launch-key parameter to get-password-data which allow…
Browse files Browse the repository at this point in the history
…s the encrypted password to be decrypted using the supplied SSH private key file.
  • Loading branch information
garnaat committed Jul 28, 2013
1 parent 87cdf83 commit b117c0d
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 3 deletions.
5 changes: 5 additions & 0 deletions awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,11 @@ def __call__(self, args, parsed_globals):
if remaining:
raise UnknownArgumentError(
"Unknown options: %s" % ', '.join(remaining))
service_name = self._service_object.endpoint_prefix
operation_name = self._operation_object.name
self._emit('operation-args-parsed.%s.%s' % (service_name,
operation_name),
operation=self._operation_object, parsed_args=parsed_args)
call_parameters = self._build_call_parameters(parsed_args,
self.arg_table)
return self._operation_caller.invoke(
Expand Down
150 changes: 150 additions & 0 deletions awscli/customizations/ec2decryptpassword.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import logging
import os
import base64
import rsa
import six

from awscli.clidriver import BaseCLIArgument
from botocore.parameters import StringParameter

logger = logging.getLogger(__name__)


HELP = """<p>The file that contains the private key used to launch
the instance (e.g. windows-keypair.pem). If this is supplied, the
password data sent from EC2 will be decrypted before display.</p>"""


# This is a bit kludgy.
# We need a way to pass some state between the event handlers.
# One event handler determines if a path to the private key
# file was specified and, if so, validates the path.
# The other event handler attempts to decrypt the password data
# if a private key path was specified.
# I'm using the module-level globalvar to maintain that shared
# state. In the context of a CLI, I think that's okay.

_key_path = None

def _set_key_path(path):
global _key_path
_key_path = path

def _get_key_path():
global _key_path
return _key_path


def decrypt_password_data(event_name, shape, value, **kwargs):
"""
This handler gets called after the PasswordData element of the
response has been parsed. It checks to see if a private launch
key was specified on the command. If it was, it tries to use
that private key to decrypt the password data and return it.
"""
key_path = _get_key_path()
if key_path:
logger.debug('decrypt_password_data: %s', key_path)
try:
private_key_file = open(key_path)
private_key_contents = private_key_file.read()
private_key_file.close()
private_key = rsa.PrivateKey.load_pkcs1(six.b(private_key_contents))
value = base64.b64decode(value)
value = rsa.decrypt(value, private_key)
except:
# TODO
# Should we raise an exception or just return the
# unencrypted data? Or maybe just print a message?
logger.debug('Unable to decrypt PasswordData', exc_info=True)
msg = ('Unable to decrypt password data using '
'provided private key file.')
raise ValueError(msg)
return value


def ec2_add_priv_launch_key(argument_table, operation, **kwargs):
"""
This handler gets called after the argument table for the
operation has been created. It's job is to add the
``priv-launch-key`` parameter.
"""
argument_table['priv-launch-key'] = LaunchKeyArgument(operation,
'priv-launch-key')


def ec2_process_priv_launch_key(operation, parsed_args, **kwargs):
"""
This handler gets called after the command line arguments to
the ``get-password-data`` command have been parsed. It is
passed the ``Operation`` object and the ``Namespace`` containing
the parsed args.
It needs to check to see if ``priv-launch-key`` was supplied
by the user. If it was, it checks to make sure the path provided
points to a real file and, if so, stores the path in the module
global var for access later by the decrypt method.
"""
if parsed_args.priv_launch_key:
path = os.path.expandvars(parsed_args.priv_launch_key)
path = os.path.expanduser(path)
logger.debug(path)
if os.path.isfile(path):
_set_key_path(path)
else:
msg = ('priv-launch-key should be a path to the '
'local SSH private key file used to launch '
'the instance.')
raise ValueError(msg)


class LaunchKeyArgument(BaseCLIArgument):

def __init__(self, operation, name):
param = StringParameter(operation,
name='priv_launch_key',
type='string')
super(LaunchKeyArgument, self).__init__(
name, argument_object=param)
self._operation = operation
self._name = name

@property
def cli_name(self):
return '--' + self._name

@property
def cli_type_name(self):
return 'string'

@property
def required(self):
return False

@property
def documentation(self):
return HELP

def add_to_parser(self, parser, cli_name=None):
parser.add_argument(self.cli_name, metavar=self.py_name,
help='Number of instances to launch')

def add_to_params(self, parameters, value):
"""
Since the extra ``priv-launch-key`` parameter is local and
doesn't need to be sent to the service, we don't have to do
anything here.
"""
pass
9 changes: 9 additions & 0 deletions awscli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
from awscli.customizations.removals import register_removals
from awscli.customizations.ec2addcount import ec2_add_count
from awscli.customizations.paginate import unify_paging_params
from awscli.customizations.ec2decryptpassword import ec2_add_priv_launch_key
from awscli.customizations.ec2decryptpassword import ec2_process_priv_launch_key
from awscli.customizations.ec2decryptpassword import decrypt_password_data


def awscli_initialize(event_handlers):
Expand All @@ -44,4 +47,10 @@ def awscli_initialize(event_handlers):
ec2_add_count)
event_handlers.register('building-argument-table',
unify_paging_params)
event_handlers.register('building-argument-table.ec2.GetPasswordData',
ec2_add_priv_launch_key)
event_handlers.register('operation-args-parsed.ec2.GetPasswordData',
ec2_process_priv_launch_key)
event_handlers.register('after-parsed.ec2.GetPasswordData.String.PasswordData',
decrypt_password_data)
register_removals(event_handlers)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ nose==1.3.0
colorama==0.2.5
mock==1.0.1
httpretty==0.6.1
rsa==3.1.1
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
"""

import os
import sys
import awscli
import glob

try:
from setuptools import setup
Expand Down Expand Up @@ -37,7 +35,8 @@ def get_data_files():
'six>=1.1.0',
'colorama==0.2.5',
'argparse>=1.1',
'docutils>=0.10']
'docutils>=0.10',
'rsa==3.1.1']


setup(
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/ec2/test_get_password_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python
# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from tests.unit import BaseAWSCommandParamsTest
import os
from awscli.clidriver import create_clidriver
from awscli.customizations.ec2decryptpassword import _get_key_path
from botocore.response import XmlResponse

GET_PASSWORD_DATA_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
<GetPasswordDataResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
<requestId>000000000000</requestId>
<instanceId>i-12345678</instanceId>
<timestamp>2013-07-27T18:29:23.000Z</timestamp>
<passwordData>&#xd;
GWDnuoj/7pbMQkg125E8oGMUVCI+r98sGbFFl8SX+dEYxMZzz+byYwwjvyg8iSGKaLuLTIWatWopVu5cMWDKH65U4YFL2g3LqyajBrCFnuSE1piTeS/rPQpoSvBN5FGj9HWqNrglWAJgh9OZNSGgpEojBenL/0rwSpDWL7f/f52M5doYA6q+v0ygEoi1Wq6hcmrBfyA4seW1RlKgnUru5Y9oc1hFHi53E3b1EkjGqCsCemVUwumBj8uwCLJRaMcqrCxK1smtAsiSqk0Jk9jpN2vcQgnMPypEdmEEXyWHwq55fjy6ch+sqYcwumIL5QcFW2JQ5+XBEoFhC66gOsAXow==&#xd;
</passwordData>
</GetPasswordDataResponse>"""


class TestGetPasswordData(BaseAWSCommandParamsTest):

prefix = 'ec2 get-password-data'

def test_no_priv_launch_key(self):
args = ' --instance-id i-12345678'
cmdline = self.prefix + args
result = {'InstanceId': 'i-12345678'}
self.assert_params_for_cmd(cmdline, result)

def test_nonexistent_priv_launch_key(self):
args = ' --instance-id i-12345678 --priv-launch-key foo.pem'
cmdline = self.prefix + args
result = {}
self.assert_params_for_cmd(cmdline, result, expected_rc=255)

def test_priv_launch_key(self):
driver = create_clidriver()
key_path = os.path.join(os.path.dirname(__file__),
'testcli.pem')
args = ' --instance-id i-12345678 --priv-launch-key %s' % key_path
cmdline = self.prefix + args
cmdlist = cmdline.split()
rc = driver.main(cmdlist)
self.assertEqual(rc, 0)
self.assertEqual(key_path, _get_key_path())
service = driver.session.get_service('ec2')
operation = service.get_operation('GetPasswordData')
r = XmlResponse(driver.session, operation)
r.parse(GET_PASSWORD_DATA_RESPONSE, 'utf-8')
response_data = r.get_value()
self.assertEqual(response_data['PasswordData'], '=mG8.r$o-s')
23 changes: 23 additions & 0 deletions tests/unit/ec2/testcli.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAiaf6VsJ5w7emzh+6ffFj31ropPeW2N8wnNtfkItp4gmCdCpTQA4Kce/tzk5V
VPTaPyanYXcdwib4KXqf77dSACxpqtel7jwotdLLNU9uUm9pIFsVh0qnLJxXdgJgujsY/HBq4BRE
l7W1uA0e7QD1DH3QImELEXmnwO9ym/CiuwpUYGYrLLwIaezGjv/XUsn/KMzR2cQsseQu+jiBWXhA
+4plnC4aNOWCXym/VlTgRDaGL8FijEzfiL+k4ZrG6/qakAt1xDkB8xGY4yOCexnL80Pi31ybT7QG
odhuIkUr1CCgakvBpY1y8X8/yXmwZZtzl/qS47cIz9OUMjeFsfBBZwIDAQABAoIBAEXNN9PmqXfl
GGBNFnPmg44uuulr4sH16uCfHMZe60IDMHNXQv+oHwPHdf63Ge4KeuCq6RUzIZPhztS5qYAUpTAR
VUOcNjenqb0JNqHBtV93vwb5KOGBqWOlo3PjoMjOTs0y8/7MSDvlmE/L13K2mYvMAE5uhv5FghsD
UEpiqyHMSl4gGqOxz1SbOLvmSdlmlLHW9qjrT+oEoqZX/nfQyJHk0zoZlfTJdnBL5R1GtTP2mI/E
GCy4z7qllmGJR4x24GZxNAggr3mWvmkaVxivO2+3FN1TtwGJo/Wj5ncZLv889TXzj2V8HOcoZ8L/
GsZGxdJ5rWrxRLQySePSUKze+gECgYEA27n4p6uJLF8Ra25nQmgs2/VpzM1K0fd1tX3yKCYFGwbl
45Lzi7a672GQryAmSVWSfCgf9uwZmVWcPW1gZT2pBqP1+8EF1T2L6yPnGR1psF9jMmWGJogFlM4o
P++Ym3SHT4bd0we1iUnEH6JS65BEmopVhM8/7ZP50WHibDddoRcCgYEAoGGSLRLpvwczgOlirNSM
RCsJ9uAWUBZYMO1XMoAet0CWat9YwOqiiavcsD+0bNH0bZISQrLtaoiDSdFpRYjcgpBeu4MaGGqJ
EXYUGxNwmY1TZ3ml+9Oh1I84p0XIALeCueEFH9lJaXHzdhjNDn+KxKUg7UIz72+v3n/AyhvqdDEC
gYEAsyUwR7xCreug7z9nbywyju/LYBBtFT22OdBC9FrzRLLeEirI6LuGNBAO/8mtjZL4SMQKM68R
vAOhzC92LXUVb3WU47rff5mbj46JJ9/kQMm0ve0qcBXsvwNKq740ZWKfw8ZI63rYluOOxN/6zVal
qH5q9Upoa9J/FyjAi8ykSOcCgYBJknjsFHEGINePm4CYqChwXQ4FImcZ9iYey8HkeMGebxKRlEOy
u/A0F5L1h0PNZ8MpQIj/7/TZmiYgBuCz9USy4GeUvV+LM9QNHo26ngBZcGuCXFu4Wi0yxUDH+0r0
iTp+6qrfIV578LouwtHOhNOzwcyJCoWooSOcfh6CmKvFAQKBgH+6tVQi9Tj4Ig15HueURVVeiPVn
L9MPiR4unUsPz6OevoNsRaDkj7XDFBuMiFPGoXoyL9SedSSFw+oRxcPi4YMMvsZNC0yxOnj2Qe8z
V1Xgvb+rlN6NEIlPcscxE2F8gu1zIan/lXLMEXMr9hAAaGqS/JSALzt6XcoHwzEGnTrH
-----END RSA PRIVATE KEY-----
1 change: 1 addition & 0 deletions tests/unit/test_clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def test_expected_events_are_emitted_in_order(self):
'top-level-args-parsed',
'building-command-table.s3',
'building-argument-table.s3.ListObjects',
'operation-args-parsed.s3.ListObjects',
'process-cli-arg.s3.list-objects',
])

Expand Down

0 comments on commit b117c0d

Please sign in to comment.