前言
最近,我花了一些时间在编译、优化我的一台运行 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-washington/requests-aws4auth
Amazon Web Service 身份验证版本 4 的 Python Request 库的
2.amazon-requests-auth
https://github.com/DavidMuller/aws-requests-auth
亚马逊云科技签名版本 4 签名过程的 Python requests module
3.amazon-request-signer
https://github.com/iksteen/aws-request-signer
使用亚马逊云科技签名 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/zh_cn/AmazonS3/latest/API/API_PutObject.html
Amazon-Cli:
https://aws.amazon.com/it/cli/
本篇作者
费良宏
Amazon Web Services Principal Developer Advocate
在过去的 20 多年一直从事软件架构、程序开发以及技术推广等领域的工作。他经常在各类技术会议上发表演讲进行分享,他还是多个技术社区的热心参与者。他擅长 Web 领域应用、移动应用以及机器学习等的开发,也从事过多个大型软件项目的设计、开发与项目管理。目前他专注于云计算以及互联网等技术领域,致力于帮助中国的开发者构建基于云计算的新一代的互联网应用。
评论