بیلدهای تکرارپذیر از یه توصیه‌ی خوب به یه الزام امنیتی تبدیل شدن

اشتراک‌گذاری:
بیلدهای تکرارپذیر از یه توصیه‌ی خوب به یه الزام امنیتی تبدیل شدن

در بیشتر تاریخ نرم‌افزار، «بیلدهای تکرارپذیر» یه دغدغه‌ی گوشه‌ای بود که بیشتر به تیم پکیج‌بندی Debian و یه مشت محقق امنیتی نگران حملات زنجیره‌ی تأمین در سطح کامپایلر مربوط می‌شد. این وضعیت عوض شده. از سال ۲۰۲۶، بیلدهای تکرارپذیر و هرمتیک از یه ویژگی مطلوب مهندسی به چیزی شبیه یه شرط اجباری در فرآیند تأمین تبدیل شدن: مشتریان سازمانی، قراردادهای دولتی، و بنیادهای Open Source امنیت‌محور دارن بیشتر و بیشتر از فروشندگان می‌خوان که ثابت کنن فایل‌های باینری‌شون رو می‌شه به‌طور مستقل بیت‌به‌بیت از سورس کد دوباره ساخت — و تیم‌هایی که نمی‌تونن به این سوال جواب بدن دارن به خاطرش معامله‌هاشون رو از دست می‌دن.

استدلال اینجا ساده‌ست: بیلدی که نمی‌تونی دوبارهش بسازی، بیلدی هست که نمی‌تونی کاملاً آدیتش کنی. در سالی که رخنه‌های زنجیره‌ی تأمین — CI runner های لو رفته، کَش‌های بیلد آلوده، نسخه‌های CLI بک‌دوردار روی رجیستری‌های پکیج — تبدیل به اخبار روزمره‌ی امنیتی شدن و دیگه رویداد نادری نیستن، «به باینری اعتماد کن چون به فروشنده اعتماد داری» یه رویکرد امنیتی کافی برای هیچ‌کسی که با داده‌های حساس یا زیرساخت‌های حیاتی سروکار داره نیست.

«تکرارپذیری» در عمل یعنی چی

یه بیلد تکرارپذیر هست وقتی که با همون سورس کد و همون دستورالعمل‌های بیلد اعلام‌شده، دو تا دستگاه مختلف — که شاید سال‌ها از هم فاصله داشته باشن، روی سخت‌افزارهای متفاوت، توی مسیرهای مختلف — خروجی‌های بایت‌به‌بایت یکسان تولید کنن. این به نظر می‌رسه که باید رفتار پیش‌فرض هر کامپایلری باشه، ولی نیست — چون خروجی‌های بیلد معمولاً داده‌های غیرقطعی رو توی خودشون جاسازی می‌کنن: تایم‌استمپ‌هایی که توی باینری‌ها پخته می‌شن، مسیر مطلق فایل‌سیستم پوشه‌ی بیلد، ترتیب تصادفی‌ای که کامپایلر موقع تولید سیمبول‌های دیباگ روی یه hash map پیمایش می‌کنه، یا داده‌های locale و timezone که به محیط اجرا بستگی دارن. هر کدوم از اینا یه بیلد که در غیر این صورت یکسان بود رو به یه باینری تبدیل می‌کنن که چند بایت باهاش فرق داره — و این برای هر تأییدکننده‌ای که سعی می‌کنه مطمئن بشه «این باینری با این سورس کد مطابقت داره»، هیچ فرقی با یه دستکاری واقعی نداره.

پروژه‌ی Reproducible Builds (reproducible-builds.org) که از دل تلاش‌های Debian از حدود سال ۲۰۱۳ بیرون اومد، ده‌ها منبع مشخص غیرقطعی‌بودن رو در toolchain‌های رایج و پَچ‌های لازم برای حذف هر کدوم فهرست کرده. اکثر کامپایلرهای اصلی الان از فلگ‌های تکرارپذیری پشتیبانی می‌کنن — سیستم بیلد Go از نسخه‌ی ۱.۱۳ از طریق حذف و کوتاه‌کردن مسیرهای محلی از باینری‌ها به‌صورت بومی از این قضیه پشتیبانی می‌کنه؛ زبان Rust هم --remap-path-prefix و ترتیب قطعی متادیتا رو اضافه کرده؛ و LLVM/Clang هم -ffile-prefix-map رو برای همین هدف پشتیبانی می‌کنن. بخش سخت فلگ‌های کامپایلر نیست — اینه که غیرقطعی‌بودن رو از همه چیزی که بالادست کامپایلر قرار داره حذف کنی: اسکریپت‌های بیلد، ترتیب رزولوشن وابستگی‌ها، و container یا image ماشین مجازی که بیلد داخلش اجرا می‌شه.

SLSA یه چارچوب و اهرمی برای تدارکات فراهم می‌کنه

چارچوب SLSA (مخفف Supply-chain Levels for Software Artifacts، که «سالسا» خونده می‌شه) اول درون Google توسعه یافت و الان زیر چتر OpenSSF قرار داره. این چارچوب یکپارچگی فرآیند build رو به چهار سطح رسمی تعریف می‌کنه: از «حداقل فرآیند build ردیابی می‌شه» (سطح ۱) تا «build ها کاملاً ایزوله، قابل بازتولید، و توسط سیستمی که خودِ سازنده‌ی build کنترلش نمی‌کنه تأیید می‌شن» (سطح ۴). چیزی که SLSA رو از یه موضوع صرفاً آکادمیک به یه ابزار مهم تجاری تبدیل کرده اینه که به تیم‌های خرید و تدارکات یه چک‌باکس واقعی داده که می‌تونن توی پرسشنامه‌های امنیتی تأمین‌کنندگان بذارنش: سوال «build pipeline شما به چه سطحی از SLSA می‌رسه؟» الان واقعاً توی ارزیابی‌های امنیتی سازمان‌های بزرگ مطرح می‌شه و جای پیشینیان مبهم‌تری مثل «فرآیندهای امنیتی SDLC خودتون رو توضیح بدید» رو گرفته — سوال‌هایی که در عمل تأیید مستقل‌شون تقریباً غیرممکن بود.

رجیستری‌های بزرگ پکیج مثل npm، PyPI و چند تای دیگه دارن با تأییدیه‌های منشأ (provenance attestations) گره‌خورده به سطوح SLSA آزمایش می‌کنن. یه پکیج می‌تونه یه بیانیه‌ی امضاشده‌ی رمزنگاری‌شده همراه خودش داشته باشه که دقیقاً مشخص کنه کدوم CI job روی کدوم commit اون رو ساخته؛ بیانیه‌ای که مصرف‌کننده (یا یه اسکنر خودکار) می‌تونه بدون اینکه به خودِ رجیستری اعتماد کنه، تأییدش کنه. این مستقیماً الگوی دقیق حملاتی رو نشونه می‌گیره که پشت چند حادثه‌ی زنجیره‌ی تأمین در سال ۲۰۲۶ بودن: حساب کاربری یه نگهدارنده که هک شده، یا یه مرحله‌ی build که backdoor داشته و یه نسخه‌ی مخرب رو زیر نام یه پکیج قانونی منتشر کرده — جایی که آرتیفکت مخرب هیچ ارتباط قابل‌تشخیصی با مخزن کد منبع اصلی نداشت.

این موضوع عملاً چه تأثیری روی تیم‌های مهندسی داره؟

رسیدن به بازتولیدپذیری hermetic کامل در سطح SLSA Level 4 — یعنی بیلدی که کاملاً از محیط بیرونی ایزوله‌ست — یه کار بزرگه که بیشتر تیم‌ها نباید اول از همه سراغش برن. مسیر تدریجی که بیشترین سود امنیتی عملی رو داره اینه:

  • هر dependency رو با hash پین کن، نه با بازه نسخه. این پرسودترین و کم‌دردسرترین قدمه: lockfile هایی که content hash دارن (که الان در بیشتر package manager های مدرن استاندارد هست) مشکل «dependency بین بیلدها بی‌سروصدا عوض شده» رو از بین می‌برن و همزمان رایج‌ترین بردار حمله‌ی زنجیره تأمین رو هم می‌بندن — یعنی وقتی یه dependency آسیب‌دیده یه نسخه جدید رو زیر یه بازه نسخه موجود منتشر می‌کنه.
  • برچسب‌های زمانی جاسازی‌شده و مسیرهای مطلق رو از خروجی بیلد حذف کن با استفاده از فلگ‌های بازتولیدپذیری toolchain ات (-trimpath برای Go، --remap-path-prefix برای Rust، -ffile-prefix-map برای Clang/LLVM، و SOURCE_DATE_EPOCH به عنوان استاندارد مشترک بین اکوسیستم‌ها برای برچسب‌های زمانی). این معمولاً یه تغییر یک‌خطی در تنظیمات CI به ازای هر toolchain هست.
  • داخل یه container image پین‌شده با آدرس hash بیلد بگیر، نه از base image های «latest»؛ اینطوری خودِ محیط بیلد هم بخشی از چیزیه که داری تأییدش می‌کنی، نه یه متغیر بدون ممیزی.
  • گواهی‌های provenance بیلد (attestation) رو کنار release هات منتشر کن — چه provenance مربوط به SLSA، چه حداقل یه رکورد امضاشده از commit hash، محیط بیلد و زمان بیلد — حتی قبل از اینکه دنبال بازتولیدپذیری بیت‌به‌بیت کامل بری. اینطوری مصرف‌کننده‌های پایین‌دست همین الان چیزی دارن که بتونن تأییدش کنن، در حالی که هنوز داری به سمت بازتولیدپذیری hermetic پیش می‌ری.
  • به صورت دوره‌ای release های قدیمی رو دوباره بیلد کن و خروجی رو diff بگیر — به عنوان یه جاب دائمی CI، نه یه ممیزی یک‌بار مصرف. یه تضمین بازتولیدپذیری که موقع release درست بوده ولی از اون موقع بی‌سروصدا منحرف شده، دیگه تضمینی نیست که بشه روش حساب کرد.

هیچ‌کدام از اینا نیاز به بازنویسی کد یا toolchain جدید نداره. نیاز داره که determinism بیلد رو مثل یه ویژگی قابل ردیابی سیستم در نظر بگیری — دقیقاً همون‌طوری که تیم‌ها الان پوشش تست یا تطابق type-check رو دنبال می‌کنن — و تیم‌هایی که الان شروع کنن، خیلی قبل از اینکه «چه سطح SLSA ای دارید؟» به یه شرط سخت در قراردادهایی که می‌خوان ببندن تبدیل بشه، با یه عدد واقعی جوابش رو می‌دن.

اشتراک‌گذاری: