Skip to content

Commit b117c0d

Browse files
committed
Adding a --priv-launch-key parameter to get-password-data which allows the encrypted password to be decrypted using the supplied SSH private key file.
1 parent 87cdf83 commit b117c0d

File tree

8 files changed

+253
-3
lines changed

8 files changed

+253
-3
lines changed

awscli/clidriver.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,11 @@ def __call__(self, args, parsed_globals):
701701
if remaining:
702702
raise UnknownArgumentError(
703703
"Unknown options: %s" % ', '.join(remaining))
704+
service_name = self._service_object.endpoint_prefix
705+
operation_name = self._operation_object.name
706+
self._emit('operation-args-parsed.%s.%s' % (service_name,
707+
operation_name),
708+
operation=self._operation_object, parsed_args=parsed_args)
704709
call_parameters = self._build_call_parameters(parsed_args,
705710
self.arg_table)
706711
return self._operation_caller.invoke(
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
import logging
14+
import os
15+
import base64
16+
import rsa
17+
import six
18+
19+
from awscli.clidriver import BaseCLIArgument
20+
from botocore.parameters import StringParameter
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
HELP = """<p>The file that contains the private key used to launch
26+
the instance (e.g. windows-keypair.pem). If this is supplied, the
27+
password data sent from EC2 will be decrypted before display.</p>"""
28+
29+
30+
# This is a bit kludgy.
31+
# We need a way to pass some state between the event handlers.
32+
# One event handler determines if a path to the private key
33+
# file was specified and, if so, validates the path.
34+
# The other event handler attempts to decrypt the password data
35+
# if a private key path was specified.
36+
# I'm using the module-level globalvar to maintain that shared
37+
# state. In the context of a CLI, I think that's okay.
38+
39+
_key_path = None
40+
41+
def _set_key_path(path):
42+
global _key_path
43+
_key_path = path
44+
45+
def _get_key_path():
46+
global _key_path
47+
return _key_path
48+
49+
50+
def decrypt_password_data(event_name, shape, value, **kwargs):
51+
"""
52+
This handler gets called after the PasswordData element of the
53+
response has been parsed. It checks to see if a private launch
54+
key was specified on the command. If it was, it tries to use
55+
that private key to decrypt the password data and return it.
56+
"""
57+
key_path = _get_key_path()
58+
if key_path:
59+
logger.debug('decrypt_password_data: %s', key_path)
60+
try:
61+
private_key_file = open(key_path)
62+
private_key_contents = private_key_file.read()
63+
private_key_file.close()
64+
private_key = rsa.PrivateKey.load_pkcs1(six.b(private_key_contents))
65+
value = base64.b64decode(value)
66+
value = rsa.decrypt(value, private_key)
67+
except:
68+
# TODO
69+
# Should we raise an exception or just return the
70+
# unencrypted data? Or maybe just print a message?
71+
logger.debug('Unable to decrypt PasswordData', exc_info=True)
72+
msg = ('Unable to decrypt password data using '
73+
'provided private key file.')
74+
raise ValueError(msg)
75+
return value
76+
77+
78+
def ec2_add_priv_launch_key(argument_table, operation, **kwargs):
79+
"""
80+
This handler gets called after the argument table for the
81+
operation has been created. It's job is to add the
82+
``priv-launch-key`` parameter.
83+
"""
84+
argument_table['priv-launch-key'] = LaunchKeyArgument(operation,
85+
'priv-launch-key')
86+
87+
88+
def ec2_process_priv_launch_key(operation, parsed_args, **kwargs):
89+
"""
90+
This handler gets called after the command line arguments to
91+
the ``get-password-data`` command have been parsed. It is
92+
passed the ``Operation`` object and the ``Namespace`` containing
93+
the parsed args.
94+
95+
It needs to check to see if ``priv-launch-key`` was supplied
96+
by the user. If it was, it checks to make sure the path provided
97+
points to a real file and, if so, stores the path in the module
98+
global var for access later by the decrypt method.
99+
"""
100+
if parsed_args.priv_launch_key:
101+
path = os.path.expandvars(parsed_args.priv_launch_key)
102+
path = os.path.expanduser(path)
103+
logger.debug(path)
104+
if os.path.isfile(path):
105+
_set_key_path(path)
106+
else:
107+
msg = ('priv-launch-key should be a path to the '
108+
'local SSH private key file used to launch '
109+
'the instance.')
110+
raise ValueError(msg)
111+
112+
113+
class LaunchKeyArgument(BaseCLIArgument):
114+
115+
def __init__(self, operation, name):
116+
param = StringParameter(operation,
117+
name='priv_launch_key',
118+
type='string')
119+
super(LaunchKeyArgument, self).__init__(
120+
name, argument_object=param)
121+
self._operation = operation
122+
self._name = name
123+
124+
@property
125+
def cli_name(self):
126+
return '--' + self._name
127+
128+
@property
129+
def cli_type_name(self):
130+
return 'string'
131+
132+
@property
133+
def required(self):
134+
return False
135+
136+
@property
137+
def documentation(self):
138+
return HELP
139+
140+
def add_to_parser(self, parser, cli_name=None):
141+
parser.add_argument(self.cli_name, metavar=self.py_name,
142+
help='Number of instances to launch')
143+
144+
def add_to_params(self, parameters, value):
145+
"""
146+
Since the extra ``priv-launch-key`` parameter is local and
147+
doesn't need to be sent to the service, we don't have to do
148+
anything here.
149+
"""
150+
pass

awscli/handlers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
from awscli.customizations.removals import register_removals
2323
from awscli.customizations.ec2addcount import ec2_add_count
2424
from awscli.customizations.paginate import unify_paging_params
25+
from awscli.customizations.ec2decryptpassword import ec2_add_priv_launch_key
26+
from awscli.customizations.ec2decryptpassword import ec2_process_priv_launch_key
27+
from awscli.customizations.ec2decryptpassword import decrypt_password_data
2528

2629

2730
def awscli_initialize(event_handlers):
@@ -44,4 +47,10 @@ def awscli_initialize(event_handlers):
4447
ec2_add_count)
4548
event_handlers.register('building-argument-table',
4649
unify_paging_params)
50+
event_handlers.register('building-argument-table.ec2.GetPasswordData',
51+
ec2_add_priv_launch_key)
52+
event_handlers.register('operation-args-parsed.ec2.GetPasswordData',
53+
ec2_process_priv_launch_key)
54+
event_handlers.register('after-parsed.ec2.GetPasswordData.String.PasswordData',
55+
decrypt_password_data)
4756
register_removals(event_handlers)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ nose==1.3.0
1111
colorama==0.2.5
1212
mock==1.0.1
1313
httpretty==0.6.1
14+
rsa==3.1.1

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
"""
66

77
import os
8-
import sys
98
import awscli
10-
import glob
119

1210
try:
1311
from setuptools import setup
@@ -37,7 +35,8 @@ def get_data_files():
3735
'six>=1.1.0',
3836
'colorama==0.2.5',
3937
'argparse>=1.1',
40-
'docutils>=0.10']
38+
'docutils>=0.10',
39+
'rsa==3.1.1']
4140

4241

4342
setup(
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python
2+
# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"). You
5+
# may not use this file except in compliance with the License. A copy of
6+
# the License is located at
7+
#
8+
# http://aws.amazon.com/apache2.0/
9+
#
10+
# or in the "license" file accompanying this file. This file is
11+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12+
# ANY KIND, either express or implied. See the License for the specific
13+
# language governing permissions and limitations under the License.
14+
from tests.unit import BaseAWSCommandParamsTest
15+
import os
16+
from awscli.clidriver import create_clidriver
17+
from awscli.customizations.ec2decryptpassword import _get_key_path
18+
from botocore.response import XmlResponse
19+
20+
GET_PASSWORD_DATA_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
21+
<GetPasswordDataResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
22+
<requestId>000000000000</requestId>
23+
<instanceId>i-12345678</instanceId>
24+
<timestamp>2013-07-27T18:29:23.000Z</timestamp>
25+
<passwordData>&#xd;
26+
GWDnuoj/7pbMQkg125E8oGMUVCI+r98sGbFFl8SX+dEYxMZzz+byYwwjvyg8iSGKaLuLTIWatWopVu5cMWDKH65U4YFL2g3LqyajBrCFnuSE1piTeS/rPQpoSvBN5FGj9HWqNrglWAJgh9OZNSGgpEojBenL/0rwSpDWL7f/f52M5doYA6q+v0ygEoi1Wq6hcmrBfyA4seW1RlKgnUru5Y9oc1hFHi53E3b1EkjGqCsCemVUwumBj8uwCLJRaMcqrCxK1smtAsiSqk0Jk9jpN2vcQgnMPypEdmEEXyWHwq55fjy6ch+sqYcwumIL5QcFW2JQ5+XBEoFhC66gOsAXow==&#xd;
27+
</passwordData>
28+
</GetPasswordDataResponse>"""
29+
30+
31+
class TestGetPasswordData(BaseAWSCommandParamsTest):
32+
33+
prefix = 'ec2 get-password-data'
34+
35+
def test_no_priv_launch_key(self):
36+
args = ' --instance-id i-12345678'
37+
cmdline = self.prefix + args
38+
result = {'InstanceId': 'i-12345678'}
39+
self.assert_params_for_cmd(cmdline, result)
40+
41+
def test_nonexistent_priv_launch_key(self):
42+
args = ' --instance-id i-12345678 --priv-launch-key foo.pem'
43+
cmdline = self.prefix + args
44+
result = {}
45+
self.assert_params_for_cmd(cmdline, result, expected_rc=255)
46+
47+
def test_priv_launch_key(self):
48+
driver = create_clidriver()
49+
key_path = os.path.join(os.path.dirname(__file__),
50+
'testcli.pem')
51+
args = ' --instance-id i-12345678 --priv-launch-key %s' % key_path
52+
cmdline = self.prefix + args
53+
cmdlist = cmdline.split()
54+
rc = driver.main(cmdlist)
55+
self.assertEqual(rc, 0)
56+
self.assertEqual(key_path, _get_key_path())
57+
service = driver.session.get_service('ec2')
58+
operation = service.get_operation('GetPasswordData')
59+
r = XmlResponse(driver.session, operation)
60+
r.parse(GET_PASSWORD_DATA_RESPONSE, 'utf-8')
61+
response_data = r.get_value()
62+
self.assertEqual(response_data['PasswordData'], '=mG8.r$o-s')

tests/unit/ec2/testcli.pem

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEowIBAAKCAQEAiaf6VsJ5w7emzh+6ffFj31ropPeW2N8wnNtfkItp4gmCdCpTQA4Kce/tzk5V
3+
VPTaPyanYXcdwib4KXqf77dSACxpqtel7jwotdLLNU9uUm9pIFsVh0qnLJxXdgJgujsY/HBq4BRE
4+
l7W1uA0e7QD1DH3QImELEXmnwO9ym/CiuwpUYGYrLLwIaezGjv/XUsn/KMzR2cQsseQu+jiBWXhA
5+
+4plnC4aNOWCXym/VlTgRDaGL8FijEzfiL+k4ZrG6/qakAt1xDkB8xGY4yOCexnL80Pi31ybT7QG
6+
odhuIkUr1CCgakvBpY1y8X8/yXmwZZtzl/qS47cIz9OUMjeFsfBBZwIDAQABAoIBAEXNN9PmqXfl
7+
GGBNFnPmg44uuulr4sH16uCfHMZe60IDMHNXQv+oHwPHdf63Ge4KeuCq6RUzIZPhztS5qYAUpTAR
8+
VUOcNjenqb0JNqHBtV93vwb5KOGBqWOlo3PjoMjOTs0y8/7MSDvlmE/L13K2mYvMAE5uhv5FghsD
9+
UEpiqyHMSl4gGqOxz1SbOLvmSdlmlLHW9qjrT+oEoqZX/nfQyJHk0zoZlfTJdnBL5R1GtTP2mI/E
10+
GCy4z7qllmGJR4x24GZxNAggr3mWvmkaVxivO2+3FN1TtwGJo/Wj5ncZLv889TXzj2V8HOcoZ8L/
11+
GsZGxdJ5rWrxRLQySePSUKze+gECgYEA27n4p6uJLF8Ra25nQmgs2/VpzM1K0fd1tX3yKCYFGwbl
12+
45Lzi7a672GQryAmSVWSfCgf9uwZmVWcPW1gZT2pBqP1+8EF1T2L6yPnGR1psF9jMmWGJogFlM4o
13+
P++Ym3SHT4bd0we1iUnEH6JS65BEmopVhM8/7ZP50WHibDddoRcCgYEAoGGSLRLpvwczgOlirNSM
14+
RCsJ9uAWUBZYMO1XMoAet0CWat9YwOqiiavcsD+0bNH0bZISQrLtaoiDSdFpRYjcgpBeu4MaGGqJ
15+
EXYUGxNwmY1TZ3ml+9Oh1I84p0XIALeCueEFH9lJaXHzdhjNDn+KxKUg7UIz72+v3n/AyhvqdDEC
16+
gYEAsyUwR7xCreug7z9nbywyju/LYBBtFT22OdBC9FrzRLLeEirI6LuGNBAO/8mtjZL4SMQKM68R
17+
vAOhzC92LXUVb3WU47rff5mbj46JJ9/kQMm0ve0qcBXsvwNKq740ZWKfw8ZI63rYluOOxN/6zVal
18+
qH5q9Upoa9J/FyjAi8ykSOcCgYBJknjsFHEGINePm4CYqChwXQ4FImcZ9iYey8HkeMGebxKRlEOy
19+
u/A0F5L1h0PNZ8MpQIj/7/TZmiYgBuCz9USy4GeUvV+LM9QNHo26ngBZcGuCXFu4Wi0yxUDH+0r0
20+
iTp+6qrfIV578LouwtHOhNOzwcyJCoWooSOcfh6CmKvFAQKBgH+6tVQi9Tj4Ig15HueURVVeiPVn
21+
L9MPiR4unUsPz6OevoNsRaDkj7XDFBuMiFPGoXoyL9SedSSFw+oRxcPi4YMMvsZNC0yxOnj2Qe8z
22+
V1Xgvb+rlN6NEIlPcscxE2F8gu1zIan/lXLMEXMr9hAAaGqS/JSALzt6XcoHwzEGnTrH
23+
-----END RSA PRIVATE KEY-----

tests/unit/test_clidriver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def test_expected_events_are_emitted_in_order(self):
205205
'top-level-args-parsed',
206206
'building-command-table.s3',
207207
'building-argument-table.s3.ListObjects',
208+
'operation-args-parsed.s3.ListObjects',
208209
'process-cli-arg.s3.list-objects',
209210
])
210211

0 commit comments

Comments
 (0)