使用 Python 驗證 AWS S3 Signed Url Signature

Evan
evan.fang
Published in
10 min readMay 21, 2022

如果要將 AWS S3 上的非公開的檔案,暫時地分享給其他人存取,可以使用 S3 的 presigned url 的機制。詳細資訊可以參考官方文件

先看看 signed url 的模樣吧:

https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404

這個 signed url 是嘗試取得 examplebucket 底下的 test.txt 這個檔案。可以看到在 query string 的部分,跟了一大堆訊息,這些訊息就是用來製作 X-Amz-Signature 這個欄位所攜帶的數位簽章。

signed url 主要的概念是,物件擁有者可以使用 private key ,透過特定步驟,製作一個包含了數位簽章的 URL,就像是對該 URL 進行了簽名,向 AWS 表示:「嘿,這是我簽名過的 URL,任何人只要是原封不動地拿著這個網址來向你要求檔案,就給他吧!」

對 AWS 來說,當然不是看到有簽章 URL 就照單全收。AWS 會驗證該簽章是否有效;也就是使用者出示的簽章,是否就是物件持有者簽署的簽章?使用者出示的 URL,有沒有被篡改過?該簽章是否過期?

製作簽章的步驟,可以說相當繁複,必須按照特定的順序,選取指定的欄位,使用特定的編碼、加密方式,才能產生符合 AWS 認可的簽章。一般來說,產生 presigned url,可以使用 AWS SDK 來處理;如 python 的 boto3 就可以輕易完成(參考這裡)。

但簽章是怎麼製作出來的,又該如何驗證?

以下將以 Authenticating Requests: Using Query Parameters (AWS Signature Version 4) 這篇官方文章作為範例,示範如何使用 Python 來驗證 signed url 中的簽章。

首先,把 signed url 稍微排列一下,如此一來比較能看出 url 的每個部分。

https://s3.amazonaws.com/examplebucket/test.txt
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=<your-access-key-id>/20130721/us-east-1/s3/aws4_request
&X-Amz-Date=20130721T201207Z
&X-Amz-Expires=86400
&X-Amz-SignedHeaders=host
&X-Amz-Signature=<signature-value>

各個部分所代表的意涵:

https://s3.amazonaws.com/examplebucket/test.txt:網址列,包含了 bucket name,以及物件的 key,表明要存取哪個東西。

X-Amz-Algorithm 指名使用的加密算法是 HMAC-SHA256

X-Amz-Credential 包含了四個部分,以 / 區隔開,分別是:

  • Access Key Id
  • 製作簽章的日期(格式為YYYYMMDD)
  • region,此例中為 us-east-1
  • service 類型,此例中理所當然的是 s3
  • 最後是固定字串 aws4_request,代表了 AWS Signature Version 4

X-Amz-Date,代表了製作簽章當下的時間點,也是計算簽章有效期限的時間起始點。

X-Amz-Expires,代表了該簽章的有效期間,單位是秒。如範例中為 86400,即代表此簽章有效期限是一天。

X-Amz-SignedHeaders,這個欄位稍微複雜了些,其代表的是「有哪些 http request headers有被包含在簽章中」。例如本例的 host,就指示了驗證簽章的時候,必須要連著 header 中的 host 欄位一起驗證,換句話說,在使用 URL 時,如果沒有在 header 中帶上正確的 host 值的話,簽章將會驗證失敗。至於如果有多個 header 都需要被驗證,那麼將以分號隔開各個 header,如:X-Amz-SignedHeaders=host;x-amz-server-side-encryption

X-Amz-Signature 就是使用了以上資訊,所製作出來的簽章。在範例中,最後製作出的數位簽章長這樣:

aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404

因此,驗證簽章的方式就是把 URL 的每個段落解開,加上 http header 的資訊,按照指定的步驟算出數位簽章,然後再跟 X-Amz-Signature 所攜帶的數位簽章比較。

如果兩者相等,而且簽章沒有過期,那麼該 signed url 就算是通過驗證。

首先,依照範例,使用以下這組 AWS Access Key and Secret

AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE
AWSSecretAccessKey=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

範例 URL(為方便閱讀,以下範例將每個 query string 的 parameter 以斷行隔開):

https://s3.amazonaws.com/examplebucket/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404

Step 1. 組合出 Canonical request

Canonical request 包含以下部分:

Http method
Resource
Canonical query string
Canonical headers
Signed headers
UNSIGNED-PAYLOAD
  • Http method: GET
  • Resource:代表要存取的物件 /test.txt
  • Canonical query string:把 URL 中的 query string,按照 param 的字母順序排序,並使用 url encode 的方式 encode 之(例如本範例中的 / 會被 encode 為%2F),再重新以 & 串接起來。
  • Canonical headers:把 X-Amz-SignedHeaders 指定的 header(本範例中為 host),從 http request header 中取出,按照 header 的字母順序排序,並使用 url encode 的方式 encode 之後,再使用 “ : ” 連接 header 及其 value,最後別忘了加上 “\n”
  • Signed headers:X-Amz-SignedHeaders 指定的 header,如果有多個,就使用 “ ; ” 隔開。本範例中僅有 host。
  • UNSIGNED-PAYLOAD:固定字串

結果如下:

GET
/test.txt
X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host
host:examplebucket.s3.amazonaws.com
host
UNSIGNED-PAYLOAD

Step 2. 製作 String To Sign

接著,準備好要用來簽章的字串。此字串包含以下部分:

  • Algorithm:固定為 AWS4-HMAC-SHA256
  • Datetime:X-Amz-Date 攜帶來的日期字串 20130524T000000Z
  • X-Amz-Credential 攜帶來的字串,去除掉 Access Key Id 之後的各個部分,以 / 分隔,也就是:20130524/us-east-1/s3/aws4_request。
  • 最後是將 canonical request 以 sha256 hash 過後,再使用 hex encode 的結果。encode 的方式請參考以下程式碼片段:

綜上所述,產生 string to sign 如下:

AWS4-HMAC-SHA256
20130524T000000Z
20130524/us-east-1/s3/aws4_request
3bfa292879f6447bbcda7001decf97f4a54dc650c8942174ae0a9121cf58ad04

有了要簽署的文件之後,接著製作要用來簽署文件的鑰匙。

Step 3. 製作 signing key

signing key 的製作方式是,將 secret access key,透過執行四次的 HMAC-SHA256 運算而得,每一次的運算都會混入不同的資訊:

  • Key1 = HMAC-SHA256( “AWS4” + secret access key, <yyyymmdd>)
  • Key2 = HMAC-SHA256(Key1, <aws-region>)
  • Key3 = HMAC-SHA256(Key2, <aws-service>)
  • Signing key = HMAC-SHA256(Key3, “aws4_request”)

注意到,以上資訊如:access key id、date、region、service,都是 query string 中的 X-Amz-Credential 帶來的資訊。

Step 4. Make Signature and Verify

有了 string to sign 以及 signing key,就可以製作簽章。一樣是利用 HMAC-SHA256 算法,然後別忘了轉為 hex 表示法。

hmac_sha256(signing_key, string_to_sign).hex()# output:
# aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404

至此,已經可以確認,我們根據 URL 所製作出來的簽章,與 URL 中的 X-Amz-Signature 所使用的簽章是相同的。這表示發出 request 給我們的人,原封不動的將物件持有者所簽署的 URL 按照指定的方式轉發給我們。

確認 URL 沒問題後,別忘了檢查 URL 是否過期。跟製作簽章的過程相比,最後一步可算是小菜一碟了。

Step 5. Check URL Expiry

將 X-Amz-Date 的時間字串轉換成 datetime 物件,再從 X-Amz-Expires 取出秒數,相加之後跟當前時間比較一下即可。注意當前時間要使用 UTC time。

--

--