前言
最近,我花了一些时间在编译、优化我的一台运行OpenWRT的路由器,型号是 Linksys WRT 1900ACS 。为了扩展路由器的功能,就需要在OpenWRT上开发一些新的功能,尤其是需要使用到亚马逊云科技上的一些有趣的服务。当我习惯性地开始使用Amazon SDK 的时候才突然意识到,在一台硬件配置不高,软件极度精简的系统中使用这些 SDK 无疑是一件极为奢侈的想法。即使如我手上的这台硬件配置颇高的路由器,也不过只有128M 的存储、512M的内存资源而已。
这或许让我的工作更加有趣,让我可以更深入地去研究亚马逊云科技的API的调用机制以及如何更有效使用,而不是依赖于高度封装好的SDK。这个任务的主要挑战是成功的执行经过身份验证的Amazon REST API请求,例如EC2 的安全组、VPC的ACL规则、Amazon S3上文件的存取以及其它一些有意思的功能。
为什么?
我的工作并非如“黑客”那样的非法使用系统。事实上亚马逊云科技早就针对REST API 的调用提供了标准的调用接口,其中最关键的环节就是名为Signatura Version 4 的API请求报头的签名过程。在亚马逊云科技的文档中对这个流程有详细的介绍,但我相信应该只有很少的人能够读完这个文档,原因是因为这个过程实在是繁-琐-无-比。理智的开发者通常会忽略这些API而更习惯于使用各类亚马逊云科技的SDK。甚至,某些简单的任务也完全可以通过Amazon-Cli,使用一段脚本来解决问题。
但是,就像我的情况一样。某些场景下可能无法使用适用于工作平台或者编程语言的SDK,这些需求包括但不仅限于这些
1.资源限制。例如嵌入式环境中
2.性能要求。例如性能较低的CPU这是我做的一个简单的性能对比。场景是针对S3 上的一个文件下载到本地
3.SDK的缺失。例如macOS 上的Amazon SDK
4.缺少特定的语言的SDK。例如Rust等 (注:rusoto为非官方的SDK包)
5.现有SDK功能的缺失。例如 Amazon Transcribe 实时转录的功能
5.减少依赖。例如使用Python 的boto3, 就需要安装这样的一些依赖项python3、python3-yaml、python3-pyasn1、python3-botocore、python3-rsa、 python3-colorama、python3-docutils、python3-s3transfer 等等
此外,了解并掌握了Amazon REST API 的细节,对于开发人员在进行系统优化、架构设计以及提升系统安全性等方面一定大有裨益。
我们需要的工具
对于这项任务,我们将会用到:
1.python3+ (python2 理论上也可以实现,但我没有去尝试)2.可以安装Python 的requests 包(pip3 install requests)。也可以使用Python内置的urllib而不用requests。
2.文本编辑器 (例如我常用的vim)
3.curl (用来请求 Web 服务的命令行工具)
4.openssl (安全通信的基础软件包)
5.sed (一种流编辑器,常用于Linux 脚本中)
我们将使用这些工具分别在Python程序以及shell 脚本中实现对于Amazon API的调用。通常,亚马逊云科技的SDK (例如用于Python的boto3)会帮助我们的应用自动完成请求的签名,因此对于开发者来说这个环节是透明的。而对于今天的这个任务我们将需要自己动手完成最重要的签名的操作。
相关的参考实现
类似于我的这个想法,早就有人实践过并分享出来。其中较为知名的有这样几个:
1.requests-amazon4auth
https://github.com/sam-washin...
Amazon Web Service身份验证版本4 的Python Request库的
2.amazon-requests-auth
https://github.com/DavidMulle...
亚马逊云科技签名版本4签名过程的Python requests module
3.amazon-request-signer
https://github.com/iksteen/aw...
使用亚马逊云科技签名V4签署亚马逊云科技请求的Python库
上述3个开源的Python库,除了最后一个在4个月前有过更新以外,其它的两个已经超过2年以上没有更新了,很难有信心去使用啊!最后介绍的一个比较有趣,因为这个方法没有使用boto3 却利用botocore 来实现签名,算是一种投机取巧的做法。
# Key derivation functions. See:
# http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python
defsign(key, msg):
returnnew(key, msg.encode('utf-8'), hashlib.sha256).digest()
defgetSignatureKey(key, dateStamp, regionName, serviceName):
kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp)
kRegion = sign(kDate, regionName)
kService = sign(kRegion, serviceName)
kSigning = sign(kService, 'aws4_request')
returnkSigning
为什么需要对API的请求签名?
几乎亚马逊云科技所有服务的每一个功能都提供了一个API,并且这些API都是REST API。这就意味着我们可以通过HTTP 请求的方式完成对于Amazon API 的调用。实现这样的调用是非常简单的事情,但是我们还需要在这个调用过程中满足这样的三个需求:
1.验证请求者的身份
确保请求是由某个具有有效访问密钥的用户发送的
2.保护传输中的数据
为了防止传输时请求被篡改,一些请求元素将用于计算请求的哈希(摘要),得到的哈希值将包括在请求中。在Amazon服务收到请求时,它将使用相同信息计算哈希,并将其与请求中包括的哈希值进行匹配。如果值不匹配,Amazon将拒绝请求。
3.防止潜在的反演攻击
在大多数情况下,请求必须在请求中的时间戳的5分钟内到达Amazon。否则,Amazon将拒绝该请求。
这就引入了非常重要的一个方法-签名请求。当我们的应用将HTTP 请求发送到Amazon时,需要对请求签名,以便Amazon能够识别发送它们的用户。使用Amazon访问密钥来签名请求,该访问密钥包含访问密钥 ID 和秘密访问密钥。有一些请求不需要签名,如发送到Amazon S3的匿名请求以及Amazon STS 中的一些 API 操作以外,其它的API 请求都需要签名。
Signature Version 4 的工作流程
要对请求签名,先要计算请求的哈希 (摘要)值。然后,使用这个哈希值、来自请求的其他一些信息以及Amazon私密访问密钥,计算另一个称为“签名”的哈希值。
1.针对签名版本 4 创建规范请求将请求的内容(主机、操作、标头等)组织为标准(规范)格式。规范请求是用于创建待签字符串的输入之一。请求规范具有以下格式:
“ HTTP_Method” \ n“ Canonical_URI” \ n“ Canonical_Query” \ n“ Canonical_Headers” \ n“ Signed_Headers” \ n“ Request_payload”
2.创建签名版本 4 的待签字符串使用规范请求和额外信息(例如算法、请求日期、凭证范围和规范请求的摘要(哈希))创建待签字符串。字符串具有以下格式:
“AWS4-HMAC-SHA256”\n “UTC 日期” \n“日期/区域ID / s3 / aws4_request” \ n“ Canonical_str”
3.为 Amazon Signature 版本 4 计算签名使用Amazon秘密访问密钥作为初始哈希操作的密钥,对请求日期、区域和服务执行一系列加密哈希操作(HMAC 操作),从而派生签名密钥。在派生签名密钥后,通过对待签字符串执行加密哈希操作来计算签名。使用派生的签名密钥作为此操作的哈希密钥。格式如下:
MAC_SHA256(HMAC_SHA256(HMAC_SHA256(HMAC_SHA256(“Amazon4”秘密密钥,日期),区域ID),“ s3”),“amazon4_request”)
4.向 HTTP 请求添加签名在计算签名后,将其添加到请求的 HTTP 标头或查询字符串中。具体说来,就是使用步骤3中的签名密钥,将步骤2中创建的签名字符串的SHA256 HMAC计算结果转换为十六进制字符。格式如下:
HMAC_SHA(签名密钥,签名字符串)
接下来,可以通过以下两种方式之一将签名添加到请求:
1.使用 HTTP Authorization 标头
2.将查询字符串值添加到请求中。由于签名是 URL 的一部分,因此这类 URL 被称为预签名URL
Amazon服务收到请求后,将执行您完成的相同步骤来计算请求中发送的签名。之后,Amazon会将计算得到的签名与您在请求中发送的签名进行比较。如果签名匹配,则处理请求。如果签名不匹配,则拒绝请求。
关于实现的细节,我们可以通过两个关键的函数一窥究竟(Python 代码)
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
importsys
importos
importdatetime
importhashlib
importhmac
importrequests
fromexceptions importHTTPError
fromexceptions importTimeout
ALGORITHM = 'AWS4-HMAC-SHA256'
METHOD = 'GET'
def_sign(key, msg):
returnnew(key, msg.encode('utf-8'), hashlib.sha256).digest()
defget_SignatureKey(key, dateStamp, regionName, serviceName):
date = _sign(('AWS4'+ key).encode('utf-8'), dateStamp)
region = _sign(date, regionName)
service = _sign(region, serviceName)
signing = _sign(service, 'aws4_request')
returnsigning
defget_key():
returnenviron.get('AWS_ACCESS_KEY_ID'), \
environ.get('AWS_SECRET_ACCESS_KEY')
defget_datetime():
current = datetime.datetime.utcnow()
returnstrftime('%Y%m%dT%H%M%SZ'), current.strftime('%Y%m%d')
defget_endpoint(service, region):
return'https://{}.{}.amazonaws.com'.format(service, region)
defget_host(endpoint):
returnreplace('https://', '')
defget_reqUrl(endpoint, canonical_querystring):
return'{}?{}'.format(endpoint, canonical_querystring)
defget_header(region, service, request_parameters):
amzdate, datestamp = get_datetime()
endpoint = get_endpoint(service, region)
host = get_host(endpoint)
access_key, secret_key = get_key()
ifaccess_key is None or secret_key is None:
print('No access key is available.')
exit()
canonical_uri = '/'
canonical_querystring = request_parameters
canonical_headers = 'host:{}\nx-amz-date:{}\n'.format(host, amzdate)
signed_headers = 'host;x-amz-date'
payload_hash = hashlib.sha256(('').encode('utf-8')).hexdigest()
canonical_request = '{}\n{}\n{}\n{}\n{}\n{}'.format(
METHOD,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
payload_hash
)
credential_scope = '{}/{}/{}/aws4_request'.format(
datestamp, region, service)
string_to_sign = '{}\n{}\n{}\n{}'.format(
ALGORITHM,
amzdate,
credential_scope,
sha256(
encode('utf-8')).hexdigest()
)
signing_key = get_SignatureKey(secret_key, datestamp, region, service)
signature = hmac.new(
signing_key,
(string_to_sign).encode('utf-8'),
sha256
).hexdigest()
authorization_header = \
'{} Credential={}/{},SignedHeaders={},Signature={}'.format(
ALGORITHM,
access_key,
credential_scope,
signed_headers,
signature
)
headers = {'x-amz-date': amzdate, 'Authorization': authorization_header}
request_url = get_reqUrl(endpoint, canonical_querystring)
return request_url, headers
defmain():
service = 'ec2'
region = 'us-west-1'
action = 'DescribeInstances'\
'&Filter.1.Name=instance-state-name&Filter.1.Value.1=running'
version = "2016-11-15"
request_parameters = 'Action={}&Version={}'.format(action, version)
request_url, headers = get_header(region, service, request_parameters)
print('\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++')
print('Request URL = {}'.format(request_url))
print('Request header = {}'.format(str(headers)))
try:
res = requests.get(request_url, headers=headers, timeout=(2, 5))
except Timeout:
print('The request timed out')
except HTTPError as http_err:
print(f'HTTP error occurred: {http_err}')
except Exception as err:
print(f'Other error occurred: {err}')
else:
print('\nRESPONSE++++++++++++++++++++++++++++++++++++')
print('Response code: %d\n' % res.status_code)
print(res.text)
if__name__ == "__main__":
main()
使用Python3、rrequests 实现的对于Amazon Translate 的调用,实现英文-中文的翻译
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Writen by Lianghong2020-03-12 11:42:56
importsys
importos
importdatetime
importhashlib
importhmac
importjson
importrequests
fromexceptions importHTTPError
fromexceptions importTimeout
ALGORITHM = 'AWS4-HMAC-SHA256'
METHOD = 'POST'
def_sign(key, msg):
returnnew(key, msg.encode("utf-8"), hashlib.sha256).digest()
defget_SignatureKey(key, datestamp, regionName, serviceName):
k_date = _sign(('AWS4'+ key).encode('utf-8'), datestamp)
k_region = _sign(k_date, regionName)
k_service = _sign(k_region, serviceName)
k_signing = _sign(k_service, 'aws4_request')
returnk_signing
defget_key():
returnenviron.get('AWS_ACCESS_KEY_ID'), \
environ.get('AWS_SECRET_ACCESS_KEY')
defget_datetime():
current = datetime.datetime.utcnow()
returnstrftime('%Y%m%dT%H%M%SZ'), current.strftime('%Y%m%d')
defget_host(service, region):
return'{}.{}.amazonaws.com'.format(service, region)
defget_header(service, region, request_parameters):
access_key, secret_key = get_key()
ifaccess_key is None or secret_key is None:
print('No access key is available.')
exit()
amz_date, date_stamp = get_datetime()
host = get_host(service, region)
canonical_uri = '/'
canonical_querystring = ''
content_type = 'application/x-amz-json-1.1'
amz_target = 'AWSShineFrontendService_20170701.TranslateText'
canonical_headers = \
'content-type:{}\nhost:{}\nx-amz-date:{}\nx-amz-target:{}\n'.format(
content_type,
host,
amz_date,
amz_target
)
signed_headers = 'content-type;host;x-amz-date;x-amz-target'
payload_hash = hashlib.sha256(
encode(
'utf-8'
)).hexdigest()
canonical_request = '{}\n{}\n{}\n{}\n{}\n{}'.format(
METHOD,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
payload_hash
)
credential_scope = '{}/{}/{}/aws4_request'.format(
date_stamp, region, service)
string_to_sign = '{}\n{}\n{}\n{}'.format(
ALGORITHM, amz_date, credential_scope,
sha256(canonical_request.encode('utf-8')).hexdigest()
)
signing_key = get_SignatureKey(secret_key, date_stamp, region, service)
signature = hmac.new(
signing_key,
(string_to_sign).encode('utf-8'),
sha256).hexdigest()
authorization_header = \
'{} Credential={}/{},SignedHeaders={},Signature={}'.format(
ALGORITHM,
access_key,
credential_scope,
signed_headers,
signature
)
headers = {'Content-Type': content_type,
'X-Amz-Date': amz_date,
'X-Amz-Target': amz_target,
'Authorization': authorization_header}
return headers
defmain():
service = 'translate'
region = 'ap-northeast-1'
host = get_host(service, region)
endpoint = 'https://{}/'.format(host)
text = 'Amazon Translate is a text translation service that use '\
'advanced machine learning technologies to provide high-quality '\
'translation on demand. You can use Amazon Translate to translate '\
'unstructured text documents or to build applications that work in '\
'multiple languages.'\
'Amazon Translate provides translation between a source language '\
'(the input language) and a target language (the output language). ' \
'A source language-target language combination is known as a '\
'language pair.'
source_lang_code = 'en'
target_lang_code = 'zh'
request_parameters = '{{"{}": "{}","{}": "{}","{}": "{}"}}'.format(
"Text",
text,
"SourceLanguageCode",
source_lang_code,
"TargetLanguageCode",
target_lang_code
)
headers = get_header(service, region, request_parameters)
# print('endpoint is ==>\n{}\n'.format(endpoint))
# print('request_parameters is ==>\n{}\n'.format(request_parameters))
# print('headers is ==>\n{}\n'.format(headers))
try:
res = requests.post(
endpoint,
data=request_parameters,
headers=headers
)
except Timeout:
print('The request timed out')
except HTTPError as http_err:
print(f'HTTP error occurred: {http_err}')
except Exception as err:
print(f'Other error occurred: {err}')
else:
json_content = json.loads(res.text)
print('The original is -->\n{}\n'.format(text))
print('The translation is -->\n{}\n'.format(
json_content['TranslatedText']
))
# print('Response:\n\t{}'.format(res.text))
if__name__ == "__main__":
main()
如果不喜欢Python也没有关系。即使shell的脚本仅仅使用curl、openssl以及sed,就可以实现上传文件到Amazon S3的存储桶之中的操作
content-type:${contentType}
host:${bucket}${baseUrl}
x-amz-content-sha256:${payloadHash}
x-amz-date:${dateValueL}
x-amz-server-side-encryption:AES256
x-amz-storage-class:${storageClass}
${headerList}
${payloadHash}"
# Hash it
canonicalRequestHash=$(printf '%s'"${canonicalRequest}"| openssl dgst -sha256 -hex 2>/dev/null | sed 's/^.* //')
# 2. Create string to sign
stringToSign="\
${authType}
${dateValueL}
${dateValueS}/${region}/${service}/aws4_request
${canonicalRequestHash}"
# 3. Sign the string
signature=$(awsStringSign4 "${awsSecret}""${dateValueS}" "${region}" "${service}" "${stringToSign}")
# Upload
curl -s -L --proto-redir =https -X "${httpReq}"-T "${fileLocal}" \
-H "Content-Type: ${contentType}" \
-H "Host: ${bucket}${baseUrl}" \
-H "X-Amz-Content-SHA256: ${payloadHash}" \
-H "X-Amz-Date: ${dateValueL}" \
-H "X-Amz-Server-Side-Encryption: AES256" \
-H "X-Amz-Storage-Class: ${storageClass}" \
-H "Authorization: ${authType} Credential=${awsAccess}/${dateValueS}/${region}/${service}/aws4_request, SignedHeaders=${headerList}, Signature=${signature}" \
"https://${bucket}${baseUrl}/${fileRemote}"
纸上得来终觉浅,绝知此事要躬行。最初开始阅读Signature Version 4 的文档倍觉繁琐,几乎不能坚持下去。屡经挫折,尤其是那个脚本实现的S3上传的例子足足折磨了我一天的时间。但是当成功的完成几个例子之后就顿时觉得融会贯通,欲罢不能了。这个小小的实践,让我对于Amazon API的设计与实现有了更进一层的了解。
参考资料
Signatura Version:
https://docs.aws.amazon.com/z...
Amazon-Cli:
https://aws.amazon.com/it/cli/
本篇作者
费良宏
Amazon Web Services Principal Developer Advocate
在过去的20多年一直从事软件架构、程序开发以及技术推广等领域的工作。他经常在各类技术会议上发表演讲进行分享,他还是多个技术社区的热心参与者。他擅长Web领域应用、移动应用以及机器学习等的开发,也从事过多个大型软件项目的设计、开发与项目管理。目前他专注于云计算以及互联网等技术领域,致力于帮助中国的开发者构建基于云计算的新一代的互联网应用。