راهنمای تخصصی SLSA Build Provenance در نسخه ۱.۲
چرا provenance برای زنجیره تأمین مهم است؟
در زنجیره تأمین نرمافزار، هش یک فایل فقط محتوای همان فایل را مشخص میکند؛ نمیگوید چه کسی آن را ساخت، چه ورودیهایی داشت یا چه فرایندی اجرا شد. SLSA Build Provenance این خلأ را با گواهی قابلراستیآزمایی پر میکند: شرح میدهد سکوی ساخت مشخص، یک یا چند artifact را با چه تعریف ساختی تولید کرده است. مصرفکننده از این راه میتواند خروجی را به منبع، پیکربندی و وابستگیهای دقیق برگرداند و در صورت نیاز بازسازی کند.
در SLSA v1.2، provenance یک predicate در چارچوب attestation اینتوتو است. مقدار predicateType باید دقیقاً https://slsa.dev/provenance/v1 باشد، نه URI صفحه نسخه. این URI فقط شماره major را دارد تا تغییرات minor سازگار، مصرفکننده را بیدلیل مجبور به تغییر نکنند.
معماری کلی: از attestation تا artifact
مدل provenance چهار جزء عملی دارد. Statement بیرونی اینتوتو subject را برای معرفی خروجیها حمل میکند. در predicate، buildDefinition ورودی و قرارداد ساخت را بیان میکند و runDetails اجرای مشخص و سکوی اجرا را شرح میدهد. امضای attestation هویت امضاکننده را به سند وصل میکند؛ امضا بخشی از معنای build نیست، اما برای اصالت سند و انتخاب کلید اعتماد لازم است.
این تفکیک دو پرسش را جدا میکند: «چه چیزی باید اجرا میشد؟» و «چه کسی و در کدام اجرای مشخص آن را اجرا کرد؟» نمودار رسمی provenance-model.svg همین رابطه میان subject، تعریف ساخت و جزئیات اجرا را نشان میدهد.
subject؛ خروجیای که باید به آن اعتماد کرد
subject فهرست artifactهای تولیدشده است. برای هر مورد، digest شناسه اصلی است و نام فقط خوانایی را بهتر میکند. policy باید digest خروجی دریافتشده از رجیستری یا مخزن را با subject تطبیق دهد؛ نام برابر با digest متفاوت ارزشی ندارد. چند خروجی مستقل را میتوان در یک attestation آورد، اگر رابطه همه با یک اجرای ساخت روشن باشد.
buildDefinition؛ قرارداد ورودی و فرایند
buildDefinition میگوید چه چیزی به سکوی ساخت داده شد و چگونه باید تفسیر شود. buildType شناسه الگوی ساخت است، نه فقط نام ابزار. URI آن بهتر است به توضیح، schema پارامترها، روش آغاز و نمونه کامل اشاره کند. workflow تولیدی، آزمایشی و انتشار باید قرارداد یا buildType قابل تشخیص داشته باشند تا verifier معنای فیلدها را بداند.
externalParameters ورودیهایی هستند که کاربر یا tenant کنترل میکند؛ مانند URI مخزن، ref، target یا نسخهای که رفتار ساخت را عوض میکند. آنها غیرقابلاعتمادند و مصرفکننده باید بررسیشان کند. در سطح بالاتر SLSA فهرست باید کامل باشد؛ یعنی مسیر پنهان دیگری برای اثرگذاری بیرونی وجود نداشته باشد. آن را کوچک و ساده نگه دارید تا policy پایدار بماند و فیلد ناشناخته را رد کنید، نه اینکه نادیده بگیرید.
internalParameters دادهای است که موجودیت معرفیشده با builder.id تعیین میکند؛ مانند جزئیات داخلی اجرا یا شناسه ماشین. این بخش برای بازسازی، اشکالزدایی و رخداد مفید است، اما در محدوده اعتماد builder قرار دارد و معمولاً ورودی غیرقابلاعتماد مصرفکننده نیست. یک نام فیلد را در هر دو مجموعه تکرار نکنید؛ مالکیت داده باید روشن بماند.
پارامتر باید فقط مقدار واقعی عبورکرده از interface باشد. digest فایلی که پارامتر به آن اشاره میکند، بهتر است در resolvedDependencies بیاید. مثلاً branch main متحرک است و باید به commit مشخص resolve شود؛ URI بهتنهایی بازتولیدپذیر نیست.
resolvedDependencies؛ وابستگیهای حلشده، نه فهرست تزئینی
این آرایه مجموعهای نامرتب از artifactهای لازم در آمادهسازی یا اجرای ساخت است. هر ResourceDescriptor میتواند URI، digest، نام، محل دانلود، نوع رسانه، content و annotation داشته باشد. برای امنیت، ترکیب URI و digest معمولاً مهمترین است.
ثبت وابستگی باید فراتر از کتابخانه مستقیم باشد. اگر اسکریپت ساخت فایلی بگیرد و آن فایل آرشیو دیگری را دانلود کند، هر دو برای تحلیل زنجیره مهماند. هدف، تحلیل بازگشتی از خروجی به منبع و ابزار و سپس provenance آنهاست. کاملبودن در سطوح پایینتر ممکن است بهترین تلاش باشد، اما دامنه نقص باید صادقانه مستند شود؛ حذف وابستگی پرخطر سطح اعتماد را بالا نشان میدهد.
پیکربندی را تا حد امکان به artifact ورودی منتقل کنید. بهجای flagهای فراوان در پارامتر بیرونی، آنها را در فایل نسخهگذاریشده build configuration قرار دهید و digest فایل را بررسی کنید. اگر مشتری YAML را به JSON موقتی تبدیل میکند، منشأ JSON دشوار میشود؛ راه بهتر خواندن مستقیم configuration از کنترل نسخه و ثبت URI و digest تأییدشده است. در غیر این صورت، digest پیکربندی و attestation جداگانه برای پیوند آن با منبع لازم است.
runDetails و هویت builder
runDetails اجرای واقعی را حمل میکند. builder اجباری و metadata و byproducts تکمیلیاند. invocationId باید امکان یافتن لاگ و رخداد را بدهد؛ startedOn و finishedOn زمانهای اجرا هستند و برای audit و رخداد مفیدند.
مهمترین میدان builder.id است. این URI باید کل موجودیتهای مورداعتماد برای اجرای ساخت و تولید provenance را نمایندگی کند: نرمافزار، سختافزار و control plane. منظور یک نصب عمومی از ابزار CI نیست؛ حالت عملیاتی مشخص ممکن است مرز اعتماد جدا داشته باشد. hosted runner و self-hosted runner را با یک ID نمایش ندهید اگر ضمانتشان متفاوت است.
builder.id باید به مستنداتی resolve شود که دامنه شناسه، سطح ادعایی SLSA، تضمین دقت و کاملبودن، داده tenant و معنای extensionها را شرح دهد. حالتهای ناهمگون باید ID جدا و ترجیحاً signer جدا داشته باشند تا حالت ضعیف بر قوی اثر نگذارد. builder.version نسخه اجزا و builderDependencies ابزارهای orchestrator بیرون workload را ثبت میکنند؛ ابزارهایی که ممکن است بر provenance یا ضمانت امنیتی اثر بگذارند.
signer با builder یکی نیست. signer سند را امضا میکند، اما builder عملیات ساخت را انجام میدهد. یک signer میتواند برای چند builder سند بسازد؛ بنابراین verifier باید جفتهای مجاز signer–builder را صریحاً فهرست کند. کلید معتبر برای builder نامرتبط کافی نیست.
امضا، ریشه اعتماد و threat model
امضا را در دو مرحله ببینید: اعتبار رمزنگاری attestation و زنجیره کلید تا trust root، سپس ارزیابی محتوا و رابطه آن با artifact. trust root میتواند کلید یا هویت ثبتشده سازمان یا سامانه مورد اعتماد policy باشد. صفحه provenance ریشه واحدی برای همه محیطها تعیین نمیکند؛ انتخاب باید با builder.id و مدل امنیتی پلتفرم سازگار باشد.
تهدیدهای اصلی شامل جایگزینی artifact، branch یا tag متحرک، حذف dependency غیرمستقیم، جعل builder، ثبت غلط پارامترها، runner آلوده و امضای سند با کلید معتبر اما برای builder نامرتبط است. ارتباط با control plane یا cache مستقیم در provenance نمیآید و در محدوده اعتماد builder است؛ اگر بر رفتار ساخت اثر دارد، باید dependency یا ورودی قابلبررسی شود.
در threat model مرز tenant، workload، orchestrator، control plane، مخزن کلید و مصرفکننده را روشن کنید: چه کسی external parameter را تغییر میدهد؟ چه کسی subject را درج میکند؟ کدام جزء میتواند گزارش بسازد اما امضا نکند؟ اگر runner آلوده شود، builder چه تضمینی دارد؟ پاسخ، مرز بین فیلد معتبر و ادعا را مشخص میکند.
اتصال به CI/CD
برای CI/CD، تولید provenance را پس از تعیین ورودی و پیش از انتشار به مرحله مورداعتماد بسپارید، نه به اسکریپت tenant. pipeline باید commit یا digest منبع، buildType، پارامترها، dependencyهای resolveشده و subject نهایی را جمع کند؛ سپس control plane attestation را بسازد و با هویت مجاز امضا کند. شناسه CI در invocationId مسیر audit تا لاگ و artifact را کوتاه میکند.
در ساخت چندمرحلهای، خروجی هر مرحله را با digest به مرحله بعد بدهید و provenance قبلی را قابل ردیابی نگه دارید. cache نباید ورودی واقعی را پنهان کند؛ اگر بر رفتار ساخت اثر دارد، جایگاهش در dependency یا کنترل پلتفرم روشن شود. برای production، artifact فقط پس از بررسی attestation امضاشده و منطبق با policy promote شود؛ fail-open ارزش provenance را تشریفاتی میکند.
verification و policy enforcement
یک verifier عملی باید این زنجیره را اجرا کند: امضا و trust root؛ predicateType و فیلدهای اجباری؛ تطبیق digest دریافتی با subject؛ جفت signer–builder و builder.id مجاز؛ schema buildType و رد پارامترهای ناشناخته؛ بررسی commit و digestهای resolvedDependencies؛ و نگهداری زمان، invocationId و byproductهای لازم برای audit.
Policy باید انتظار دقیق تعریف کند، نه فقط وجود یک فیلد را بپذیرد: artifact از builder تولیدی مشخص، منبع مجاز و dependencyهای digestشده بیاید؛ build آزمایشی در production پذیرفته نشود؛ و extension نتواند deny را به allow تبدیل کند. extensionها را namespace کنید و معنای فیلدهای استاندارد را تغییر ندهید. minor version را سازگار بخوانید، اما در برابر major ناشناخته توقف کنید.
خطاهای رایج
- ثبت نام مخزن یا tag بهجای digest commit؛ بازتولید را شکننده میکند.
- یکیگرفتن signer و builder؛ کلید معتبر، اجرای معتبر با builder مورد انتظار را ثابت نمیکند.
- مخلوطکردن پارامتر tenant و تنظیمات داخلی یا انتظار اعتماد به داده نادرست.
- گزارشنکردن ابزارهای زنجیرهای که اسکریپت ساخت دانلود و اجرا میکند.
- یکسانگرفتن hosted و self-hosted runner با وجود ضمانتهای متفاوت.
- انباشت همه فایلهای میانی در
byproductsیا حذف log و configuration لازم برای رخداد. - استفاده از URI نسخه v1.2 برای
predicateTypeبهجای URI major استاندارد. - پذیرش بیقیدوشرط extensionها و ازکارانداختن قواعد deny.
- تولید provenance روی workstation مشتری و فرض منشأ قابل اعتماد فقط از روی digest.
چکلیست پیادهسازی
۱. مرز اعتماد platform، tenant، runner، control plane و signer را بنویسید و برای هر حالت عملیاتی builder.id مستند و متمایز تعیین کنید.
۲. buildType را به یک قرارداد قابل خواندن وصل کنید؛ schema، ورودیهای مجاز، روش آغاز و نمونه کامل آن را منتشر کنید.
۳. فهرست external parameterها را کوچک کنید، همه مسیرهای اثرگذاری بیرونی را پیدا کنید و فیلدهای ناشناخته را در verifier رد کنید.
۴. منبع، commit، اسکریپت، toolchain و artifactهای دانلودی را با URI و digest در resolvedDependencies ثبت کنید؛ برای زنجیرههای غیرمستقیم روش کشف داشته باشید.
۵. subject را از خروجی نهایی و در همان قالب digestی که مصرفکننده مقایسه میکند تولید کنید؛ نام را فقط برای خوانایی نگه دارید.
۶. invocationId یکتا، زمانهای شروع و پایان، نسخه اجزای builder و byproductهای ارزشمند مانند log یا پیکربندی حلشده را اضافه کنید.
۷. attestation را با کلید مجاز امضا کنید، trust root و چرخش کلید را عملیاتی کنید و جفتهای signer–builder را در policy ثبت کنید.
۸. verifier را پیش از رجیستری و در دروازه انتشار اجرا کنید؛ نتیجه fail-closed را مانیتور کنید و رخدادهای ردشده را قابل بررسی نگه دارید.
۹. با سناریوهای branch متحرک، dependency مخرب، runner خودمیزبان، cache ناسالم و provenance دستکاریشده تست منفی انجام دهید.
۱۰. مستندات builder و محدودیت کاملبودن provenance را بهروز نگه دارید. provenance محصول نهایی نیست؛ یک قرارداد قابل بررسی است و ارزش آن از صداقت مدل اعتماد، کیفیت digestها و اجرای واقعی policy به دست میآید.
جمعبندی
SLSA Build Provenance v1.2 هم مدل داده است و هم قرارداد اعتماد. subject خروجی را تثبیت میکند، buildDefinition میگوید چه چیزی باید اجرا میشد، resolvedDependencies مسیر ورودیها را باز میکند و runDetails اجرای مشخص و builder را معرفی میکند. امضا اصالت گواهی را به trust root میرساند، اما فقط policyای که signer، builder، پارامترها، digestها و سطح تضمین را یکجا میسنجد، جلوی انتشار artifact نامطمئن را میگیرد. پیادهسازی موفق از تعریف مرز اعتماد و قرارداد buildType آغاز میشود، نه از تولید یک JSON تزئینی.