ファイルをAPIやクラウドストレージへ送れたとしても、受信したbyte列が送信時と同じとは限りません。文字コード、改行、圧縮、途中の変換、保存ミスによって内容が変わることがあります。
ハッシュ値を使うと、送信側と受信側が同じbyte列を持っているか検証できます。ただし、ハッシュは認証や暗号化の代わりではありません。この記事では、SHA-256、HTTPのContent-Digest、JSON正規化を使い分け、ファイル・API・DB・CADデータを再実行可能な形で連携する方法を整理します。
ハッシュが解決する問題
ハッシュ関数は任意長のbyte列を受け取り、固定長のdigestを返します。SHA-256なら出力は256bitです。
入力byte列 ── SHA-256 ── 256bitのdigest
入力が1byteでも変われば、通常は異なるdigestになります。この性質を利用して、次を確認できます。
- 転送前後のfileが同一byte列か
- 受信bodyが途中で変わっていないか
- 同じfileを以前に処理したか
- 保存した成果物が後から変化していないか
一方、digestから元データを復元することはできません。また、同じ内容なら誰でも同じdigestを計算できます。したがって「誰が送ったか」の証明にはなりません。
最小の完全性メタデータ
fileとdigestを別のmanifestに記録する例です。
{
"file_id": "drawing-0042-rev-c",
"file_name": "drawing-0042-rev-c.dxf",
"size_bytes": 184320,
"hash_algorithm": "sha-256",
"digest_hex": "7f83b1657ff1fc53b92dc18148a1d65dfa13514f...",
"created_at": "2026-10-02T01:30:00Z",
"schema_version": 1
}
file_id:業務上の対象を識別するIDsize_bytes:欠落や取違いを早く検出する補助値hash_algorithm:digestの計算方式digest_hex:digestを16進文字列で表した値created_at:manifestを作成した日時schema_version:manifestのデータ契約の版
digestだけで業務上の同一性を判断してはいけません。改訂Cと改訂Dが偶然同じ内容ならdigestは一致しますが、業務上は別revisionとして管理する場合があります。ID、revision、digestの役割を分けます。
何をハッシュするかを先に固定する
ハッシュ関数へ渡すのは文字列やobjectではなく、最終的なbyte列です。同じ見た目でも、次が違えばdigestは変わります。
- UTF-8とShift_JIS
- LFとCRLF
- BOMの有無
- JSONの空白やkey順
- 圧縮前と圧縮後
- file末尾の改行の有無
データ契約には、対象を明記します。
integrity_profile:
algorithm: sha-256
target: exact_file_bytes
digest_encoding: lowercase_hex
text_encoding: not_applicable
compression: none
転送fileの完全一致を検証するなら、変換前のraw bytesを対象にします。内容を解凍・文字コード変換してから計算すると、送信側と受信側で対象がずれるためです。
PythonでfileのSHA-256を計算する
大きなfileを一度にmemoryへ読み込まず、一定量ずつ処理します。
import hashlib
from pathlib import Path
def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str:
if chunk_size <= 0:
raise ValueError("chunk_sizeは1以上が必要です")
digest = hashlib.sha256()
with path.open("rb") as file:
while chunk := file.read(chunk_size):
digest.update(chunk)
return digest.hexdigest()
fileはtext modeではなくrbで開きます。text modeでは改行変換やdecodeが入り、保存されたraw bytesと異なる対象を計算する可能性があります。
受信後は、期待値と実測値を比較します。
import hmac
from pathlib import Path
def verify_file(path: Path, expected_hex: str) -> None:
if len(expected_hex) != 64:
raise ValueError("SHA-256 digestは64桁の16進文字列が必要です")
try:
bytes.fromhex(expected_hex)
except ValueError as exc:
raise ValueError("digestの16進表現が不正です") from exc
actual_hex = sha256_file(path)
if not hmac.compare_digest(actual_hex, expected_hex.lower()):
raise ValueError("fileのdigestが一致しません")
PythonのhashlibはSHA-256を含む複数のhash algorithmへ共通interfaceを提供します。update()へ渡す値はbytes-like objectであり、digest()はbytes、hexdigest()は16進文字列を返します。
HTTPではContent-Digestを使う
RFC 9530は、HTTP message contentの完全性を伝えるContent-Digest fieldを定義しています。sha-256の値は、digestのraw bytesをBase64で表したStructured FieldsのByte Sequenceです。
次のbodyをUTF-8のbyte列として送る例を考えます。末尾改行は含めない契約です。
{"asset_id":"000042","revision":"C"}
POST /imports HTTP/1.1
Content-Type: application/json
Content-Digest: sha-256=:kNmS7inZRAzelVzsXXDKoGsA//y9yk7wnxv2MIKTIUE=:
{"asset_id":"000042","revision":"C"}
受信側はJSONをparseし直した文字列ではなく、受信したcontent bytesからdigestを再計算します。空白やkey順を変更してから検証すると、送信側と同じJSON objectでも不一致になります。
次の関数は、連携契約をsha-256一つに限定した最小例です。本番で複数algorithmやparameterを扱う場合は、RFC 8941のStructured Fieldsに対応したparserを使います。
import base64
import hashlib
import hmac
def verify_content_digest(raw_body: bytes, header_value: str) -> None:
prefix = "sha-256=:"
suffix = ":"
if not header_value.startswith(prefix) or not header_value.endswith(suffix):
raise ValueError("Content-Digestの形式が不正です")
encoded = header_value[len(prefix):-len(suffix)]
try:
expected = base64.b64decode(encoded, validate=True)
except ValueError as exc:
raise ValueError("digestのBase64が不正です") from exc
if len(expected) != hashlib.sha256().digest_size:
raise ValueError("SHA-256 digestの長さが不正です")
actual = hashlib.sha256(raw_body).digest()
if not hmac.compare_digest(actual, expected):
raise ValueError("Content-Digestが一致しません")
Content-Digestはcontentの完全性を扱います。representation全体の完全性を扱うRepr-Digestとは対象が異なるため、圧縮、range response、representation変換を伴う連携では、どちらを検証するかAPI契約へ明記します。
JSON objectの同一性には正規化が必要
次の2つは同じfieldとvalueを持ちますが、byte列は異なります。
{"id":"A-001","active":true}
{ "active": true, "id": "A-001" }
raw bodyの転送完全性を検証するなら、digestが異なることは正常です。一方、「意味が同じJSONなら同じdigestにしたい」という用途では、serialize規則を統一する必要があります。
RFC 8785のJSON Canonicalization Schemeは、JSON primitiveの表現、propertyの決定的な並び順などを定め、cryptographic operationへ使える不変な表現を作ります。ただし、単にPythonのsort_keys=Trueを使うだけでJCS準拠になるとは限りません。numberやUnicodeを含む規則全体に対応した実装を選びます。
用途を次のように分けます。
| 目的 | ハッシュ対象 |
|---|---|
| 転送されたbodyの完全性 | 受信したraw body bytes |
| fileの完全一致 | 保存されたexact file bytes |
| JSONの論理的な同一性 | 合意したcanonical JSON bytes |
| 業務recordの同一性 | digestではなく安定したrecord ID |
保存schemaと再実行
受信fileの履歴をSQLiteへ保存する例です。
CREATE TABLE import_files (
import_id TEXT PRIMARY KEY NOT NULL,
source_system TEXT NOT NULL,
file_name TEXT NOT NULL,
size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),
hash_algorithm TEXT NOT NULL CHECK (hash_algorithm = 'sha-256'),
digest_hex TEXT NOT NULL CHECK (length(digest_hex) = 64),
profile_version INTEGER NOT NULL,
status TEXT NOT NULL
CHECK (status IN ('received', 'verified', 'processed', 'rejected')),
received_at TEXT NOT NULL,
UNIQUE (source_system, digest_hex, profile_version)
);
同じ送信元、digest、profile versionの組み合わせを一意にすれば、同じfileの再送を検出できます。ただし、同一fileを別の業務処理へ意図的に使う場合もあるため、重複時に常に捨てるのではなく、既存のimport_idと処理結果を返す設計にします。
安全な処理順は次のとおりです。
raw bytesを受信
↓
size上限を確認
↓
SHA-256を計算・照合
↓
受信履歴を一意制約付きで保存
↓
形式とschemaを検証
↓
業務recordをIDでUPSERT
↓
処理結果を確定
digest検証とJSON Schema、CSV列schemaなどの構造検証は別の処理です。digestが一致しても、必須field欠落や型違反はあり得ます。
正常系と異常系をテストする
| ケース | 期待結果 |
|---|---|
| 同じfileを再計算 | 同じSHA-256になる |
| 1byte変更 | 不一致として拒否する |
| 改行コードだけ変更 | exact bytes契約では不一致になる |
| 大文字の16進digest | 小文字へ正規化して比較する |
| 不正なBase64 | parse errorとして拒否する |
| digest一致・schema違反 | 完全性は成功、業務取込は拒否する |
| 同じfileを再送 | 新規取込を増やさず既存結果を返す |
| 同じrecord IDで内容更新 | record version規則に従って更新する |
検証失敗時は、期待digest、実測digest、algorithm、file size、受信元、処理IDを監査情報として残します。機密性の高いpayload全体は通常logへ出しません。
ハッシュ・HMAC・署名を混同しない
通常のSHA-256 digestだけでは、攻撃者がbodyとdigestの両方を置き換えられます。役割を分けます。
| 仕組み | 主な目的 | 秘密鍵 |
|---|---|---|
| SHA-256 digest | 偶発的な破損・内容差分の検出 | 不要 |
| HMAC | 共有秘密鍵を持つ相手からの改ざん検知 | 共有鍵 |
| digital signature | 秘密鍵による署名と公開鍵による検証 | 非対称鍵 |
| encryption | 内容を読めないようにする | 方式による |
RFC 9421のHTTP Message Signaturesは、HTTP componentへdigital signatureまたはMACを適用する仕組みです。RFC 9530のIntegrity fieldを署名対象へ含めれば、contentのdigestと送信者のauthenticityを結び付けられます。
HTTPSも併用します。digestはアクセス制御、送信者認証、機密性を提供しません。API key、HMAC secret、秘密鍵はmanifestやpayloadへ含めず、secret管理機能へ分離します。
CAD・BOM・設計データへの展開
DXF、PDF図面、CAD native file、BOM export、計算結果は、再生成や転送の経路が増えるほど取違いが起きやすくなります。
- revisionごとに安定したfile IDを割り当てる
- raw fileのSHA-256、size、生成日時をmanifestへ記録する
- preview画像と正本CAD fileのdigestを分ける
- ZIP配布ではarchive全体と必要な内部fileを別々に検証する
- DBにはdigestだけでなくalgorithmとprofile versionを保存する
- 再生成物は元データID、変換version、出力digestを関連付ける
この構造なら、同じ図面番号のfileが複数の経路へコピーされても、どのbyte列から生成され、途中で変化したかを追跡できます。
まとめ
ハッシュ設計で重要なのはSHA-256を計算することだけではなく、「どのbyte列を、どのalgorithmと表現で比較するか」を契約にすることです。
- fileやHTTP bodyはraw bytesを対象にする
- algorithm名、digest表現、圧縮・encoding条件を固定する
- HTTP contentにはRFC 9530の
Content-Digestを利用できる - JSONの意味上の同一性にはcanonicalizationを検討する
- digestとrecord ID、revision、schema versionを分離する
- digest一致後も構造・型・業務ルールを検証する
- file hashとDBの一意制約で再送を冪等に扱う
- SHA-256を認証や暗号化の代わりにしない
- authenticityが必要ならHMACやHTTP Message Signaturesを組み合わせる
完全性メタデータを共通契約として残せば、API、CSV、DB、CAD、BOM、自動処理の間でデータを安全に受け渡し、障害後も同じ入力から同じ結果を再現できます。

