この記事を1行で
boto3で発行したS3の署名付きURLは、3年前に廃止された形式になっている
S3の署名付きURLを調べていたら…
2023年の夏の日のことにございます。
もともとは、「CloudFront署名付きURLがわりと難のある仕組みなので、S3の署名付きURLと比べるとこれだけセキュリティが緩い」みたいな記事を書こうと思っていました。
記事を書くためにS3とCloudFrontの性能比較が必要でした。S3のほうが安全であることの裏付けをとるために、まっさらなS3バケットを作って、公式ドキュメントの設定方法を調べなおして、最新のSDKをインストールして、公式のサンプル通りの手順でS3の署名付きURLを発行することにしました。
boto3のバージョンは1.28.25
です。2023年の8月12日にリリースされたばかりです。
署名付きURLの発行方法はAWSで紹介されているドキュメントをそのまま利用します。
# Generate a presigned URL for the S3 object
s3_client = boto3.client('s3')
try:
response = s3_client.generate_presigned_url('get_object',
Params={'Bucket': bucket_name,
'Key': object_name},
ExpiresIn=expiration)
except ClientError as e:
logging.error(e)
return None
# The response contains the presigned URL
return response
最新のSDKで、公式ドキュメント通りの方法で、署名付きURLを取るだけです。
ソースも簡潔です。S3の署名は安全で、しかも簡単にとr………
# とってきた結果
https://xxxxxxxxxxxxxxxxxxxxxxx.s3.amazonaws.com/shell.sh
?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXXX
&Signature=x3GFwubkxvUtzcoKy8kfSl%2BsReg%3D
&Expires=1691901031
………おん?
……………(つд⊂)ゴシゴシ
……………………???
亡霊を見た
奇妙な署名が返ってきた気がしますが、気のせいかもしれません。
pythonのSDKではなく、JavascriptのSDKで署名付きURLを発行してみます。
pythonと同じ条件で署名付きURLを発行します。ライブラリは最新にして、署名付きURLの発行方法はAWS-SDK V3のドキュメントをそのまま引用します。
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// 署名付きURLを発行する
const createPresignedUrlWithClient = ({ region, bucket, key }) => {
const client = new S3Client({ region });
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
return getSignedUrl(client, command, { expiresIn: 3600 });
};
// 署名付きURLの発行結果を参照
createPresignedUrlWithClient({
region: "ap-northeast-1",
bucket: bucket_name,
key: object_name,
}).then((result) => {
console.log(result);
});
署名付きURLを発行してみます。
# とってきた結果
https://xxxxxxxxxxxxxxxxxxxxxxxxxx.s3.ap-northeast-1.amazonaws.com/shell.sh
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD
&X-Amz-Credential=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
&X-Amz-Date=20230813T232839Z
&X-Amz-Expires=3600
&X-Amz-Signature=10dc3185aea30ad3baa44898ab0dbf0488346bf64ef3a7168e18ffe3a8dab35d
&X-Amz-SignedHeaders=host
&x-id=GetObject
一目見ただけで、pythonで発行した署名付きURLとは違うフォーマットです。
分かりやすい特徴を上げると、以下の通りです。
- Expiresは有効期限の秒数で指定されます。
- AlgorithmにはAWS4(V4署名)であることが書いてあります
Pythonで見た亡霊の正体
公式ドキュメントを開いて、3年前に廃止された形式(V2署名)を探します。
懐かしいですね。V2署名には以下のような特徴がありました。
- Expiresは1970年からの経過秒数を指定します。
- Algorithmの指定はありません。
ドキュメントには、V2の署名付きURLが以下の形式であることが書いてあります。
pythonで発行した署名付きURLと同じです。短いフォーマットです。
GET /photos/puppy.jpg
?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE
&Expires=1141889120
&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D
廃止された形式ですから、ドキュメントの先頭には大きく注意書きが書いてあります。
署名バージョン2は無効(非推奨)にされているため、Amazon S3は署名バージョン4を使用して署名されたAPIリクエストのみを受け入れます。
きみは3年前に消えたはずじゃなかったのか
Amazon S3 アップデート — SigV2 の廃止時期、延期と変更
V2署名は、AWSが初期から利用してきた署名形式でした。
後継のV4が発表されたあとは、そちらへの移行が進みました。
引退が始まったのは10年前です。2013年以降に開始されたすべてのリージョンでは、V2署名はサポートされず、後継のV4署名だけをサポートすることになりました。2019年の6月になり、V2のサポートが完全に終了すること、現行環境のV2署名が動かなくなることが決まりました。
それから1度だけサポートが延長されましたが、2020年の6月以降に作成された新規のバケットではV2は使えない、という最終のアナウンスがありました。
2020年6月24日以降に作成された新しいバケットは SigV2 署名付きリクエストはサポートされません。ただし、既存のバケットについて引き続き SigV2 がサポートされますが、我々はお客様が古いリクエスト署名方法から移行するよう働きかけます。
一応確かめてみる
2013年から段階を踏んで、V2は「俺、消えっから」したはずです。
もしかするとティーダとシューインみたいなものかもしれない。V2に見えるV4署名かもしれないので、念のため検証してみます。
V2とV4を比べると、2つの署名は有効期限に大きな違いがあります。
- V4の有効期限は7日間まで設定することができる
- V2の有効期限は数年単位の規模で設定することができる
S3のドキュメントにも、7日間より長い有効期限を持つ署名付きURLは発行できないと書いてあります。
5日間の有効期限で署名してみます。
$ curl "https://xxxxxxxxxxxxxxxxxxxxxxxxxxx.s3.amazonaws.com/shell.sh
?AWSAccessKeyId=xxxxxxxxxxxxxxxxxxxxx
&Signature=hJfoBQcxV0QlGgC%2FuRnFhYd%2B7AU%3D
&Expires=1692403443"
> 200 OK
通ります。
2年間の有効期限で署名してみます。
$ curl "https://xxxxxxxxxxxxxxxxxxxxxxxx.s3.amazonaws.com/shell.sh
?AWSAccessKeyId=xxxxxxxxxxxxxxxxxxx
&Signature=y1xg%2BFiDUJrB8sqwOO%2BD7ES0iZo%3D
&Expires=1755043571"
> 200 OK
通りました。
15年間有効な署名を発行させてみます。
curl "https://xxxxxxxxxxxxxxxxxxxx.s3.amazonaws.com/shell.sh
?AWSAccessKeyId=AKIA4MMMID2QLIOQ3BP4
&Signature=KGZXQ3FfuuLMTV8aIf7jD%2FvN398%3D
&Expires=2165011769"
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Invalid date (should be seconds since epoch): 2165011769</Message><RequestId>03DS7EX9Z2AA81Y9</RequestId><HostId>JMJka1sJYX0eThd+9X7XHJFuqGjvhnGr2P72ARubrp9iCQdfn3CE+MqwpuVohbWUba0yQtR51N4=</HostId></Error>
エラーになりました。
Javascript版でも確認:10日間有効な署名を発行してみます
Javascript版は、7日より長い有効期限を設定した時点で例外を投げました。
$ node main.mjs
Error: Signature version 4 presigned URLs must have an expiration date less than one week in the future
挙動から見ても、pythonの署名は3年前に消えたはずのV2で間違いなさそうです。
お盆だからでしょうか。V2さん、おかえりなさい。
botoを見てみた
githubを見たらissueがありました。
Botocore does not make S3 Signed URLs with SigV4
https://github.com/boto/botocore/issues/2109
Q:「BotocoreがSigV4で署名してくれない。ソースのここで置き換えがあるから、めちゃくちゃ古い形式で署名してる(This replaces the correct v4 version with one that maps to the very old HmacV1QueryAuth signer.)」
A: 「Botocoreは明示的に設定されない限り、以前と同じようにV2で署名します。V2とV4では後方互換性がないからです(Botocore still uses Sigv2 for generating presigned url unless it has explicitly configured to use Sigv4 because it is a backward incompatible change to switch them from v2 to v4.)」
botoの仕様らしいです…が、V2は廃止されて3年がたちます。
いまだに動いている理由は分かりません。
我々はどうすればいいのか
いつまでも亡霊に頼るわけにはいきません。
signature_versionにs3v4
を指定すると、pythonでもV4で署名を発行できます。
import boto3
from botocore.config import Config
def presign():
my_config = Config(region_name="ap-northeast-1", signature_version="s3v4")
s3 = boto3.client("s3", config=my_config)
s3.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": bucket_name,
"Key": file_name,
},
ExpiresIn=3600,
)
発行した署名は他の言語のSDKと同じV4の形式です。正しく動作します。
# とってきた署名付きURL
$ curl "https://xxxxxxxxxxxxxxxxxxxxxx.s3.amazonaws.com/shell.sh
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=xxxxxxxxxxxxxxxxxxxx%2F20230814%2Fap-northeast-1%2Fs3%2Faws4_request
&X-Amz-Date=20230814T093156Z
&X-Amz-Expires=432000
&X-Amz-SignedHeaders=host
&X-Amz-Signature=519d25006a16fcb2c000e6bf8798827994b37390bbc9327f7c88a0f74b4d8d97"
> 200 OK
有効期間を延ばして、1年間有効な署名付きURLを発行してみます。
$ curl "https://xxxxxxxxxxxxxxxxxx.s3.amazonaws.com/shell.sh
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=xxxxxxxxxxxxxxxxxxxx%2F20230814%2Fap-northeast-1%2Fs3%2Faws4_request
&X-Amz-Date=20230814T001851Z
&X-Amz-Expires=31536000
&X-Amz-SignedHeaders=host
&X-Amz-Signature=82b924dcbae9d65af789081e749b0e75bfafb0f6137feba5bc6a42e44063ac2d"
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AuthorizationQueryParametersError</Code><Message>X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds</Message><RequestId>ATPQGMBNTYPA8PSW</RequestId><HostId>aNuJCSgp7esKOHQuJjTS7cdsbNklzF4avAa+IQh2JhyTo0nJmOL/nO3VXJIaqYNbwu+A3IZ5WAM=</HostId></Error>
有効期間を延ばすとエラーになりました。一週間(604800)が上限だとメッセージが出ています。
V4にすることで、ドキュメント通りの動きをするようになりました。
まとめ
Pythonを使って、公式のドキュメント通りにS3の署名付きURLを実装すると、公式のドキュメントに「動かない」と書いてある内容で実装されます。
実際の挙動を見ると、ドキュメントと異なる仕様で動きます。
AWSでよく使われる仕組みが、実は思ったよりも不安な状態で動いている、そんな夏の怪談でした。