فرض کنیم یک بخشِ پاسخ‌گوی صوتی، پیامِ درخواستِ مسیریابی را به بخش دیگری تحویل می‌دهد. پیش از فکر کردن به اجرای آن، می‌خواهیم رابطهٔ یک برچسب با بایت‌های همان پیام و کلیدِ مشترک را بررسی کنیم. این درس یک پروفایلِ کوچکِ HMAC-SHA256 می‌سازد. همهٔ ورودی‌ها تألیفی و آفلاین‌اند؛ هیچ تماس، ابزار یا درخواستِ شبکه‌ای اجرا نمی‌شود. کلیدِ ثابتِ چاپ‌شده نیز دادهٔ عمومیِ آزمایش است و رازِ قابل‌استفاده در استقرار نیست.

۱. برچسبِ پیام را از مجوزِ اجرای ابزار جدا کنیم

وارسیِ پیام و تصمیمِ اجرا دو مرزِ مستقل‌اند
تصویر مفهومی: وارسیِ پیام و تصمیمِ اجرا دو مرزِ مستقل‌اند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در RFC 2104، HMAC با تابعِ درهم‌سازی و کلیدِ مشترک تعریف می‌شود. در این تمرین، ساختن برچسب و وارسیِ آن هر دو همان کلید را دارند. نتیجهٔ مثبتِ تابع فقط دربارهٔ رابطهٔ همین کلید، پیام و برچسب است؛ شناسهٔ تماس‌گیرنده، رضایتِ او یا مجاز بودنِ عملیات را بررسی نمی‌کند.

بدنهٔ نمونه سه رشتهٔ tool، target و request دارد؛ مقدارها route، sales و demo-007 هستند. این نام‌ها قراردادِ سرویسِ واقعی نیستند. حتی اگر برچسب درست باشد، برنامهٔ ما بررسی نمی‌کند که route عملیاتِ شناخته‌شده‌ای است یا sales مقصدی مجاز. وارسیِ ساختار و سیاستِ اجرای ابزار، لایهٔ دیگری می‌خواهند.

HMAC متن را پنهان نمی‌کند؛ همان بدنهٔ خوانا در خروجی دیده می‌شود. همچنین تابعِ وارسی هیچ حافظه‌ای از درخواست‌های قبلی ندارد. در شاهدِ اجرا، یک پیامِ دست‌نخورده دو بار پذیرفته می‌شود. این رفتار خطای پیاده‌سازیِ HMAC نیست؛ ضدبازپخش در قراردادِ حاضر وجود ندارد و از برچسبِ درست به‌تنهایی به دست نمی‌آید.

درس کلید یکتای درخواست و بازپخش نتیجه تصمیمِ تکرار و تعارضِ محتوا را مدل می‌کند. کنترل نرخ ابزار با سطل توکن نیز دربارهٔ بودجهٔ پذیرش است. هیچ‌یک جای وارسیِ بایت‌ها نیستند؛ پاسخ مثبت در این درس نیز به معنی داشتنِ بودجه یا جلوگیری از اجرای دوباره نیست.

۲. بایت‌های دقیق و پیشوندِ کاربرد را تثبیت کنیم

بایت‌های ثابت و زمینهٔ کاربرد
تصویر مفهومی: بایت‌های ثابت و زمینهٔ کاربرد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

ورودیِ تابع، bytes واقعی است. پیش از بدنه، پیشوندِ ثابتِ voice-tool/v1 و یک بایتِ صفر می‌گذاریم. این پیشوندِ چهارده‌بایتی انتخابِ تألیفیِ ما برای زمینهٔ کاربرد است؛ جزئی از استانداردِ قالب‌بندیِ HMAC نیست. گیرنده و سازنده باید بر همین قرارداد توافق داشته باشند. تغییرِ آن در شاهدِ ما، برچسبِ قابل‌قبولِ قبلی را حفظ نمی‌کند.

بدنهٔ ASCII نمونه پنجاه‌وچهار بایت دارد؛ با پیشوند، شصت‌وهشت بایت وارد محاسبه می‌شود. طول در اینجا بایت است، نه تعدادِ نویسه‌های نمایشی یا واحدِ زمان. تابع دادهٔ دودویی، بایتِ صفر و UTF-8 را نیز بدون تغییر می‌گیرد. انتخابِ رمزگذاریِ متن پیش از فراخوانی، مسئولیتِ فراخواننده است.

برای دیدنِ اهمیتِ نمایش، ترتیبِ کلیدهای JSON را عوض و فاصله‌هایی اضافه می‌کنیم. بارگذاریِ دو نمونه در آزمون، دیکشنری‌های برابر می‌دهد، ولی بایت‌ها برابر نیستند و برچسبِ اول برای نمونهٔ دوم رد می‌شود. کدِ عملیاتی JSON را تجزیه یا مرتب نمی‌کند؛ قراردادِ عمومیِ canonical JSON نیز نساخته‌ایم. آزمونِ برابریِ معنایی فقط شاهدِ این تفاوت است.

پروفایلِ ما کلیدِ دقیقاً سی‌ودوبایتی، برچسبِ کاملِ سی‌ودوبایتی و بدنهٔ حداکثر ۴۰۹۶ بایتی می‌خواهد. بدنهٔ تهی مجاز است و باز هم پیشوند وارد محاسبه می‌شود. این حدها انتخابِ رابطِ آموزشی‌اند؛ HMAC به‌طور کلی فقط همین طولِ کلید یا همین سقفِ پیام را نمی‌پذیرد. متنِ هگز نیز مستقیماً برچسبِ باینری محسوب نمی‌شود.

۳. کد کاملِ ساخت و مقایسهٔ HMAC

ساختِ برچسب و مقایسهٔ دو نمونه
تصویر مفهومی: ساختِ برچسب و مقایسهٔ دو نمونه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل را با نام voice_tool_hmac_bytes.py ذخیره و با python voice_tool_hmac_bytes.py اجرا کنید. اجرای ثبت‌شده Python 3.14.5 و فقط کتابخانهٔ استاندارد دارد. مسیرِ اصلی hmac.digest را با SHA256 فراخوانی می‌کند؛ مرجعِ دوم صرفاً در آزمون است و برای ساختنِ برچسبِ مسیرِ اصلی استفاده نمی‌شود.

"""Offline exact-byte HMAC profile; fixed public demo key; no tool execution."""
import hashlib
import hmac
import json
import platform

DOMAIN = b'voice-tool/v1\x00'
MAX_BODY_BYTES = 4096


def checked_message(key, body):
    if type(key) is not bytes or type(body) is not bytes:
        raise TypeError('key and body must be actual bytes')
    if len(key) != 32:
        raise ValueError('this own profile requires a 32-byte key')
    if len(body) > MAX_BODY_BYTES:
        raise ValueError('body exceeds the own 4096-byte profile')
    return DOMAIN + body


def sign_payload(key, body):
    return hmac.digest(key, checked_message(key, body), 'sha256')


def verify_payload(key, body, received_tag):
    message = checked_message(key, body)
    if type(received_tag) is not bytes:
        raise TypeError('received_tag must be actual bytes')
    if len(received_tag) != 32:
        raise ValueError('full 32-byte tag required; no truncation or hex parser')
    expected = hmac.digest(key, message, 'sha256')
    return hmac.compare_digest(expected, received_tag)


def two_stage_sha256_reference(key, message):
    # Verification-only RFC2104 construction; does not call hmac or sign_payload.
    reduced = hashlib.sha256(key).digest() if len(key) > 64 else key
    block = reduced + b'\x00' * (64 - len(reduced))
    inner = hashlib.sha256(bytes(x ^ 0x36 for x in block) + message).digest()
    return hashlib.sha256(bytes(x ^ 0x5c for x in block) + inner).digest()


def rejects(call):
    try:
        call()
    except (TypeError, ValueError):
        return True
    return False


def run():
    # Public, deterministic demonstration material, NOT a deployment secret.
    key = bytes(range(32))
    body = b'{"tool":"route","target":"sales","request":"demo-007"}'
    tag = sign_payload(key, body)
    known = [
        (b'\x0b' * 20, b'Hi There',
         'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'),
        (b'Jefe', b'what do ya want for nothing?',
         '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843'),
        (b'\xaa' * 131, b'Test Using Larger Than Block-Size Key - Hash Key First',
         '60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54'),
    ]
    checks = {}
    # RFC4231 cases1/2/6 exercise the unprefixed primitive, NOT our32-byte wrapper.
    checks['three_attributed_RFC4231_SHA256_vectors_ONE_group'] = all(
        hmac.digest(k, message, 'sha256').hex() == expected
        and two_stage_sha256_reference(k, message).hex() == expected
        for k, message, expected in known)
    checks['hand_byte_lengths_and_full_tag_representation'] = (
        len(body) == 54 and len(DOMAIN) == 14 and len(checked_message(key, body)) == 68
        and len(tag) == 32 and len(tag.hex()) == 64)
    checks['matching_body_and_tag_verify'] = verify_payload(key, body, tag) is True
    changed_body = body.replace(b'sales', b'admin')
    checks['changed_same_length_body_rejected'] = verify_payload(key, changed_body, tag) is False
    checks['different_key_rejected_for_own_fixture'] = verify_payload(bytes(reversed(key)), body, tag) is False
    checks['all_256_single_tag_bit_changes_ONE_group'] = all(
        not verify_payload(key, body, bytes(value ^ (1 << (bit % 8)) if i == bit // 8 else value
                                          for i, value in enumerate(tag)))
        for bit in range(256))
    replay = [verify_payload(key, body, tag), verify_payload(key, body, tag)]
    checks['same_packet_replay_is_still_accepted'] = replay == [True, True]
    reordered = b'{"request":"demo-007", "target":"sales", "tool":"route"}'
    checks['equal_JSON_meaning_is_not_equal_bytes'] = (
        json.loads(body) == json.loads(reordered) and body != reordered
        and not verify_payload(key, reordered, tag))
    binary = b'\x00\xff' + '\U0001f642'.encode('utf-8')
    checks['binary_NUL_and_UTF8_bytes_are_preserved'] = verify_payload(key, binary, sign_payload(key, binary))
    class BytesChild(bytes):
        pass
    checks['exact_bytes_types_no_implicit_conversion'] = all([
        rejects(lambda: sign_payload(bytearray(key), body)),
        rejects(lambda: sign_payload(key, memoryview(body))),
        rejects(lambda: sign_payload(key, BytesChild(body))),
        rejects(lambda: sign_payload(key, body.decode('ascii'))),
        rejects(lambda: verify_payload(key, body, tag.hex())),
        rejects(lambda: verify_payload(key, body, bytearray(tag))),
    ])
    boundary = b'x' * MAX_BODY_BYTES
    checks['own_key_body_and_tag_length_boundaries'] = (
        verify_payload(key, boundary, sign_payload(key, boundary))
        and rejects(lambda: sign_payload(key, boundary + b'x'))
        and rejects(lambda: sign_payload(key[:-1], body))
        and rejects(lambda: sign_payload(key + b'x', body))
        and rejects(lambda: verify_payload(key, body, tag[:-1]))
        and rejects(lambda: verify_payload(key, body, tag + b'x')))
    checks['empty_body_allowed_but_domain_still_present'] = (
        verify_payload(key, b'', sign_payload(key, b''))
        and sign_payload(key, b'') == two_stage_sha256_reference(key, DOMAIN))
    checks['different_application_prefix_fails_own_fixture'] = (
        not verify_payload(key, body, hmac.digest(key, body, 'sha256'))
        and not verify_payload(key, body, hmac.digest(key, b'voice-tool/v2\x00' + body, 'sha256')))
    oracle_cases = prefixed_cases = 0
    oracle_ok = True
    for key_length in (0, 1, 31, 32, 63, 64, 65, 131):
        reference_key = bytes((i * 17 + 3) % 256 for i in range(key_length))
        for message_length in (0, 1, 13, 63, 64, 65, 4096):
            message = bytes((i * 29 + 5) % 256 for i in range(message_length))
            oracle_ok &= hmac.digest(reference_key, message, 'sha256') == two_stage_sha256_reference(reference_key, message)
            oracle_cases += 1
            if key_length == 32:
                oracle_ok &= sign_payload(reference_key, message) == two_stage_sha256_reference(reference_key, DOMAIN + message)
                prefixed_cases += 1
    checks['independent_two_stage_key_block_edges_ONE_oracle_group'] = bool(oracle_ok) and oracle_cases == 56 and prefixed_cases == 7
    assert all(type(value) is bool and value for value in checks.values()), checks
    return {'python': platform.python_version(), 'dependencies': 'standard library only',
            'scope': 'exact-byte offline HMAC profile; no encryption, replay prevention, authorization or tool execution',
            'public_demo_key_hex': key.hex(), 'body_ascii': body.decode('ascii'), 'body_bytes': len(body),
            'domain_hex': DOMAIN.hex(), 'domain_bytes': len(DOMAIN), 'authenticated_bytes': len(DOMAIN + body),
            'tag_hex': tag.hex(), 'tag_bytes': len(tag), 'original_verifies': verify_payload(key, body, tag),
            'changed_body_verifies': verify_payload(key, changed_body, tag), 'same_packet_verified_twice': replay,
            'primitive_reference_comparisons_inside_one_group': oracle_cases,
            'profiled_prefix_comparisons_inside_same_group': prefixed_cases, 'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run(), ensure_ascii=False, indent=2))

در مستندات hmac پایتون، compare_digest برای مقایسهٔ برچسبِ دریافتی پیشنهاد می‌شود، چون برحسبِ محتوای مقایسه‌شده زود متوقف نمی‌شود. هر دو ورودیِ مقایسه در رابطِ ما bytes و هم‌طول‌اند. از این انتخاب، زمانِ ثابتِ کل برنامه، شبکه یا اعتبارسنجیِ ورودی را نتیجه نمی‌گیریم؛ آزمایشِ زمانی اجرا نشده است.

اگر نوع یا طول با قرارداد ناسازگار باشد، خطای روشن می‌گیریم؛ اگر شکلِ برچسب پذیرفتنی ولی مقدارش متفاوت باشد، پاسخ False است. bytearray، memoryview، زیرکلاسِ bytes و رشتهٔ هگز بی‌صدا تبدیل نمی‌شوند. این سخت‌گیریِ نوع، تصمیمِ رابطِ خودمان است و محدودیتِ عمومیِ کتابخانهٔ hmac معرفی نمی‌شود.

کلیدِ آزمایش، بایت‌های صفر تا سی‌ویک است و عمداً در خروجی چاپ می‌شود تا محاسبه بازتولیدپذیر باشد. این کار، تولید یا نگهداریِ کلیدِ محرمانه را نمایش نمی‌دهد. RFC دربارهٔ اهمیتِ انتخاب و حفاظتِ کلید صحبت می‌کند؛ کدِ حاضر توزیع، تعویض، بازیابی یا حفاظتِ رازها را پیاده نکرده است.

۴. بردارِ استاندارد و مرجعِ دو مرحله‌ای را بسنجیم

دو مرحلهٔ مستقل و شاهدِ استاندارد
تصویر مفهومی: دو مرحلهٔ مستقل و شاهدِ استاندارد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

RFC 4231 بردارهای شناخته‌شدهٔ HMAC-SHA256 دارد. سه موردِ اول، دوم و ششم را با مقدارِ موردانتظارِ مستقل از برنامه می‌سنجیم. این داده‌ها صریحاً متعلق به منبع‌اند، نه پیام‌های تألیفیِ ما. آزمون روی تابعِ پایه و پیامِ بدونِ پیشوند است؛ کلیدهای آن بردارها لزوماً در پروفایلِ سی‌ودوبایتیِ ما جا نمی‌گیرند.

مرجعِ دوم، ساختِ دو مرحله‌ایِ تعریفِ HMAC را با hashlib اجرا می‌کند. کلیدِ بلندتر از بلوک ابتدا درهم می‌شود؛ سپس کلیدِ تکمیل‌شده با الگوهای داخلی و خارجی ترکیب می‌شود. این مرجع hmac یا sign_payload را صدا نمی‌زند. بااین‌حال، هر دو مسیر برای خودِ SHA256 به کتابخانهٔ استاندارد متکی‌اند؛ یک پیاده‌سازیِ مستقلِ تابعِ درهم‌سازی ننوشته‌ایم.

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "scope": "exact-byte offline HMAC profile; no encryption, replay prevention, authorization or tool execution",
  "public_demo_key_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
  "body_ascii": "{\"tool\":\"route\",\"target\":\"sales\",\"request\":\"demo-007\"}",
  "body_bytes": 54,
  "domain_hex": "766f6963652d746f6f6c2f763100",
  "domain_bytes": 14,
  "authenticated_bytes": 68,
  "tag_hex": "73de2443843f1b6dd06e8595fd3292d8c66887c11d869393cdf6a18d65fab194",
  "tag_bytes": 32,
  "original_verifies": true,
  "changed_body_verifies": false,
  "same_packet_verified_twice": [
    true,
    true
  ],
  "primitive_reference_comparisons_inside_one_group": 56,
  "profiled_prefix_comparisons_inside_same_group": 7,
  "checks": {
    "three_attributed_RFC4231_SHA256_vectors_ONE_group": true,
    "hand_byte_lengths_and_full_tag_representation": true,
    "matching_body_and_tag_verify": true,
    "changed_same_length_body_rejected": true,
    "different_key_rejected_for_own_fixture": true,
    "all_256_single_tag_bit_changes_ONE_group": true,
    "same_packet_replay_is_still_accepted": true,
    "equal_JSON_meaning_is_not_equal_bytes": true,
    "binary_NUL_and_UTF8_bytes_are_preserved": true,
    "exact_bytes_types_no_implicit_conversion": true,
    "own_key_body_and_tag_length_boundaries": true,
    "empty_body_allowed_but_domain_still_present": true,
    "different_application_prefix_fails_own_fixture": true,
    "independent_two_stage_key_block_edges_ONE_oracle_group": true
  }
}

در خروجی، وارسیِ بدنهٔ اصلی True و تغییرِ sales به admin برابر False است. برچسبِ باینری سی‌ودو بایت و نمایشِ هگز آن شصت‌وچهار نویسه دارد. این دو نمایش را در رابط مخلوط نمی‌کنیم. دست‌کاریِ تک‌تکِ ۲۵۶ بیتِ برچسب برای همین شاهد رد می‌شود؛ این مشاهده، اثباتِ ناممکن بودنِ جعل در همهٔ شرایط نیست.

هشت طولِ کلید و هفت طولِ پیام، پنجاه‌وشش مقایسهٔ تابعِ پایه می‌سازند؛ هفت مقایسهٔ پیشونددار هم در همان گروهِ مرجع انجام می‌شوند. مرزِ بلوک، کلیدِ بلند و پیامِ تهی پوشش دارند. کلِ این مجموعه یک گروه است و تعدادِ واقعیِ گروه‌های نام‌دار چهارده می‌ماند؛ شمارِ حالت‌ها را به شمارِ گروه‌ها اضافه نمی‌کنیم.

منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند. RFC 2104 مربوط به فوریهٔ ۱۹۹۷ و RFC 4231 مربوط به دسامبرِ ۲۰۰۵ است؛ مثال‌های تاریخیِ MD5 و ادعاهای کاراییِ آن‌ها مبنای انتخابِ الگوریتمِ این درس نیستند. مستنداتِ خوانده‌شده نسخهٔ 3.14.8 دارد و با اجرای واقعیِ 3.14.5 جدا ثبت شده است.

۵. چهار پرسش دربارهٔ بازپخش، کلید و نمایشِ پیام

تکرارِ پیام هنوز ممکن است
تصویر مفهومی: تکرارِ پیام هنوز ممکن است؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: برچسبِ درست، اجرای دوباره را متوقف می‌کند؟ پاسخ: خیر؛ همان پیام و همان برچسب دو بار True می‌دهند. تابع وضعیتِ درخواست‌های قبلی ندارد. برای تصمیمِ تکرار باید زمینه و قراردادِ جداگانه‌ای تعریف شود؛ این مثال چنین سازوکاری را پنهانی فرض نمی‌کند.

پرسش دوم: برابر بودنِ دیکشنری‌های JSON کافی است؟ پاسخ: در این رابط خیر؛ ورودی بایت است و جابه‌جاییِ کلید یا فاصله، ورودیِ دیگری می‌سازد. اگر قراردادی به نمایشِ یکتا نیاز دارد، باید همان قرارداد جدا طراحی و در دو طرف یکسان اجرا شود.

پرسش سوم: شصت‌وچهار نویسهٔ هگز را می‌توان برچسبِ سی‌ودوبایتی فرستاد؟ پاسخ: خیر؛ رابط، bytes خام می‌خواهد و رشته را رد می‌کند. تبدیلِ هگز، کنترلِ خطا و قالبِ انتقال بیرونِ این برنامه است؛ طولِ نمایش را با طولِ دادهٔ باینری یکی نمی‌گیریم.

پرسش چهارم: پذیرفته شدنِ پیام، هویتِ تماس‌گیرنده را ثابت می‌کند؟ پاسخ: خیر؛ برنامه فقط رابطهٔ کلید و بایت‌ها را می‌سنجد. در این آزمایش خودِ کلید عمومی است. هویت، محرمانگی، مجوزِ مقصد و اعتبارِ معنای درخواست از این خروجی به دست نمی‌آیند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.