タグアーカイブ 署名エラー

WordPressからS3アップロードが失敗する署名エラーの直し方

WordPressのプラグインや自作コードからAWS S3へファイルをアップロードすると「SignatureDoesNotMatch」というエラーで失敗する場合、シグネチャ生成後にヘッダー名の大文字小文字が入れ替わっていることが代表的な原因だ。署名の計算時とリクエスト送信時でヘッダー名を完全に一致させると、この問題は解決する。

S3アップロードの署名エラーは何が起きているのか

S3アップロードの署名エラーは何が起きているのか

AWS S3に対してファイルを直接アップロードする場合、多くのWordPressプラグインは「Presigned URL」や「署名付きリクエスト」を使う。これは、アクセスキーを直接ブラウザに渡さず、署名入りのURLやHTTPヘッダーを生成してアップロードを許可する仕組みだ。静的にサイトを書き出すプラグイン「Static Porter」もこの方式を採用している。

署名付きリクエストでは、リクエストの一部をSHA256でハッシュ化し、そのハッシュ値をシークレットキーで暗号化して「シグネチャ」を作る。S3側は同じ計算を再現し、シグネチャが一致するかどうかで正当性を検証する。ここで気をつけたいのは、署名の計算対象にHTTPヘッダー名も含まれる点だ。

つまり、署名を計算した時点で「content-type」という小文字のヘッダー名を使っていたのに、送信する直前に「Content-Type」と大文字に変更してしまうと、S3側の署名検証が失敗する。これが今回のエラーの正体だ。

Content-Typeの大文字小文字が署名を無効にする仕組み

Content-Typeの大文字小文字が署名を無効にする仕組み

AWSのSignature V4(署名バージョン4)では、署名の計算対象となる正規リクエストを作る際、ヘッダー名はそのままの文字列として扱われる。HTTPヘッダー名は本来大文字小文字を区別しないとされているが、署名計算の世界では「文字列として完全に一致していること」が要求される。

ここで問題になるのが、PHPの配列キーの扱いだ。PHPの連想配列はキーの大文字小文字を厳密に区別するため、「content-type」と「Content-Type」は別物として扱われる。署名を計算する関数に小文字の「content-type」を渡した後、別の場所で大文字の「Content-Type」を代入すると、配列には両方のキーが存在する状態になる。

wp_remote_requestがヘッダーを送信する際、どちらのキーが優先されるかは実装次第だ。しかし重要なのは、シグネチャの計算対象になったヘッダー名と、実際に送信されるヘッダー名が一致しなくなる点にある。S3はこの不一致を「署名が無効」と判定する。

Before(エラー)
シグネチャ生成時 → content-type を署名
送信直前 → Content-Type に上書き
S3検証 → 署名不一致で403エラー
↓
After(修正後)
シグネチャ生成時 → content-type を署名
送信直前 → content-type のまま変更なし
S3検証 → 署名一致でアップロード成功
■ エラー状態 ■ 修正後

上図の通り、シグネチャ生成時と送信時でヘッダー名の表記を揃えるだけで、署名検証のエラーは解消する。

署名エラーを直す具体的な修正手順

署名エラーを直す具体的な修正手順

Static Porterのようにプラグイン内部のコードに起因する場合は、プラグインの更新で修正されるのを待つ以外にも、一時的なパッチを当てる方法がある。次の手順で問題箇所を特定し、修正する。

STEP 1 シグネチャ生成関数への引数を確認する
↓
STEP 2 送信前のヘッダー上書き処理を探す
↓
STEP 3 ヘッダー名の大文字小文字を署名側に合わせる
↓
STEP 4 キャッシュをクリアして再テストする

Static Porterの場合、問題が起きる箇所はプラグインのメインPHPファイル内で、MIMEタイプを推測した後にヘッダーを上書きしている行だ。

シグネチャ生成に渡したヘッダー配列を特定する

まず、プラグインのソースコードを開き、AWSの署名を計算している関数呼び出しを探す。Static Porterでは「static_porter_aws_sigv4」という関数だ。この関数に渡しているヘッダー配列には、小文字の「content-type」がキーとして含まれている。

署名の計算はこの配列を受け取った時点の内容で行われる。ここでのキー名を記憶しておくことが修正の基準になる。

送信前のヘッダー上書き処理を探す

署名を計算した後、wp_remote_requestに渡す直前に、ヘッダー配列へ新しい値を代入している行がないか確認する。今回のケースでは、次のようなコードが問題になる。

// 修正前のコード
$content_type = static_porter_guess_mime($file_path);
$headers = static_porter_aws_sigv4('PUT', $host, $uri, $region, 's3', $body, [
    'content-type' => $content_type,
], $access_key, $secret_key);
$headers['Content-Type'] = $content_type; // ← ここで大文字キーが追加され署名と食い違う
$response = wp_remote_request('https://' . $host . $uri, [
    'method'  => 'PUT',
    'headers' => $headers,
    'body'    => $body,
    'timeout' => 60,
]);

シグネチャ生成時には小文字の「content-type」を渡しているのに、その直後に大文字の「Content-Type」という別キーを代入している。これが署名不一致の直接の原因だ。

ヘッダー名のケースを署名側に合わせる

最も確実な修正は、シグネチャ生成時のヘッダー名と送信時のヘッダー名を完全に一致させることだ。今回のケースでは、シグネチャ生成時に小文字を使っているため、後から大文字キーを代入している行をコメントアウトするだけで解決する。

// 修正後のコード
$content_type = static_porter_guess_mime($file_path);
$headers = static_porter_aws_sigv4('PUT', $host, $uri, $region, 's3', $body, [
    'content-type' => $content_type,
], $access_key, $secret_key);
// $headers['Content-Type'] = $content_type; // コメントアウトして小文字のまま送信する
$response = wp_remote_request('https://' . $host . $uri, [
    'method'  => 'PUT',
    'headers' => $headers,
    'body'    => $body,
    'timeout' => 60,
]);

あるいは逆に、シグネチャ生成時のキーを大文字の「Content-Type」に統一し、後からの代入を削除する方法もある。どちらにせよ、署名を計算した時点と送信時点で同じヘッダー名を使うことが本質だ。

キャッシュをクリアして再テストする

コードを修正したら、WordPressのキャッシュプラグインやオブジェクトキャッシュをクリアし、実際にS3へのアップロードを実行して確認する。アップロード処理に署名済みURLがキャッシュされる仕組みがある場合は、古い署名が残っていないことも確認しておく。

プラグイン本体の編集は子テーマや別プラグインで安全に行う

プラグイン本体のPHPファイルを直接編集すると、プラグインのアップデート時に修正が上書きされて消える。長期的には、プラグイン開発者へバグ報告を出し、公式のアップデートで修正されるのを待つのが妥当だ。

どうしても動かす必要がある場合は、プラグインのコードを直接修正した上で、変更内容を記録しておくか、mu-plugin(Must Use Plugin)で代替処理を差し込む方法を検討する。

自作コードでS3署名を使う場合の落とし穴

自作コードでS3署名を使う場合の落とし穴

プラグイン任せにせず、自前のPHPコードでS3への署名付きアップロードを実装している場合、同じような問題がいくつか潜んでいる。ここでは署名検証が失敗する主なパターンを整理する。

ホスト名とURIの不一致

シグネチャの計算対象には、エンドポイントのホスト名とパス(URI)が含まれる。バケット名のリージョン表記や、URLエンコードの有無、末尾のスラッシュの扱いが違うだけで署名は別物になる。署名を作る関数と実際にリクエストを送るURLが完全に一致しているか確認する。

ボディの変更によるシグネチャ崩れ

S3のPUTリクエストでは、アップロードするファイルのバイナリデータをそのままボディとして署名に含める。署名を計算した後に、余計な空白や改行、文字コード変換が入るとボディが変化し、署名検証に失敗する。ファイルはバイナリのまま扱い、加工しないことが基本だ。

タイムスタンプのスキュー

Signature V4は、リクエスト日時を署名に含める。サーバーのシステムクロックが大幅にずれていると、S3側が「リクエストが古すぎる」と判断して拒否する。エラーメッセージに時刻に関する記述がある場合は、サーバーの時刻同期(NTP)を確認する。

よくある質問

S3の署名エラーとアクセス権限エラーの違いは何か

署名エラーは「SignatureDoesNotMatch」や「The request signature we calculated does not match」と表示され、リクエスト内容が署名と食い違っていることを示す。一方、権限エラーは「AccessDenied」や「403 Forbidden」の形で、署名自体は正しいがIAMポリシーで操作が許可されていないことを示す。エラーコードを見分けることが切り分けの第一歩だ。

プラグインをアップデートしたらS3アップロードが壊れた場合はどうするか

プラグインのアップデートで内部実装が変わった可能性が高い。まずはプラグインの変更履歴(チャンジログ)を確認し、S3関連の修正が入っているか調べる。問題が再現するなら、1つ前のバージョンへ戻して切り分けるのが確実だ。プラグインのフォーラムやGitHubリポジトリで同様の報告が上がっていないかも確認する。

PHPのHTTPヘッダー名は大文字小文字を区別するのか

HTTPの仕様上、ヘッダー名は本来大文字小文字を区別しない。だが、AWSの署名計算は「文字列としての完全一致」を要求するため、区別される。PHPの配列キーも「content-type」と「Content-Type」は別物として扱われる。この2つの事情が重なって問題が起きる。

wp_remote_requestのheaders配列で小文字を使っても動作するのか

WordPressのHTTP APIはHTTPヘッダー名の大文字小文字をある程度正規化する。しかし、S3への署名付きリクエストでは、署名計算時のヘッダー名と食い違うと検証に失敗する。小文字で署名したなら小文字のまま渡すのが安全だ。

AWS SDKを直接使うほうが安全なのか

AWS SDK for PHPを使えば、シグネチャ生成と送信の整合性をライブラリが保証するため、今回のような手動ミスは起きにくい。ただし、SDKを導入すると依存関係が増え、WordPressとの相性も考える必要がある。小規模なアップロード機能なら、シグネチャ生成を手動で実装しつつ、ヘッダー名の一致を徹底する方がシンプルな場合もある。

この記事のポイント

  • S3の署名エラーはヘッダー名の大文字小文字不一致が原因になりやすい
  • シグネチャ生成時と送信時でヘッダー名を完全に一致させる
  • 問題のある代入行をコメントアウトするだけで多くの場合解決する
  • プラグイン本体の直接編集はアップデートで消えるため要注意
  • 自作コードではホスト名やボディ、時刻ずれにも気をつける