前言
最近,我花了一些時間在編譯、優化我的一臺執行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領域應用、移動應用以及機器學習等的開發,也從事過多個大型軟體專案的設計、開發與專案管理。目前他專注於雲端計算以及網際網路等技術領域,致力於幫助中國的開發者構建基於雲端計算的新一代的網際網路應用。