データ連携のハッシュ設計|SHA-256・Content-Digest・JSON正規化

「データ連携のハッシュ設計|SHA-256・Content-Digest・JSON正規化」の内容を表す技術イラスト

ファイルを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:業務上の対象を識別するID
  • size_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小文字へ正規化して比較する
不正なBase64parse 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、自動処理の間でデータを安全に受け渡し、障害後も同じ入力から同じ結果を再現できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次