ファイルが改ざんされていないこと、およびキーを知っている当事者だけが正しい署名を生成できることを確認することができます。単純なハッシュ(SHA256など)とは異なり、HMACはキーを使用して偽造を防ぎます。
ファイルをアップロードするための正しいプロセスは通常です。
共有秘密を生成/配布します(またはサーバー側の秘密鍵を使用します)。
クライアントまたは送信者は、ファイルのHMAC(たとえば、HMAC-SHA256)を計算し、アップロードとともに署名(HTTPヘッダーまたはフォームフィールド)を送信します。
ファイルを受信した後、サーバーは同じキーを使用してHMACを再計算し、シグネチャをタイミングセキュリティ比較( hash_equals )と比較し、一致したときに検証を渡します。
HTTPSを介して署名とファイルを永久に転送します。署名は、プレーンテキストのHTTPで盗聴され、再利用されます。
hash_hmac_file($ algo、$ filename、$ key、$ raw_output = false)ファイル上でHMACを直接計算し、読み取りファイルのストリーミングは内部で処理されます。最初にファイル全体をメモリに読み込む必要はありません。これは中程度および大規模なファイルに適しています。
サーバー側の検証をすばやく実装する必要がある場合、 hash_hmac_file()は簡潔で効率的なソリューションです。ただし、より複雑なシナリオ(シャードアップロードなど)の場合、 hash_init() / hash_update() / hash_final()を使用してブロックごとの計算を実装することもできます。
以下は一般的な状況を示しています。クライアントは共有キーを使用してファイルでHMACを実行し(クライアントの例が省略されています)、サーバーはhash_hmac_file()を使用して$ _filesのアップロードを確認します。
サーバー受信エンドverify_upload.php (簡単な例):
<span><span><span class="hljs-meta"><?php</span></span><span>
</span><span><span class="hljs-comment">// verify_upload.php</span></span><span>
</span><span><span class="hljs-comment">// 1) 安全なキーストレージ(例で環境変数を使使用します)</span></span><span>
</span><span><span class="hljs-variable">$HMAC_KEY</span></span><span> = </span><span><span class="hljs-title function_ invoke__">getenv</span></span><span>(</span><span><span class="hljs-string">'UPLOAD_HMAC_KEY'</span></span><span>); </span><span><span class="hljs-comment">// 展開中に安全に設定してください(環境変数 / Vault)</span></span><span>
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-variable">$HMAC_KEY</span></span><span>) {
</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">500</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Server misconfiguration."</span></span><span>;
</span><span><span class="hljs-keyword">exit</span></span><span>;
}
</span><span><span class="hljs-comment">// 2) クライアントが署名を入れると仮定します HTTP Header: X-File-Signature</span></span><span>
</span><span><span class="hljs-variable">$clientSig</span></span><span> = </span><span><span class="hljs-keyword">isset</span></span><span>(</span><span><span class="hljs-variable">$_SERVER</span></span><span>[</span><span><span class="hljs-string">'HTTP_X_FILE_SIGNATURE'</span></span><span>]) ? </span><span><span class="hljs-variable">$_SERVER</span></span><span>[</span><span><span class="hljs-string">'HTTP_X_FILE_SIGNATURE'</span></span><span>] : </span><span><span class="hljs-literal">null</span></span><span>;
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-variable">$clientSig</span></span><span>) {
</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">400</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Missing signature."</span></span><span>;
</span><span><span class="hljs-keyword">exit</span></span><span>;
}
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-keyword">isset</span></span><span>(</span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>]) || </span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>][</span><span><span class="hljs-string">'error'</span></span><span>] !== UPLOAD_ERR_OK) {
</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">400</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Upload failed."</span></span><span>;
</span><span><span class="hljs-keyword">exit</span></span><span>;
}
</span><span><span class="hljs-comment">// 一時的なアップロードファイルパス</span></span><span>
</span><span><span class="hljs-variable">$tmpPath</span></span><span> = </span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>][</span><span><span class="hljs-string">'tmp_name'</span></span><span>];
</span><span><span class="hljs-comment">// 3) 使使用 hash_hmac_file サーバー側の署名を計算します(使使用 SHA256)</span></span><span>
</span><span><span class="hljs-variable">$algo</span></span><span> = </span><span><span class="hljs-string">'sha256'</span></span><span>;
</span><span><span class="hljs-variable">$serverSig</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_hmac_file</span></span><span>(</span><span><span class="hljs-variable">$algo</span></span><span>, </span><span><span class="hljs-variable">$tmpPath</span></span><span>, </span><span><span class="hljs-variable">$HMAC_KEY</span></span><span>);
</span><span><span class="hljs-comment">// 4) 使用 hash_equals タイミングの安全性比較を行います,情報の漏れは避けてください</span></span><span>
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">hash_equals</span></span><span>(</span><span><span class="hljs-variable">$serverSig</span></span><span>, </span><span><span class="hljs-variable">$clientSig</span></span><span>)) {
</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">403</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Signature mismatch. File may be tampered."</span></span><span>;
</span><span><span class="hljs-comment">// 監査ログを記録できます:ソース IP、ファイル名、時間と待ってください</span></span><span>
</span><span><span class="hljs-keyword">exit</span></span><span>;
}
</span><span><span class="hljs-comment">// 5) 署名検証が合格しました,ファイルを最終ディレクトリに安全に移動し、アクセス許可を設定します</span></span><span>
</span><span><span class="hljs-variable">$dest</span></span><span> = </span><span><span class="hljs-keyword">__DIR__</span></span><span> . </span><span><span class="hljs-string">'/uploads/'</span></span><span> . </span><span><span class="hljs-title function_ invoke__">basename</span></span><span>(</span><span><span class="hljs-variable">$_FILES</span></span><span>[</span><span><span class="hljs-string">'file'</span></span><span>][</span><span><span class="hljs-string">'name'</span></span><span>]);
</span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">move_uploaded_file</span></span><span>(</span><span><span class="hljs-variable">$tmpPath</span></span><span>, </span><span><span class="hljs-variable">$dest</span></span><span>)) {
</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">500</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Failed to store file."</span></span><span>;
</span><span><span class="hljs-keyword">exit</span></span><span>;
}
</span><span><span class="hljs-comment">// オプション:署名を保存します/メタデータは、その後の検証のためにデータベースに送信されます</span></span><span>
</span><span><span class="hljs-comment">// $db->insert('uploads', ['name'=>..., 'sig'=>$serverSig, ...]);</span></span><span>
</span><span><span class="hljs-title function_ invoke__">http_response_code</span></span><span>(</span><span><span class="hljs-number">200</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-string">"Upload verified and stored."</span></span><span>;
</span></span>
クライアントは、リクエストで署名値(16進文字列など)を送信する必要があります。サンプルHMAC生成(PHPクライアントまたはコマンドラインのデモンストレーション):
<span><span><span class="hljs-meta"><?php</span></span><span>
</span><span><span class="hljs-comment">// client_sign.php</span></span><span>
</span><span><span class="hljs-variable">$key</span></span><span> = </span><span><span class="hljs-string">'shared-secret-key'</span></span><span>;
</span><span><span class="hljs-variable">$file</span></span><span> = </span><span><span class="hljs-string">'/path/to/file.bin'</span></span><span>;
</span><span><span class="hljs-variable">$algo</span></span><span> = </span><span><span class="hljs-string">'sha256'</span></span><span>;
</span><span><span class="hljs-variable">$sig</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_hmac_file</span></span><span>(</span><span><span class="hljs-variable">$algo</span></span><span>, </span><span><span class="hljs-variable">$file</span></span><span>, </span><span><span class="hljs-variable">$key</span></span><span>);
</span><span><span class="hljs-keyword">echo</span></span><span> </span><span><span class="hljs-variable">$sig</span></span><span>; </span><span><span class="hljs-comment">// サーバーに送信される署名</span></span><span>
</span></span>
実際のHTTPアップロードでは、クライアントはX-File-Signature:<Sig> HeaderをMultiPart/Form-Dataアップロードまたはフォームフィールドの署名として送信します。
hash_hmac_file()自体は大きなファイルを処理できますが、シェードアップロードまたはストリーミングアップロードシナリオでは、hmacをブロックごとに計算する必要があります。
利点:単純な実装。短所:サーバー上の完全なファイルの一時的なストレージ/マージが必要であり、IOとディスクを占有する場合があります。
クライアントは、各ブロックのHMACを計算し、ブロック署名を送信します。サーバーは、各ブロックを受信するときにファイルを検証して追加します。最後のブロックが正常にアップロードされ、完全なファイルが検証されました。
または、クライアントは各ブロックの元のデータを送信し、最後に完全なファイルのHMACを送信します(クライアントがローカルで完全な署名を生成できる場合)。
サーバーがPHPからアップロードストリームを直接読み取る場合(たとえば、PSR-7またはPHP://入力を使用するなど)、 hash_init( 'sha256'、hash_hmac、$ key) 、およびhash_update ()を使用することができます。例:
<span><span><span class="hljs-meta"><?php</span></span><span>
</span><span><span class="hljs-variable">$key</span></span><span> = </span><span><span class="hljs-title function_ invoke__">getenv</span></span><span>(</span><span><span class="hljs-string">'UPLOAD_HMAC_KEY'</span></span><span>);
</span><span><span class="hljs-variable">$algo</span></span><span> = </span><span><span class="hljs-string">'sha256'</span></span><span>;
</span><span><span class="hljs-comment">// 初期化 HMAC コンテクスト</span></span><span>
</span><span><span class="hljs-variable">$context</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_init</span></span><span>(</span><span><span class="hljs-variable">$algo</span></span><span>, HASH_HMAC, </span><span><span class="hljs-variable">$key</span></span><span>);
</span><span><span class="hljs-comment">// 私たちを仮定します php://input またはファイルストリームはブロックごとに読み取りされます</span></span><span>
</span><span><span class="hljs-variable">$stream</span></span><span> = </span><span><span class="hljs-title function_ invoke__">fopen</span></span><span>(</span><span><span class="hljs-string">'php://input'</span></span><span>, </span><span><span class="hljs-string">'rb'</span></span><span>);
</span><span><span class="hljs-keyword">while</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">feof</span></span><span>(</span><span><span class="hljs-variable">$stream</span></span><span>)) {
</span><span><span class="hljs-variable">$chunk</span></span><span> = </span><span><span class="hljs-title function_ invoke__">fread</span></span><span>(</span><span><span class="hljs-variable">$stream</span></span><span>, </span><span><span class="hljs-number">8192</span></span><span>);
</span><span><span class="hljs-keyword">if</span></span><span> (</span><span><span class="hljs-variable">$chunk</span></span><span> === </span><span><span class="hljs-literal">false</span></span><span>) </span><span><span class="hljs-keyword">break</span></span><span>;
</span><span><span class="hljs-title function_ invoke__">hash_update</span></span><span>(</span><span><span class="hljs-variable">$context</span></span><span>, </span><span><span class="hljs-variable">$chunk</span></span><span>);
}
</span><span><span class="hljs-variable">$serverSig</span></span><span> = </span><span><span class="hljs-title function_ invoke__">hash_final</span></span><span>(</span><span><span class="hljs-variable">$context</span></span><span>);
</span><span><span class="hljs-title function_ invoke__">fclose</span></span><span>(</span><span><span class="hljs-variable">$stream</span></span><span>);
</span><span><span class="hljs-comment">// 次に、比較します clientSig ...</span></span><span>
</span></span>
単純なHMACは、整合性と認証のみを保証できます(キーを保持しているパーティーから派生した)が、「再生」になりやすい(同じファイル +署名が再生される)。保護方法:
署名すると、ファイルのタイムスタンプ、NonCE(乱数)、アップローダーIDなどが一緒に署名されます。たとえば、これらのメタデータを署名= HMAC(key、fileName + ":" + file_sha256 + ":" +タイムスタンプ + ":" + nonce) 、およびタイムスタンプとノンセを送信します。
サーバーは、タイムスタンプをチェックし(たとえば、±5分を許可します)、再生を防ぐために非CEが使用されているかどうか(一定期間持続する必要がある)かどうかを確認します。
APIの場合:短期間有効で、トークンに有効期限と署名を埋め込んだ事前に署名したアップロードトークンを使用します。サーバーは、ファイルHMACを確認する前にトークンの有効性をチェックします。
サンプル署名スキーム(ファイルの外部署名):
クライアントは、最初に短期のアップロード資格情報を要求します(サーバーが生成および署名したNONCEおよび有効期限を含む)。
クライアントはファイルをアップロードし、資格情報とファイルをヘッダーに配置します。
サーバーは最初に資格情報を検証し、次にファイルHMACを検証します。
キーストレージ:HMACキーは、倉庫/コードにハードコーディングされてはなりません。環境変数、構成ファイルの制限付きストレージ、または専門の主要な管理システム(Vault、Cloud KMS)を使用します。
最小許可:キーの使用と権限を最小限に抑える必要があります。異なるキーが異なる用途に割り当てられる場合がある場合(署名キーのアップロードは他のサービスキーから分離されています)。
定期的な回転:キーローテーションポリシー(90日ごとに)を開発し、検証の中断を避けるために、古いキー(回転中に新しい/古いキーの両方がサポートされている)の短期検証をサポートします。
監査:攻撃動作の検出を容易にするために、失敗した署名のリクエスト、ソースIP、および時間を記録します。
HTTPS/TLSの使用:署名とファイルをTLSを介して転送する必要があります。
署名を比較するために== 、 ==または文字列スプライシングを使用しないでください。たとえば、一定の時間比較にはhash_equals()を使用する必要があります。
<span><span><span class="hljs-keyword">if</span></span><span> (!</span><span><span class="hljs-title function_ invoke__">hash_equals</span></span><span>(</span><span><span class="hljs-variable">$serverSig</span></span><span>, </span><span><span class="hljs-variable">$clientSig</span></span><span>)) {
</span><span><span class="hljs-comment">// 拒否する</span></span><span>
}
</span></span>
Hash_equals()は、異なる長さや早期の不一致がある場合、時間差が悪用されるのを防ぐこともできます。
HMACの検証は、整合性と認証の一部にすぎないものであり、次のようにする必要があります。
悪意のあるスクリプトの実行を避けるために、ファイルタイプ(MIMEタイプ +ファイルヘッダー署名)を確認します。
サイズを制限し、アップロード周波数を制限し、ユーザーのサイズとタイプをホワイトリストに付けます。
ユーザーをWeb Root Directoryからアップロードするディレクトリを分離し、適切なファイルアクセス許可を設定します(実行可能ではありません)。
実行可能ファイルは、リスクの高いシナリオで追加のスキャン(ウイルススキャン/サンドボックス分析)で実行されます。
一部のアーキテクチャは、クライアントがオブジェクトストレージ(S3、GCSなど)に直接アップロードできるように、サーバーに「処刑されたURL」またはトークンをクライアントに発行することを望んでいます。このシナリオで:
サーバーが事前に署名されたURLを生成すると、HMACを埋め込んで(またはクラウドストレージの自己署名メカニズムを使用)、将来アップロードするときに特定のヘッダー( X抽出SHA256など)を持参する必要があることをクライアントに通知できます。
ファイルがクラウドストレージにアップロードされると、サーバーはコールバックまたは後続の取得を介してオブジェクトのHMACまたはオブジェクトの整合性を検証できます(ストレージ内のファイルのHMACの再計算、またはクライアントにアップロード時にクラウドストレージからメタデータを返すことなど)。詳細は、クラウドストレージの機能に依存します。
Q:HTTPSがある場合、なぜHMACが必要なのですか?
HTTPSは送信セキュリティを保護しますが、法的なキー保有者が悪意のあるファイルをアップロードしたり、改ざんされた後にサーバー側でそれらを再アップロードするのを防ぐことはできません。 HMACは、「キーを持つパーティーがこのアップロードで署名を生成し、ファイルの内容が送信/ストレージの前後に変更されていないことを保証できます。
Q: hash_file() (キーなし)を使用できますか?
hash_file()は、整合性を検証することができます(改ざんされているかどうか)が、誰でもハッシュ値を偽造できます。ハッシュ値を公開したくない場合、サーバーのみが計算と比較に責任がある場合、 hash_file()も有用です。ただし、アップロードイニシエーターを確認する場合は、HMACを使用する必要があります。
hash_hmac_file() (またはhash_init + hash_update )を使用して、サーバー上のファイルHMACを効率的に計算します。クライアントは同じキーで署名し、署名を送信します(または事前に署名された資格情報はサーバーによって発行されます)。
タイミングセーフ比較には、 hash_equals()を使用します。
Secure StorageおよびRotationポリシーを使用して、httpsを使用して署名とファイルを常に転送します。
大きなファイル/スライスアップロードの場合は、ストリーミングHMACまたはSHARD署名スキームを使用し、最終的なマージまたはチップごとの検証中にサーバー側の一貫性を維持します。
HMACは、改ざん防止およびソース検証の一部として使用され、ファイルタイプの検出、許可制御、ウイルススキャンを組み合わせて、完全な保護システムを形成します。