Firma e Distribuzione .NET MAUI 10: Keystore Android e Provisioning iOS (2026)

Playbook DevOps completo per firmare e distribuire un'app .NET MAUI 10 nel 2026: keystore Android, provisioning iOS, workflow GitHub Actions pronto all'uso e gotcha su Play Store e App Store.

Aggiornato: 20 luglio 2026

Firmare un'app .NET MAUI 10 significa produrre due artefatti pronti per gli store: un .aab Android firmato con il tuo keystore JKS e un .ipa iOS firmato con un provisioning profile di distribuzione e un certificato Apple. Nel 2026, con l'obbligo di targetSdkVersion=35 su Google Play e del privacy manifest su App Store, la firma non è più un dettaglio: è il passaggio dove più release si bloccano. Onestamente, questa è la parte del ciclo di rilascio che mi ha fatto perdere più notti negli ultimi due anni, e questa guida è il playbook DevOps che uso in produzione (checklist, comandi dotnet publish e pipeline GitHub Actions incluse).

  • Android richiede un keystore JKS generato con keytool e conservato fuori dal repository. La prima release su Google Play va caricata manualmente.
  • iOS richiede un certificato di distribuzione .p12, un provisioning profile App Store e, dal 2024, un privacy manifest completo.
  • Il comando corretto è dotnet publish -f net10.0-android -c Release con le proprietà di firma passate via CLI, non via .csproj.
  • In CI/CD, keystore e .p12 vanno passati come segreti base64: mai committati, mai in file di configurazione.
  • Fastlane Match resta l'opzione più stabile per gestire i profili iOS di più sviluppatori senza scaricarli a mano.
  • Google Play impone API 35 come minimo per nuove app e aggiornamenti dal 31 agosto 2025; Apple richiede lo SDK di Xcode 16 per le nuove submission dal 2025.

Prerequisiti per firmare un'app .NET MAUI 10

La maggior parte dei team salta questa parte, e poi passa due giorni a debuggare un profilo di provisioning invece di scrivere codice. Ci sono passato di persona più di una volta. Prima di toccare dotnet publish, verifica di avere in mano ogni singolo elemento della checklist: senza uno solo di questi, la pipeline si romperà nel modo più costoso possibile, cioè al momento del caricamento sullo store.

Checklist Android:

  • JDK 17 installato e JAVA_HOME configurato. .NET MAUI 10 non è compatibile con JDK 21 nelle pipeline Android.
  • Android SDK con Platform 35 e Build-Tools 35.0.0.
  • Un account Google Play Developer attivo (una tantum, 25 $) e la prima release già creata come bozza sulla Play Console.
  • keytool disponibile in PATH (arriva con il JDK).

Checklist iOS:

  • macOS con Xcode 16.2 o superiore, richiesto per le submission Apple dal 2025.
  • Apple Developer Program attivo (99 $/anno).
  • Un App ID e un bundle identifier registrati nel portale sviluppatori (es. com.mia-azienda.myapp).
  • Un certificato di distribuzione (Apple Distribution) esportato come .p12 con password.
  • Un provisioning profile di distribuzione App Store scaricato in ~/Library/MobileDevice/Provisioning Profiles/.
  • File PrivacyInfo.xcprivacy incluso nel bundle iOS del progetto.

Come firmare un'app .NET MAUI per Android

Su Android il flusso è lineare: crei un keystore una volta sola, lo backuppi in un password manager, e da lì in avanti lo passi a ogni build come coppia di variabili. Il keystore Android è un file binario che contiene i certificati e le chiavi private che identificano univocamente la tua app su Google Play, e perderlo significa non poter più pubblicare aggiornamenti con lo stesso pacchetto.

Generare il keystore JKS

Usa il tool keytool del JDK. Passa alias e password come parametri per rendere il comando riproducibile in CI:

keytool -genkeypair \
  -v \
  -keystore myapp-release.keystore \
  -alias myapp \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -storetype JKS \
  -storepass "PASSWORD_MOLTO_LUNGA" \
  -keypass "PASSWORD_MOLTO_LUNGA" \
  -dname "CN=Mia Azienda, OU=Mobile, O=MiaAzienda, L=Milano, ST=MI, C=IT"

Il flag -validity 10000 è il valore raccomandato da Google (circa 27 anni): un keystore che scade prima della durata di vita dell'app è un incidente in attesa di accadere. Il file .keystore risultante non deve mai finire in Git: aggiungilo subito a .gitignore e archivialo in un vault (1Password, Bitwarden, GitHub Secrets, Azure Key Vault).

Firmare con dotnet publish

La firma avviene passando le proprietà MSBuild direttamente al comando dotnet publish. Non mettere password nel .csproj: sono variabili d'ambiente.

dotnet publish MyApp/MyApp.csproj \
  -f net10.0-android \
  -c Release \
  -p:AndroidPackageFormat=aab \
  -p:AndroidKeyStore=true \
  -p:AndroidSigningKeyStore=$KEYSTORE_PATH \
  -p:AndroidSigningKeyAlias=$KEYSTORE_ALIAS \
  -p:AndroidSigningKeyPass=$KEYSTORE_KEY_PASS \
  -p:AndroidSigningStorePass=$KEYSTORE_STORE_PASS \
  -p:ApplicationVersion=$BUILD_NUMBER \
  -p:ApplicationDisplayVersion=1.2.0

Nota tre dettagli non ovvi. AndroidPackageFormat=aab produce l'App Bundle richiesto da Google Play (l'APK non è più accettato per nuove app dal 2021). ApplicationVersion è il versionCode intero e deve incrementare a ogni release: un pattern affidabile è passare il numero di run della pipeline. Infine, ApplicationDisplayVersion è la versionName mostrata all'utente.

Come configurare il provisioning iOS in .NET MAUI 10

Il provisioning iOS è la parte che rompe più build su .NET MAUI. Il modello di firma di Apple lega tre cose (bundle identifier, certificato di distribuzione e provisioning profile) e se una sola non combacia, xcrun rifiuta di firmare l'IPA senza spiegazioni utili. Ci sono due strade: automatica (buona per sviluppo, fragile in CI) e manuale (obbligata per pipeline riproducibili).

Provisioning automatico: solo per sviluppo locale

Nelle proprietà del progetto MAUI, tab iOS > Bundle Signing, seleziona Automatic Provisioning e scegli il team. Visual Studio crea signing identity, App ID e profile in automatico. Questo funziona bene solo su una workstation collegata al tuo Apple ID. In CI non esiste il concetto di Visual Studio, quindi il provisioning automatico non è una strada percorribile per la pipeline.

Provisioning manuale: obbligato per CI/CD

Per le pipeline devi esportare a mano il certificato e scaricare il provisioning profile dal portale sviluppatori Apple. Il certificato di distribuzione va esportato dalla Keychain macOS come .p12 protetto da password. Il profile è un file .mobileprovision.

Nel .csproj del progetto MAUI (o via CLI in CI) imposta il codice di firma esplicito:

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)' == 'Release|net10.0-ios'">
  <RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
  <CodesignKey>Apple Distribution: Mia Azienda (TEAMID123)</CodesignKey>
  <CodesignProvision>MyApp AppStore</CodesignProvision>
  <ArchiveOnBuild>true</ArchiveOnBuild>
  <TcpPort>58181</TcpPort>
</PropertyGroup>

CodesignKey è il nome visualizzato del certificato nella Keychain (non l'ID), CodesignProvision è il nome del profile mostrato nel portale. Il flag ArchiveOnBuild=true produce l'archivio Xcode necessario per esportare l'IPA firmato con xcrun.

Costruire l'IPA firmato

dotnet publish MyApp/MyApp.csproj \
  -f net10.0-ios \
  -c Release \
  -p:RuntimeIdentifier=ios-arm64 \
  -p:CodesignKey="Apple Distribution: Mia Azienda (TEAMID123)" \
  -p:CodesignProvision="MyApp AppStore" \
  -p:ArchiveOnBuild=true

L'output finisce in bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa. Verifica sempre la firma con codesign -dvvv MyApp.ipa prima di caricare: se il team ID o il profile non combaciano, Transporter lo scoprirà solo al momento dell'upload, dopo aver aspettato 15 minuti di validazione. Ci sono passato la scorsa primavera con una release urgente, e non è un'esperienza che consiglio a fine giornata.

Automatizzare la firma con GitHub Actions

Una pipeline CI/CD ben progettata rimuove tre categorie di errori: password digitate male, versioni di build che si sovrappongono, e artefatti costruiti su macchine con SDK diversi. Ecco il workflow che uso come base per progetti .NET MAUI 10, Android su Linux e iOS su macOS 14, entrambi da singolo push su tag v*.

Preparare i segreti

Su GitHub Actions, i file binari (keystore, .p12, .mobileprovision) vanno codificati in base64 prima di essere inseriti nei repository secrets:

base64 -i myapp-release.keystore | pbcopy   # macOS
base64 -w 0 myapp-release.keystore           # Linux
base64 -i AppleDistribution.p12 | pbcopy

Configura i seguenti secrets nel repository (Settings > Secrets and variables > Actions):

  • ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD
  • IOS_P12_BASE64, IOS_P12_PASSWORD, IOS_PROVISION_BASE64
  • APPSTORE_ISSUER_ID, APPSTORE_KEY_ID, APPSTORE_PRIVATE_KEY (per l'upload via Transporter/altool)

Workflow GitHub Actions

name: release-maui
on:
  push:
    tags: [ 'v*' ]

jobs:
  android:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '17' }
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '10.0.x' }
      - name: Install MAUI workload
        run: dotnet workload install maui-android
      - name: Decode keystore
        run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
      - name: Publish AAB
        run: |
          dotnet publish src/MyApp/MyApp.csproj \
            -f net10.0-android -c Release \
            -p:AndroidPackageFormat=aab \
            -p:AndroidKeyStore=true \
            -p:AndroidSigningKeyStore=$GITHUB_WORKSPACE/keystore.jks \
            -p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }} \
            -p:AndroidSigningKeyPass=${{ secrets.ANDROID_KEY_PASSWORD }} \
            -p:AndroidSigningStorePass=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
            -p:ApplicationVersion=${{ github.run_number }}
      - uses: actions/upload-artifact@v4
        with:
          name: android-aab
          path: '**/*-Signed.aab'

  ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '10.0.x' }
      - name: Install MAUI workload
        run: dotnet workload install maui-ios
      - name: Import certificate
        env:
          P12_B64: ${{ secrets.IOS_P12_BASE64 }}
          P12_PWD: ${{ secrets.IOS_P12_PASSWORD }}
        run: |
          echo "$P12_B64" | base64 -d > cert.p12
          security create-keychain -p "" build.keychain
          security import cert.p12 -k build.keychain -P "$P12_PWD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
      - name: Install provisioning profile
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "${{ secrets.IOS_PROVISION_BASE64 }}" | base64 -d \
            > ~/Library/MobileDevice/Provisioning\ Profiles/MyApp.mobileprovision
      - name: Publish IPA
        run: |
          dotnet publish src/MyApp/MyApp.csproj \
            -f net10.0-ios -c Release \
            -p:RuntimeIdentifier=ios-arm64 \
            -p:CodesignKey="Apple Distribution: Mia Azienda (TEAMID123)" \
            -p:CodesignProvision="MyApp AppStore" \
            -p:ArchiveOnBuild=true \
            -p:ApplicationVersion=${{ github.run_number }}
      - uses: actions/upload-artifact@v4
        with:
          name: ios-ipa
          path: '**/*.ipa'

Se hai più di uno sviluppatore che genera build locali, considera Fastlane Match per centralizzare i certificati iOS in un repo Git cifrato. Match risolve il problema del "sul mio Mac funziona" con certificati e profili scaricati automaticamente da tutti. Ho scritto una guida separata sui test in .NET MAUI che può stare comodamente prima di questa pipeline, come step di validazione automatica prima della firma.

Pubblicare su Google Play e App Store nel 2026

Firmato l'artefatto, resta il caricamento sugli store, passaggio che nel 2026 ha vincoli diversi rispetto a due anni fa. Sia Google che Apple hanno alzato l'asticella sulla compliance e sul metadata: pipeline che funzionavano nel 2024 vengono rifiutate oggi senza modifiche.

Google Play Console

La prima release deve essere caricata manualmente attraverso la Google Play Console. Google usa quella prima firma come "impronta" del tuo pacchetto: tutti gli aggiornamenti successivi devono essere firmati con lo stesso keystore. Da lì in avanti, puoi automatizzare gli upload via Google Play Developer Publishing API o con action come r0adkll/upload-google-play.

Assets richiesti dallo store listing:

  • Descrizione breve (max 80 caratteri), descrizione lunga (max 4.000 caratteri)
  • Icona 512×512 PNG (senza alpha)
  • Feature graphic 1024×500
  • Almeno 2 screenshot per smartphone (1080×1920 raccomandato)
  • URL della privacy policy
  • Data Safety form compilata (obbligatoria dal 2022 ma bloccante nel 2026)

App Store Connect

Su iOS l'upload avviene con xcrun altool o con Transporter. Il primo upload su una nuova bundle ID va accompagnato dai metadati completi. Attenzione: dal 2025 la validazione include controlli automatici sul privacy manifest e sulle API considerate sensibili. Se manca la dichiarazione per un'API che usi, l'IPA viene rifiutato in fase di validazione, non in review. Il messaggio è criptico ma il fix è: aggiungi la voce corretta a PrivacyInfo.xcprivacy secondo la documentazione ufficiale Apple sui privacy manifest.

Versionamento automatico

Un pattern che funziona bene in produzione è legare ApplicationVersion al numero di run della pipeline (${{ github.run_number }}) e mantenere ApplicationDisplayVersion come SemVer nel .csproj. Questo elimina completamente il "collision" degli upload doppi, un errore comune quando due sviluppatori lanciano una release nello stesso giorno. Chi vuole spingere oltre le ottimizzazioni post-firma può leggere la guida sulle prestazioni .NET MAUI: startup time e dimensione dell'IPA hanno un impatto diretto sulla percentuale di installazioni completate.

Gotcha comuni e come evitarli

Ecco la lista dei problemi che ho visto rompere più pipeline di firma .NET MAUI negli ultimi 12 mesi, con la soluzione a fianco. Tienila come riferimento offline. Nove volte su dieci l'errore che vedrai è già qui dentro.

SintomoCausaSoluzione
"Could not find matching provisioning profile" Xcode salva i profile con nomi GUID casuali; MSBuild non riesce a matchare per nome Passa CodesignProvision con il nome esatto dal portale sviluppatori, non il GUID
Upload Google Play rifiutato: "targetSdkVersion troppo basso" Nuova app o aggiornamento con target < API 35 Imposta <SupportedOSPlatformVersion> e <TargetSdkVersion>35</TargetSdkVersion> nel csproj
Errore "keystore was tampered with" in CI Password errata o keystore corrotto durante il base64 decode Usa base64 -w 0 su Linux; verifica con keytool -list -keystore ... in un job separato
App Store rifiuta la validazione: privacy manifest mancante Il file PrivacyInfo.xcprivacy non è incluso nel bundle iOS Aggiungilo con <BundleResource Include="Platforms/iOS/PrivacyInfo.xcprivacy" />
versionCode duplicato al secondo upload ApplicationVersion hard-coded a 1 nel csproj Sovrascrivi in CI con -p:ApplicationVersion=${{ github.run_number }}
"No installed provisioning profiles match" su macOS runner Il profile non è nella cartella corretta o non è stato importato Copialo esplicitamente in ~/Library/MobileDevice/Provisioning Profiles/ prima del publish

Domande frequenti

Come firmo un'app .NET MAUI 10 per Android da riga di comando?

Usa dotnet publish -f net10.0-android -c Release -p:AndroidKeyStore=true passando keystore, alias e password come parametri MSBuild. Il file .aab firmato apparirà in bin/Release/net10.0-android/publish/. Non inserire mai le password nel csproj: usa variabili d'ambiente o secrets della pipeline.

Qual è la differenza tra provisioning automatico e manuale su iOS?

Il provisioning automatico fa gestire a Visual Studio o Xcode i certificati e i profile, comodo per lo sviluppo locale, impossibile in CI dove non esiste una UI Visual Studio. Il provisioning manuale richiede di generare certificati (.p12) e profili (.mobileprovision) a mano dal portale Apple e passarli esplicitamente via CLI: è l'unica strada per pipeline riproducibili.

Perché la mia app .NET MAUI viene rifiutata da Google Play nel 2026?

Le tre cause più comuni sono: targetSdkVersion inferiore a 35 (obbligatorio dal 31 agosto 2025), Data Safety form non compilata, e caricamento di un APK invece di un AAB. Verifica il .csproj e assicurati di usare -p:AndroidPackageFormat=aab.

Posso automatizzare l'upload su App Store senza Fastlane?

Sì, con xcrun altool --upload-app autenticato tramite una App Store Connect API Key (issuer ID + key ID + chiave privata .p8). Fastlane semplifica solo la parte di gestione certificati con Match; l'upload puro è supportato nativamente dagli strumenti Apple e non richiede Ruby.

Cosa succede se perdo il keystore Android della mia app?

Se hai attivato Google Play App Signing (raccomandato per tutte le nuove app), Google conserva la chiave di upload e puoi richiedere un reset dell'upload key contattando il supporto. Se invece hai firmato direttamente con la chiave di distribuzione senza App Signing, l'app è persa: nessun aggiornamento sarà più possibile e dovrai pubblicare un nuovo package con bundle ID diverso, perdendo tutte le installazioni esistenti.

Sofia Rodriguez
Sull'Autore Sofia Rodriguez

Mobile DevOps engineer focused on the unglamorous stuff: build pipelines, signing, store releases, and the tooling that keeps teams shipping.