Code Signing til .NET MAUI: iOS Provisioning Profiles og Android Keystores (2026)

Komplet guide til code signing af .NET MAUI apps på iOS og Android: certifikater, provisioning profiles, keystores, fastlane match og CI/CD.

.NET MAUI Code Signing Guide (2026)

Opdateret: 21. juni 2026

Code signing til .NET MAUI kræver to separate processer. På iOS underskriver du med et Apple Distribution-certifikat plus en provisioning profile, der matcher dit Bundle ID. På Android underskriver du APK'en eller AAB'en med et keystore, hvor Play App Signing tager over for upload-nøglen. Ærligt talt: de fleste teams springer den del af opsætningen over, hvor man dokumenterer hvor nøglerne ligger, og så står man tre måneder senere uden adgang til at udgive en hotfix, fordi den ene udvikler der havde keystoren på sin Mac er stoppet. Jeg har set det ske to gange (begge gange smerteligt), og denne guide er den checkliste, jeg ville ønske jeg havde haft, da jeg første gang skulle automatisere signering af en MAUI-app.

  • iOS-signering kræver et Distribution-certifikat (.p12), en matching provisioning profile (.mobileprovision) og korrekt opsætning af CodesignKey og CodesignProvision i din .csproj.
  • Android-signering håndteres via et keystore (.jks/.keystore). For Play Store skal du desuden enrolle i Play App Signing, hvor Google håndterer release-nøglen.
  • Brug aldrig det automatisk genererede debug-keystore til release. Du mister evnen til at opdatere appen, hvis det forsvinder.
  • Fastlane match er det bedste værktøj til at dele iOS-certifikater på tværs af et team via et krypteret Git-repo.
  • I CI bør hemmeligheder injiceres som base64-kodede secrets. Aldrig som filer i repoet eller miljøvariabler i plain text.
  • Apple roterer enterprise-certifikater hvert år. Sæt en kalenderpåmindelse 30 dage før udløb.

Hvad er code signing på iOS?

Code signing på iOS er Apples mekanisme til at garantere, at en binær fil er bygget af en identificeret udvikler og ikke er blevet ændret efter signering. Hver iOS-build kræver tre artefakter: et signing certificate (typisk et Apple Distribution-certifikat), en provisioning profile, og et App ID registreret i din udvikler-konto. Certifikatet beviser hvem du er. Provisioning profilen beviser hvad appen må (entitlements som Push, In-App Purchase, iCloud) og hvor den må køre (App Store, ad hoc test-enheder, eller TestFlight).

For .NET MAUI er processen identisk med native iOS-udvikling, men MSBuild-egenskaberne hedder noget andet end i Xcode. Hvor en Xcode-projektfil håndterer signering via "Signing & Capabilities"-fanen, peger du i .NET MAUI direkte på certifikatnavn og profile-UUID i .csproj-filen. Det betyder også, at signing-fejl typisk dukker op som obskure MSBuild-fejl, og ikke som de hjælpsomme dialoger Xcode plejer at vise. Det er en af grundene til, at de fleste hold ender med at automatisere signeringen tidligt, så fejlene bliver reproducerbare. Hvis du kommer fra Xamarin-verdenen, kan du springe direkte til migreringsguiden fra Xamarin.Forms til .NET MAUI, hvor jeg gennemgår de små forskelle i MSBuild-egenskaber.

Sådan opretter du iOS-certifikater og provisioning profiles

De fleste hold springer denne del over, og så bruger de fire timer i Apple Developer Portal når de skal udgive deres første release. Her er den minimale checkliste, udført i denne rækkefølge:

  1. Generer en Certificate Signing Request (CSR) i Keychain Access på din Mac (Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority). Gem som CertificateSigningRequest.certSigningRequest.
  2. Log ind på Apple Developer Portal og opret et "Apple Distribution"-certifikat. Upload din CSR. Download den resulterende .cer-fil og dobbeltklik for at importere til Keychain.
  3. Eksporter som .p12: Højreklik på certifikatet i Keychain, vælg "Export", giv det en stærk adgangskode og gem som ios_distribution.p12. Det er denne fil du skal bruge i CI.
  4. Registrér dit App ID under Identifiers. Brug reverse-DNS notation (f.eks. com.minvirksomhed.minapp). Aktivér de capabilities du har brug for (Push Notifications, Sign in with Apple, osv.).
  5. Opret en provisioning profile af typen "App Store" (til release) og en "Ad Hoc" (til test på fysiske enheder). Vælg det rette App ID og certifikat. Download .mobileprovision-filerne.

Konfigurer .NET MAUI csproj til iOS-signering

Når certifikatet og profilen er importeret lokalt, peger du fra .csproj-filen på dem. I .NET MAUI sker det typisk via en PropertyGroup med en betingelse, der kun aktiveres for iOS Release-builds. Her er den minimale opsætning:

<PropertyGroup Condition="$(TargetFramework.Contains('-ios')) and '$(Configuration)' == 'Release'">
  <CodesignKey>Apple Distribution: Min Virksomhed ApS (ABCDE12345)</CodesignKey>
  <CodesignProvision>MinApp AppStore Profile</CodesignProvision>
  <CodesignEntitlements>Platforms/iOS/Entitlements.plist</CodesignEntitlements>
  <ArchiveOnBuild>true</ArchiveOnBuild>
  <RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
</PropertyGroup>

CodesignKey er det fulde navn på certifikatet, præcis som det står i Keychain (inklusive Team ID i parentes). CodesignProvision er det "Provisioning Profile Name" du gav profilen i Developer Portal, og altså ikke filnavnet og ikke UUID'en. Hvis du bygger til simulator, springes signering automatisk over. For at bygge en .ipa kører du:

dotnet publish -f net9.0-ios -c Release \
  -p:ArchiveOnBuild=true \
  -p:RuntimeIdentifier=ios-arm64

Resultatet ender i bin/Release/net9.0-ios/ios-arm64/publish/ som en .ipa-fil klar til upload via Transporter eller xcrun altool. Hvis du allerede har automatiseret resten af din pipeline, kan du integrere det med CI/CD-flowet for .NET MAUI med GitHub Actions.

Sådan opretter du et Android keystore

Et Android keystore er en krypteret container med en eller flere privatnøgler, som bruges til at signere din APK eller AAB. I modsætning til iOS, hvor Apple centralt udsteder certifikater, genererer du selv keystoren, og du er selv ansvarlig for at opbevare den sikkert. Mister du keystoren og ikke har enrolled i Play App Signing, mister du muligheden for nogensinde at opdatere appen i Play Store. Det er ikke en overdrivelse: Google kan ikke hjælpe dig.

Opret et release-keystore med keytool (følger med JDK):

keytool -genkeypair -v \
  -keystore minapp-release.keystore \
  -alias minapp \
  -keyalg RSA -keysize 2048 \
  -validity 10000 \
  -storepass DIN_STAERKE_KODE \
  -keypass DIN_STAERKE_KODE

Brug en gyldighed på mindst 25 år (10000 dage er omkring 27 år), for Google Play kræver at signeringscertifikatet er gyldigt indtil mindst 22. oktober 2033. For at signere fra .NET MAUI tilføjer du en PropertyGroup til .csproj:

<PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
  <AndroidKeyStore>true</AndroidKeyStore>
  <AndroidSigningKeyStore>minapp-release.keystore</AndroidSigningKeyStore>
  <AndroidSigningKeyAlias>minapp</AndroidSigningKeyAlias>
  <AndroidSigningKeyPass>$(ANDROID_KEY_PASSWORD)</AndroidSigningKeyPass>
  <AndroidSigningStorePass>$(ANDROID_STORE_PASSWORD)</AndroidSigningStorePass>
  <AndroidPackageFormat>aab</AndroidPackageFormat>
</PropertyGroup>

Læg mærke til AndroidPackageFormat=aab. Google Play kræver Android App Bundle for nye apps siden august 2021, og for eksisterende apps har Play Console gradvist tvunget alle over. Adgangskoderne læser jeg fra miljøvariabler ($-syntaks) for at undgå at have dem i .csproj. Lokalt sætter du dem i en .env-fil der er gitignored. I CI kommer de fra secrets. Hvis du har brug for at gemme legitimationsoplysninger sikkert i appen selv (ikke i build-processen), så læs også guiden til SecureStorage og biometri i .NET MAUI.

Play App Signing forklaret

Siden 2021 har Google Play krævet at alle nye apps bruger Play App Signing. Det er en model, hvor du uploader din AAB signeret med en upload-nøgle, og Googles servere re-signerer den med den faktiske app-signeringsnøgle, som de opbevarer for dig. Det betyder to ting i praksis:

  • Du har to keystores: én du selv opbevarer (upload-nøglen) og én Google opbevarer (app-signeringsnøglen, som faktisk verificerer appen på brugernes enheder).
  • Hvis du mister din upload-nøgle, kan du anmode Google om at nulstille den. Hvis du mister app-signeringsnøglen, så slap af, det gør du ikke, Google har den.
AspektKlassisk signingPlay App Signing
Hvem opbevarer release-nøglen?Du selvGoogle
Hvad sker hvis nøglen mistes?App kan aldrig opdateresUpload-nøgle kan resettes
Mulig nøglerotation?NejJa, siden 2021 for nye apps
Optimeret APK pr. enhed?Nej (én APK til alle)Ja (split APKs)
Krævet for nye apps?NejJa, siden august 2021

I praksis betyder det, at dit team kun behøver at passe på upload-nøglen. Jeg anbefaler at lægge den i en password manager med teamadgang (1Password, Bitwarden) sammen med adgangskoderne, så du har en single source of truth.

Centraliser iOS-nøgler med fastlane match

Fastlane match er de facto-løsningen til at synkronisere iOS-certifikater og provisioning profiles på tværs af et udviklerteam. Den krypterer dine .p12-filer og .mobileprovision-filer og opbevarer dem i et privat Git-repo (eller S3-bucket). Nye udviklere kan hente det fulde signing-setup med én kommando, og CI gør det samme.

Initial opsætning fra en Mac der allerede har de rigtige certifikater:

# Installer fastlane
brew install fastlane

# Initialisér match i projektets rod
cd /sti/til/maui-projekt
fastlane match init

# Vælg "git" som storage, og angiv URL til et privat repo
# F.eks. [email protected]:minvirksomhed/ios-certs.git

# Generér og upload appstore-profil
fastlane match appstore --app_identifier com.minvirksomhed.minapp

# Generér og upload development-profil
fastlane match development --app_identifier com.minvirksomhed.minapp

Match krypterer alt med en passphrase du angiver, og lagrer kun krypterede filer i Git. På en ny maskine (eller i CI) henter du dem med:

fastlane match appstore --readonly --app_identifier com.minvirksomhed.minapp

Signering i CI/CD pipelines

Signering i CI er hvor de fleste hold løber ind i deres første rigtige produktionsproblem. Reglen er enkel: hemmeligheder må aldrig røre disk uden at være krypteret, og de skal kunne roteres uden at ændre i pipeline-koden. Mit standardflow til GitHub Actions ser sådan ud:

  1. Base64-kod alle binære secrets lokalt: base64 -i ios_distribution.p12 | pbcopy og base64 -i minapp-release.keystore | pbcopy.
  2. Tilføj dem som GitHub Secrets: IOS_P12_BASE64, IOS_P12_PASSWORD, ANDROID_KEYSTORE_BASE64, ANDROID_STORE_PASSWORD, ANDROID_KEY_PASSWORD, MATCH_PASSWORD.
  3. Decode i pipelinen, brug, og slet: hold filen i live så kort tid som muligt.

Eksempel på iOS-signering i GitHub Actions:

- name: Import Apple Distribution Certificate
  env:
    P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
    P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
    KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
  run: |
    echo "$P12_BASE64" | base64 --decode > cert.p12
    security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    security default-keychain -s build.keychain
    security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    security import cert.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
    security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
    rm cert.p12

- name: Install Provisioning Profile
  env:
    PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
  run: |
    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
    echo "$PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision

For Android er det enklere, fordi du ikke behøver et keychain:

- name: Decode Android Keystore
  env:
    KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
  run: echo "$KEYSTORE_BASE64" | base64 --decode > minapp-release.keystore

- name: Build Signed AAB
  env:
    ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
    ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
  run: |
    dotnet publish -f net9.0-android -c Release \
      -p:AndroidPackageFormat=aab \
      -p:AndroidKeyStore=true \
      -p:AndroidSigningKeyStore=minapp-release.keystore \
      -p:AndroidSigningKeyAlias=minapp \
      -p:AndroidSigningKeyPass=$ANDROID_KEY_PASSWORD \
      -p:AndroidSigningStorePass=$ANDROID_STORE_PASSWORD

Almindelige signing-fejl og deres løsninger

Her er de fejl jeg ser igen og igen, på workshops, i Slack-tråde, i opgaver jeg konsulterer på. Hver enkelt har en specifik årsag og en kort løsning.

"No valid iOS code signing keys found in keychain"

Certifikatet er enten ikke importeret eller den private nøgle mangler. Tjek med security find-identity -v -p codesigning. Hvis der står "0 valid identities found", er din .p12 ikke importeret korrekt. Typisk fordi adgangskoden var forkert, eller fordi du importerede en .cer i stedet for en .p12 (en .cer indeholder kun den offentlige nøgle).

"Provisioning profile doesn't include signing certificate"

Profilen blev oprettet med et andet certifikat end det du underskriver med. Gå ind i Developer Portal, redigér profilen, vælg det rette certifikat, download den nye .mobileprovision og udskift den lokalt. Pas på: gamle profiler bliver liggende i ~/Library/MobileDevice/Provisioning Profiles/, og MAUI tager bare den første der matcher. Slet de gamle.

"Keystore was tampered with, or password was incorrect"

Det er som regel fordi base64-decodingen i CI har tilføjet en newline. Brug base64 -d (Linux) eller base64 --decode (macOS), og verificér filstørrelsen før og efter med wc -c. Jeg ramte præcis den her bug, da jeg flyttede en pipeline fra macOS til Ubuntu: forskellige base64-værktøjer wrapper output ved 76 tegn, så sørg for at base64-encode uden linjebrud (base64 -w 0 på Linux).

"Failed to read key from keystore"

Aliaset stemmer ikke. List aliaserne med keytool -list -keystore minapp-release.keystore. Husk at alias er case-sensitive.

"App not installed" på Android-enhed

Hvis du har installeret en debug-build og forsøger at installere en release-build oven på (eller omvendt), fejler det fordi signaturen er forskellig. Afinstallér først, eller brug forskellige ApplicationId til debug og release.

Ofte stillede spørgsmål

Hvordan signerer jeg en .NET MAUI app til iOS uden en Mac?

Du kan ikke. Apples codesign-værktøj kører kun på macOS, og du har desuden brug for Xcodes command line tools. Brug enten en GitHub Actions macos-latest-runner, en cloud-Mac (MacStadium, MacinCloud) eller en delt build-maskine. Det er en absolut blocker, også i 2026.

Hvad er forskellen på en upload-nøgle og en app-signeringsnøgle?

Upload-nøglen er den nøgle du selv opbevarer og bruger til at signere AAB'er der uploades til Play Console. Google re-signerer derefter med app-signeringsnøglen (som de opbevarer) før appen distribueres til brugere. Det er denne sidstnævnte nøgle der verificerer appen på enheden.

Kan jeg bruge det samme keystore til flere apps?

Teknisk ja, men det anbefales ikke. Hvis keystoren bliver kompromitteret, kan en angriber signere falske opdateringer til alle dine apps. Brug ét keystore (eller ét alias inden i et keystore) pr. app for at minimere blast radius.

Hvor lang tid er en iOS provisioning profile gyldig?

App Store distribution profiles er gyldige i 1 år. Ad hoc og development profiles er også 1 år. Det underliggende Apple Distribution-certifikat udløber også efter 1 år. Sæt en kalenderpåmindelse 30 dage før udløb, for udløb sker stille og uden notifikation.

Skal jeg bruge fastlane match eller manuelt håndtere certifikater?

Hvis I er flere end én udvikler, brug match. Den eliminerer "det virker på min maskine"-problemet for signing, og rotation af certifikater bliver triviel. Til solo-projekter er manuel håndtering fint, hvis du har en sikker backup-strategi for .p12-filen.

Hvad sker der hvis mit Android keystore bliver stjålet?

En angriber kan signere malware der ligner en opdatering til din app. Du kan ikke revokere keystoren, og du må derfor publicere appen under et nyt package name og bede brugerne om at migrere. Med Play App Signing kan du dog rotere upload-nøglen, så skaden begrænses til den periode mellem tyveri og opdagelse.

Sofia Rodriguez
Om Forfatteren Sofia Rodriguez

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