راهنمای امضای کد در .NET MAUI: گواهی iOS، Keystore اندروید و Fastlane match در ۲۰۲۶
راهنمای عملی امضای کد در .NET MAUI برای iOS و اندروید: گواهی توزیع، پروفایل Provisioning، Keystore، Play App Signing و Fastlane match با نمونه GitHub Actions.
امضای کد در .NET MAUI فرآیندی است که با استفاده از گواهیهای رمزنگاریشده، هویت توسعهدهنده را به بستهی اپلیکیشن (.ipa برای iOS و .aab برای اندروید) پیوند میزند تا App Store و Google Play اپ را معتبر بشناسند و روی دستگاه کاربر اجرا شود. راستش را بخواهید، بیشتر تیمهایی که تازه از Xamarin به MAUI مهاجرت کردهاند این بخش را جدی نمیگیرند و بعد در روز انتشار با خطاهای No matching provisioning profile یا Keystore was tampered with غافلگیر میشوند. من خودم دو سال پیش، شب قبل از یک انتشار حیاتی، ساعت ۲ بامداد دنبال Alias گمشده در Slack یک همکار سابق میگشتم؛ از آن شب به بعد یاد گرفتم امضای کد را قبل از خط اول UI پیکربندی کنم. در این راهنما گامبهگام هر چیزی که برای امضای امن، تکرارپذیر و خودکار نیاز دارید را پوشش میدهم.
امضای iOS به سه چیز نیاز دارد: گواهی توزیع (Distribution Certificate)، پروفایل Provisioning و App ID ثبتشده در Apple Developer.
امضای اندروید مدرن دو کلید جداگانه دارد: Upload Key (شما نگه میدارید) و App Signing Key (Google Play نگه میدارد و توسط Play App Signing مدیریت میشود).
Fastlane match استاندارد صنعت برای همگامسازی امن گواهیهای iOS در یک تیم و در CI/CD است و از Git خصوصی، S3 یا Google Cloud Storage بهعنوان بکاند رمزنگاریشده استفاده میکند.
در پروژه .csproj با پراپرتیهای <CodesignProvision>، <CodesignKey> برای iOS و <AndroidSigningKeyStore>، <AndroidSigningKeyAlias> برای اندروید امضا پیکربندی میشود.
برای انتشار خودکار به App Store Connect باید یک App Store Connect API Key (فایل .p8) بسازید؛ در GitHub Actions این کلید و کل چرخهی امضا از طریق Secrets تزریق میشود.
گمشدن Keystore اندروید در گذشته فاجعه بود، ولی با فعال بودن Play App Signing میتوانید Upload Key را ریست کنید (به شرطی که از قبل درست پیکربندی کرده باشید).
چرا امضای کد در .NET MAUI پیچیدهتر از حد انتظار است؟
یک اپلیکیشن Native iOS یا اندروید معمولاً با یک IDE (Xcode یا Android Studio) امضا میشود که خودش تمام مراحل را انتزاع میکند. در .NET MAUI اما هدف ما ساخت یک پروژه است که برای چند پلتفرم خروجی میدهد؛ یعنی زنجیرهی امضا باید هم روی macOS برای iOS و Mac Catalyst و هم روی هر ماشینی برای اندروید و ویندوز اجرا شود. این یعنی امضا نه در UI بلکه در MSBuild اتفاق میافتد و هر خطای پیکربندی، در لاگهای طولانی dotnet publish پنهان میشود.
در پروژهای که پارسال ادیت میکردم دیدم گواهی توسعهدهنده روی لپتاپ یک نفر بود، Keystore اندروید در Dropbox شخصی مدیر فنی سابق مانده بود و هیچکس رمز Alias را نمیدانست. سه دام اصلی که در .NET MAUI بیشتر از Xamarin دیده میشود:
اجباریشدن AAB روی Google Play: از آگوست ۲۰۲۱ اپهای جدید باید Android App Bundle باشند و این یعنی Play App Signing بهطور پیشفرض فعال است. الگوی قدیمی «یک Keystore بساز و برای همیشه نگهدار» دیگر بهترین روش نیست.
NativeAOT و امضا: در .NET 9/10 و MAUI مدرن، حالت AOT روی iOS/Mac Catalyst باینریهای بزرگتری میسازد که فرآیند codesign روی آنها کندتر است. اگر تایماوت CI کوتاه باشد، امضا در نیمهراه قطع میشود.
Mac Catalyst و Notarization: اگر میخواهید همان MAUI را روی Mac App Store یا خارج از فروشگاه توزیع کنید، به گواهی Developer ID و مرحلهی جداگانهی Notarization اپل نیاز دارید که ربطی به گواهی iOS ندارد.
امضای iOS: گواهی توزیع، پروفایل Provisioning و App ID
امضای iOS یک هرم سهطبقه است. در پایه، App ID قرار دارد که همان Bundle Identifier اپلیکیشن (مثلاً com.mobiletechlead.sampleapp) در پنل Apple Developer ثبتشده است. روی آن، گواهی توزیع (Distribution Certificate) قرار میگیرد که یک زوج کلید عمومی/خصوصی است و در Keychain مک ذخیره میشود. در نهایت، Profile Provisioning این دو را به هم گره میزند و مشخص میکند این گواهی مجاز است این App ID را با کدام Entitlements امضا کند.
برای انتشار در App Store به این ترکیب نیاز دارید:
یک Apple Developer Program فعال (سالی ۹۹ دلار).
یک App ID با شناسه Bundle یکتا و Capabilityهای دقیق اپ (Push Notifications، Sign in with Apple و…).
یک iOS Distribution Certificate (نوع Apple Distribution که هم برای Ad Hoc و هم App Store کار میکند).
یک App Store Provisioning Profile که به App ID و Distribution Certificate متصل است.
ساخت گواهی و پروفایل بهصورت دستی مشکل ندارد، ولی نگهداری و توزیع آنها بین اعضای تیم و در CI کابوس است. اپل فقط سه گواهی توزیع فعال در هر حساب میپذیرد و اگر یکی expire شود باید همهی پروفایلهای Provisioning را دوباره تولید کنید. راهنمای رسمی این چرخه در مستندات Apple Developer درباره گواهیها در دسترس است.
امضای اندروید: Keystore، Upload Key و Google Play App Signing
در اندروید، امضای کد ذاتاً سادهتر است. شما یک فایل .keystore (فرمت JKS یا مدرنتر PKCS12) با یک یا چند Alias میسازید و APK/AAB خود را با آن امضا میکنید. اما از سال ۲۰۲۱ که Google Play انتشار AAB را اجباری کرد، معماری کلید کاملاً عوض شد. حالا دو کلید داریم:
ویژگی
Upload Key
App Signing Key
مالک
تیم توسعه
Google Play
محل ذخیره
Keystore محلی + Secret در CI
سرورهای امن Google
در صورت گمشدن
قابل ریست از طریق پنل Play Console
غیرقابل ریست (بازیابی از پشتیبان تنها راه است)
استفاده
امضای AAB برای آپلود
امضای APK نهایی که به کاربران میرسد
الگوریتم پیشنهادی
RSA 2048 (حداقل)
RSA 4096 یا EC P-256
برای ساخت Upload Key از دستور استاندارد keytool استفاده میکنیم:
سپس در Play Console وارد بخش Setup → App integrity → App signing شوید و گزینهی Let Google manage and protect your app signing key را انتخاب کنید. Google یک App Signing Key جدید برایتان تولید میکند و شما فقط Upload Key بالا را نگه میدارید. جزئیات بیشتر در مستندات رسمی Google Play App Signing آمده است.
مدیریت خودکار امضای iOS با Fastlane match در .NET MAUI
Fastlane match یک ابزار Ruby است که گواهیها و پروفایلهای iOS را رمزنگاری کرده و در یک مخزن Git خصوصی (یا S3/GCS) ذخیره میکند. وقتی توسعهدهنده جدیدی به تیم اضافه میشود، فقط با یک دستور fastlane match development --readonly همه چیز روی مک او نصب میشود. خلاصه اینکه پایان دوران «چه کسی گواهی روی مک اوست؟» است.
راهاندازی برای یک پروژه .NET MAUI در چهار قدم:
یک مخزن Git خصوصی مثلاً maui-signing-certs بسازید.
در ریشه پروژه یا در مسیر fastlane/ فایل Matchfile اضافه کنید.
دستور fastlane match appstore را اجرا کنید تا گواهی توزیع و پروفایل App Store ساخته و در Git ذخیره شود.
# fastlane/Matchfile
git_url("[email protected]:mobiletechlead/maui-signing-certs.git")
storage_mode("git")
type("appstore") # or "development", "adhoc"
app_identifier(["com.mobiletechlead.sampleapp"])
username("[email protected]")
# Use App Store Connect API for CI (no 2FA prompt)
api_key_path("./fastlane/AuthKey.json")
و در فایل Fastfile:
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Sync App Store signing assets"
lane :sync_signing do
setup_ci if is_ci
match(
type: "appstore",
readonly: is_ci, # never create new certs from CI
keychain_name: "fastlane_tmp_keychain",
keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"]
)
end
end
روی مک توسعهدهنده اجرای fastlane sync_signing کافی است. در CI باید متغیر MATCH_PASSWORD (کلمه عبور رمزگشایی مخزن)، MATCH_KEYCHAIN_PASSWORD و فایل AuthKey.json از طریق Secrets تزریق شود.
پیکربندی امضا در فایل csproj پروژه .NET MAUI
MAUI امضا را از طریق پراپرتیهای MSBuild کنترل میکند. من همیشه یک PropertyGroup شرطی میسازم که فقط برای Configuration=Release فعال شود تا امضای اصلی سهواً در Debug اجرا نشود.
مقدار CodesignProvision باید دقیقاً نام پروفایل باشد نه UUID آن. Fastlane match پروفایلها را با الگوی match AppStore <bundle-id> نامگذاری میکند.
پیشوند env: در AndroidSigningKeyPass باعث میشود MSBuild رمز عبور را از متغیر محیطی بخواند و در لاگها چاپ نکند.
اگر <ArchiveOnBuild>true</ArchiveOnBuild> ست نشود، خروجی iOS بهجای .ipa یک پوشهی .app امضانشده خواهد بود. (این یکی را حداقل سه بار در تیمهای مختلف دیدهام.)
برای درک بهتر ساختار csproj و پیکربندیهای چندپلتفرمی میتوانید به راهنمای ما دربارهی مهاجرت Xamarin.Forms به .NET MAUI رجوع کنید که تفاوتهای ساختار پروژه را توضیح میدهد.
یکپارچهسازی امضا در CI/CD با GitHub Actions
در CI هدف این است که هیچ گواهی، Keystore یا رمز عبوری در مخزن یا لاگها ظاهر نشود. من از الگوی زیر در GitHub Actions استفاده میکنم که هم iOS و هم اندروید را پوشش میدهد.
تنظیم Secrets در GitHub
در تنظیمات مخزن این Secretها را اضافه کنید:
MATCH_PASSWORD: کلمه عبور رمزنگاری مخزن match
MATCH_KEYCHAIN_PASSWORD: یک رشته تصادفی که در ابتدای هر job برای Keychain موقت ساخته میشود
در چند سال گذشته با دهها خطای امضا در پروژههای MAUI و Xamarin برخورد کردهام. رایجترینها و راهحلشان:
خطای «No installed provisioning profiles match»
در ۹۰٪ موارد یعنی Bundle ID پروژه با App ID در Provisioning همخوانی ندارد یا پروفایل expire شده. با fastlane match nuke appstore و بازتولید حل میشود. اگر روی CI هستید مطمئن شوید Keychain موقت ساخته شده و باز است (تابع setup_ci در Fastlane این کار را میکند).
خطای «Keystore was tampered with»
معمولاً یعنی رمز عبور Store یا Key اشتباه است، یا فایل Keystore هنگام Base64/decode خراب شده. برای اطمینان روی CI با keytool -list -v -keystore $HOME/upload.keystore Keystore را قبل از build تست کنید. من همیشه این را قبل از هر job امضا اجرا میکنم؛ هزینهاش هیچ است و نیم ساعت دیباگ ذخیره میکند.
خطای «ITMS-90189 Redundant Binary Upload»
یعنی نسخهی CFBundleShortVersionString و CFBundleVersion با نسخهی موجود در App Store Connect یکی است. در MAUI با پراپرتیهای <ApplicationDisplayVersion> و <ApplicationVersion> مدیریت میشود. در CI بهتر است شمارهی Build را از github.run_number تزریق کنید.
خطای «Signature not aligned» در آپلود Play
معمولاً نتیجهی استفاده از v1 Signing تنها است. مطمئن شوید <AndroidUseAapt2>true</AndroidUseAapt2> فعال است و <AndroidSigningV2> و <AndroidSigningV3> بهطور پیشفرض روشن هستند.
چکلیست امنیتی امضای کد برای تیمهای تولیدی
این چکلیست را در روز اول هر پروژهی MAUI اجرا کنید تا در روز انتشار آرام بمانید:
مخزن Git خصوصی برای Fastlane match ساخته شود و دسترسی فقط برای Release Managers باشد.
رمز MATCH_PASSWORD در یک Password Manager (1Password، Bitwarden) با دسترسی گروهی نگهداری شود، نه در Slack.
App Store Connect API Key از نوع App Manager (نه Admin) ساخته شود و به کاربر مجزای ci-release متصل باشد.
Upload Key اندروید در همان مخزن رمزنگاریشده یا در یک Secret Manager (AWS Secrets Manager، Azure Key Vault) نگهداری شود.
هر ۹۰ روز یک بار Rotate کردن MATCH_PASSWORD و ANDROID_KEY_PASSWORD در تقویم تیم ثبت شود.
Backup رمزنگاریشدهی Keystore اندروید در دو محل جغرافیایی جداگانه نگهداری شود (S3 Cross-Region یا Google Cloud Storage با Object Versioning).
Runbook «چه کنیم اگر گواهی expire شود» در Wiki تیم نوشته شود.
پس از هر انتشار موفق، لاگهای امضا از CI حذف شوند تا مسیرها و نام Alias افشا نشوند.
آیا برای امضای اپ iOS در .NET MAUI حتماً باید Mac داشته باشم؟
بله. زنجیرهی codesign، Security.framework و xcrun فقط روی macOS در دسترس است. اما نیازی نیست همهی توسعهدهندهها Mac داشته باشند؛ میتوانید از یک macOS Runner در CI (مثلاً macos-15 در GitHub Actions یا Mac mini شخصی با self-hosted runner) استفاده کنید و توسعهدهندهی ویندوز فقط تغییرات را push کند.
تفاوت Upload Key و App Signing Key در Google Play چیست؟
Upload Key کلیدی است که شما با آن AAB را برای آپلود امضا میکنید و در Keystore خودتان نگه میدارید. App Signing Key کلید نهایی است که Google Play با آن APKهای تحویلی به کاربران را دوباره امضا میکند و در سرورهای امن Google ذخیره میشود. اگر Upload Key گم شود میتوانید آن را از پنل Play Console ریست کنید، اما App Signing Key غیرقابل بازیابی است.
اگر Keystore اندروید را از دست بدهم چه اتفاقی میافتد؟
اگر Play App Signing را فعال کردهاید، فقط Upload Key را از دست دادهاید و میتوانید از طریق App integrity → Request upload key reset در Play Console یک کلید جدید ثبت کنید. اگر Play App Signing فعال نبوده و اپ قدیمی است، دیگر نمیتوانید بهروزرسانی منتشر کنید و باید اپ را با Bundle ID جدید بازنشر کنید که یعنی از دست دادن همهی نصبها و رتبهها.
وقتی گواهی توزیع iOS منقضی میشود چه باید کرد؟
گواهی منقضیشده روی اپهای منتشرشده در App Store تأثیری ندارد و کاربران فعلی بدون مشکل ادامه میدهند. اما برای انتشار نسخهی جدید باید گواهی تازه بسازید و همهی Provisioning Profileها را دوباره تولید کنید. با Fastlane match این کار با fastlane match nuke distribution و سپس fastlane match appstore در چند دقیقه تمام میشود.
آیا Fastlane match با App Store Connect API Key کار میکند یا هنوز به Apple ID و 2FA نیاز دارد؟
از Fastlane 2.196 به بعد، match بهطور کامل با App Store Connect API Key کار میکند و دیگر نیازی به Apple ID، رمز اپلیکیشن یا Session Token نیست. کافی است در Matchfile مسیر api_key_path را به فایل JSON حاوی Key ID، Issuer ID و محتوای .p8 بدهید. این روش برای CI الزامی است چون 2FA در محیط headless کار نمیکند.
آیا میتوانم بدون Fastlane match، گواهیها را بهصورت دستی در GitHub Actions تزریق کنم؟
بله. میتوانید فایل .p12 گواهی و .mobileprovision پروفایل را Base64 کرده و در Secrets ذخیره کنید، سپس در Workflow با security import و cp در Keychain و مسیر ~/Library/MobileDevice/Provisioning Profiles/ قرار دهید. ولی با اضافهشدن هر توسعهدهنده یا هر بار Rotate گواهی، این روش خستهکننده میشود و match نگهداریاش را حل میکند.
App Center در مارس ۲۰۲۵ بازنشسته شد. در این راهنما، مهاجرت گزارشگیری کرش .NET MAUI به Sentry را با نصب Sentry.Maui، آپلود dSYM، یکپارچهسازی GitHub Actions و مقایسه با Firebase Crashlytics قدمبهقدم میبینید.
راهنمای عملی CI/CD برای .NET MAUI با GitHub Actions در ۲۰۲۶: ساخت Android و iOS، امضای دیجیتال با fastlane match، انتشار به TestFlight و Google Play، نسخهگذاری خودکار و عیبیابی خطاهای رایج.