{"id":594,"date":"2024-03-29T23:59:16","date_gmt":"2024-03-29T22:59:16","guid":{"rendered":"https:\/\/sparrow365.de\/?p=594"},"modified":"2025-02-12T18:40:08","modified_gmt":"2025-02-12T17:40:08","slug":"durchdrehen-beim-rotieren-von-entra-id-app-zertifikaten-mit-powershell","status":"publish","type":"post","link":"https:\/\/sparrow365.de\/index.php\/2024\/03\/29\/durchdrehen-beim-rotieren-von-entra-id-app-zertifikaten-mit-powershell\/","title":{"rendered":"Durchdrehen beim Rotieren von Entra ID App Zertifikaten mit PowerShell"},"content":{"rendered":"<p>W\u00e4hrend ich an meinem <a href=\"https:\/\/sparrow365.de\/index.php\/2024\/03\/04\/du-brauchst-wahrscheinlich-kein-application-readwrite-all\/\">Appell gegen Application.ReadWrite.All<\/a> arbeitete, stie\u00df ich auf eine potenzielle M\u00f6glichkeit, <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/api\/application-addkey?view=graph-rest-1.0&amp;tabs=powershell\">ohne Graph API-Rechte das Authentifizierungszertifikat einer Applikation zu rotieren<\/a>. Nach einigem Herum-Probieren hatte ich es nicht zu meiner Zufriedenheit in PowerShell umgesetzt, entsprechend habe ich es vorerst beiseite gelegt, um den Artikel abszuschlie\u00dfen. Das Thema lie\u00df mich aber nicht los, da diese Methode einige Vorteile h\u00e4tte:<\/p>\n<ul>\n<li>Applikationsverantwortliche h\u00e4tten eine schnelle Self-Service-Option, das Zertifikat einer Applikation zu \u00e4ndern<\/li>\n<li>Automatismen k\u00f6nnten kurz vor Ablauf eines Zertifikats ohne Benutzerinteraktion einen Austausch vornehmen<\/li>\n<li>Ich h\u00e4tte einen weiteren Grund, weniger zus\u00e4tzliche Berechtigungen zu vergeben bzw. zu nutzen<\/li>\n<\/ul>\n<blockquote>\n<p>&quot;Authentifizierungszertifikat&quot; hei\u00dft, dass die Credentials bzw. Anmeldeinformationen ausgetauscht werden &#8211; nicht SAML Signing Certificates, die f\u00fcr Single-Sign-On genutzt werden!<\/p>\n<\/blockquote>\n<p>Aus diesen Gr\u00fcnden habe ich mich an diesen Artikel gemacht, einerseits als Handreichung an diejenigen, die diese Methode nutzen sollen bzw. m\u00f6chten (<a href=\"https:\/\/sparrow365.de\/?p=594&amp;preview=true#toc-6\"><strong>siehe das Ergebnis<\/strong><\/a>).<br \/>\nAndererseits hoffe ich, dass ich mit der ausf\u00fchrlichen Form einen interessanten Einblick in Herausforderungen mit der Microsoft Dokumentation und PowerShell Troubleshooting bieten kann. \ud83d\ude01<\/p>\n<p><br class=\"\"><\/p>\n<h2>Herausforderung: Microsofts Codevorschlag f\u00fcr Proof of Posession<\/h2>\n<p>Einer App Registration per Graph API Zertifikatscredentials hinzuf\u00fcgen ist nichts besonderes, dieser Teil des Ablaufs ist <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/applications-how-to-add-certificate?tabs=http\">gut dokumentiert<\/a>. Die eigentliche Herausforderung liegt in der Spezialit\u00e4t des addKey Endpunkts &#8211; dem Nachweis gegen\u00fcber der Applikation, dass wir aktuell ein g\u00fcltiges Zertifikat haben.<\/p>\n<p>Fundamental basiert auch dieser Authentifizierungsprozess auf einem JSON Web Token (JWT), dieser muss aber erst gebaut werden. Gl\u00fccklicherweise bietet Microsoft auch hier eine <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/application-rollkey-prooftoken?tabs=powershell#prerequisite\">Dokumentation des Prozess<\/a>. Allerdings wird schnell klar, dass ihr PowerShell-Snippet nicht optimal ist &#8211; in der Regel wird es bei der Ausf\u00fchrung nur zu Fehlermeldungen kommen.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/2_JustExecute.png\" alt=\"ExecutionError\" \/><\/p>\n<blockquote>\n<p><em>Unable to find type [&#8230;]<\/em><\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<blockquote>\n<p>&quot;Kurzer&quot; Primer:<\/p>\n<ul>\n<li><strong>.NET Assemblies<\/strong> sind die kompilierten Codebibliotheken in .NET, meist als .dll Dateien, die Metadaten und <strong>.NET Types<\/strong> enthalten. Sie erleichtern die Wiederverwendung von Code und unterst\u00fctzen die Versionierung<\/li>\n<li><strong>.NET Types<\/strong> repr\u00e4sentieren Datenstrukturen oder Methoden im Code. Sie erleichtern die Darstellung von Objekten und Abl\u00e4ufen und tragen zur Vereinfachung der Programmierung bei        <\/li>\n<\/ul>\n<p>Beispiel: Das <strong>Assembly<\/strong> <em>Microsoft.IdentityModel.JsonWebTokens<\/em> enth\u00e4lt unter anderem den <strong>Type<\/strong> <em>[Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler]<\/em>, der eine Reihe von Methoden und Eigenschaften bereitstellt, um das Erstellen, Validieren und Verarbeiten von JWTs zu vereinfachen.<br \/>\nDurch die Verwendung dieses Types k\u00f6nnen Entwickler komplexere Abl\u00e4ufe rund um JWTs implementieren, ohne tief in die Details der Token-Erstellung und -Validierung einsteigen zu m\u00fcssen<\/p>\n<\/blockquote>\n<p>Verantwortlich f\u00fcr das Problem ist die Nutzung einiger .NET Types, die nicht im <em>Microsoft.Graph.Authentication<\/em> Modul oder der PowerShell Umgebung enthalten sind. Mir ist unklar, wo diese Types herkommen sollen, da ich die zugeh\u00f6rigen Assemblies in keinem der \u00fcblichen PowerShell Module finden konnte. Mir bleibt also nur die Vermutung, dass der Code 1:1 aus C# in PowerShell \u00fcbersetzt wurde, ohne die Ausf\u00fchrbarkeit f\u00fcr den durchschnittlichen Anwender zu \u00fcberpr\u00fcfen. Ich bin an anderen Erkl\u00e4rungen sehr interessiert. \ud83d\ude09<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/1_MSSnippet-e1711638684250.png\" alt=\"SnippetExample\" \/><\/p>\n<p>An dieser Stelle h\u00e4tte ich anfangen k\u00f6nnen, die erforderlichen Assemblies zu installieren und zu laden. Ich bevorzuge jedoch Funktionen, die auf einer breiteren Palette von Systemen sofort ausf\u00fchrbar sind, auch wenn dadurch der Code komplexer scheint. Die Arbeit mit nuget, dem Paketmanager f\u00fcr .NET-Bibliotheken, ist meiner Erfahrung nach in PowerShell wirklich nicht das Beste, wie wir sp\u00e4ter im Artikel auch sehen werden.<\/p>\n<p><br class=\"\"><\/p>\n<h2>Meine Integrationsversuche<\/h2>\n<blockquote>\n<p><strong>Wichtig: die Code-Snippets im jetzigen Kapitel dienen vorrangig der Veranschaulichung des Entwicklungsprozesses und stellen kein vollst\u00e4ndiges Script dar<\/strong><br \/>\nDer volle Quelltext findet sich <a href=\"https:\/\/sparrow365.de\/?p=594&amp;preview=true#toc-6\">am Ende des Artikels<\/a> <\/p>\n<\/blockquote>\n<p>Bei der Implementierung der <a href=\"https:\/\/sparrow365.de\/index.php\/2024\/01\/28\/connect-mggraph-mit-benutzername-und-passwort\/\">Graph API Authentifizierung mit Benutzername + Passwort<\/a> habe ich mich bereits erfolgreich auf <a href=\"https:\/\/adamtheautomator.com\/powershell-graph-api\/#Acquire_an_Access_Token_Using_a_Certificate\">die Arbeit von Adam The Automator<\/a> im Bau von JWTs gest\u00fctzt, entsprechend habe ich sie auch f\u00fcr diesen Usecase angepasst.<\/p>\n<p>Mein Ziel ist es also jetzt, die fehlenden Types aus dem Microsoft-Beispiel in allgemein verf\u00fcgbaren Code zu \u00fcberf\u00fchren:<\/p>\n<p><br class=\"\"><\/p>\n<h3>Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor<\/h3>\n<p>Wir starten mit diesem Code aus der Microsoft Dokumentation:<\/p>\n<pre><code class=\"language-powershell\"># aud and iss are the only required claims.\n$claims = [System.Collections.Generic.Dictionary[string,object]]::new()\n$claims.aud = $aud\n$claims.iss = $objectId # Bei mir $clientID\n\n$now = (Get-Date).ToUniversalTime()\n$securityTokenDescriptor = [Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor]@{\n    Claims = $claims\n    NotBefore = $now\n    Expires = $now.AddMinutes(10)\n}<\/code><\/pre>\n<p>Mit diesem Type werden also die Inhalte des Tokens definiert. Dabei gibt es einige Punkte, die durch den Standard vorgegeben sind und nicht explizit gelistet werden m\u00fcssen &#8211; praktisch, wenn man sch\u00f6neren Code will, allerdings hinderlich wenn man es nachbauen will. Schlussendlich sah mein \u00c4quivalent wie folgt aus:<\/p>\n<pre><code class=\"language-powershell\">$StartDate = (Get-Date &quot;1970-01-01T00:00:00Z&quot;).ToUniversalTime()\n$now = (Get-Date).ToUniversalTime()\n# Create JWT timestamp for expiration\n$JWTExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now.AddMinutes(2) ).TotalSeconds\n$JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)\n\n# Create JWT timestamp for beginning of validity\n$NotBeforeExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now ).TotalSeconds\n$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)\n\n# Create JWT header\n$JWTHeader = @{\n    alg = &quot;RS256&quot;\n    typ = &quot;JWT&quot;\n    x5t = $CertificateBase64Hash \n}\n\n# Create JWT payload\n$JWTPayLoad = @{\n    aud = &quot;00000002-0000-0000-c000-000000000000&quot;\n    exp = $JWTExpiration\n    iss = $clientID\n    jti = [guid]::NewGuid()\n    nbf = $NotBefore\n    sub = $clientID\n}<\/code><\/pre>\n<blockquote>\n<p><em>Bei genauer Betrachtung f\u00e4llt auf, dass die Application ID der inzwischen eingestellten &quot;Azure AD Graph&quot; API als Audience (aud) des Claims verwendet wird, das ist jedoch notwendig<\/em> <\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<p>Durch die Nutzung der Assemblies spart man sich den Header und das Konvertieren der Zeiten. Das stellt zwar noch keinen dramatischen Vorteil dar, aber es wird deutlich, dass der Code k\u00fcrzer wird, wenn man Bibliotheken nutzt \u2013 schockierend, ich wei\u00df. Unser n\u00e4chstes Beispiel verdeutlicht dies noch st\u00e4rker.<\/p>\n<p><br class=\"\"><\/p>\n<h3>Microsoft.IdentityModel.Tokens.X509SigningCredentials und Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler<\/h3>\n<p>Die Abl\u00f6sung der n\u00e4chsten zwei Types wird zusammengefasst, da &quot;X509SigningCredentials&quot; nur die Form des Zertifikats ist, das der JsonWebTokenHandler nutzt, um den Finalen JWT zu bauen. Hier wird auch tats\u00e4chlich die meiste Arbeit dem Entwickler abgenommen &#8211; im Original haben wir nur zwei Zeilen:<\/p>\n<pre><code class=\"language-powershell\">$handler = [Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler]::new()\n$token = $handler.CreateToken($securityTokenDescriptor)<\/code><\/pre>\n<p>Aus dem Code k\u00f6nnen wir nicht wirklich lernen, was im Hintergrund genutzt wird &#8211; dankbarerweise haben wir ja bereits die Vorarbeit von <a href=\"https:\/\/adamtheautomator.com\/powershell-graph-api\/#Acquire_an_Access_Token_Using_a_Certificate\">Adam the Automator<\/a>. Wir wissen also, dass Header und Payload base64 codiert und mit einer Signatur versehen werden m\u00fcssen. <\/p>\n<pre><code class=\"language-powershell\"># Convert header and payload to base64\n$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) \n\n$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)\n\n$JWT = $EncodedHeader + &quot;.&quot; + $EncodedPayload\n\n# Define RSA signature and hashing algorithm\n$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1\n$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256\n\n# Sign the JWT\n$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)\n$Signature = [Convert]::ToBase64String(\n    $rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)\n) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n# Add Signature to JWT\n$JWT = $JWT + &quot;.&quot; + $Signature<\/code><\/pre>\n<p>TaDa &#8211; wir haben unser Credential Token, oder auch &quot;Proof&quot;.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/JWT.png\" alt=\"JWT\" \/><\/p>\n<blockquote>\n<p>Und an dieser Stelle habe ich initial etwas fatales \u00fcbersehen. In der <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/application-rollkey-prooftoken?tabs=powershell#prerequisite\">Microsoft Dokumentation<\/a> steht klar, was beachtet werden muss &#8211; Halten Sie hier inne, um zu sehen, ob Sie den Fehler finden k\u00f6nnen. \ud83d\ude09<\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<p>An sich sieht eigentlich alles schl\u00fcssig aus, aber dieser Fehler w\u00fcrde mich die n\u00e4chsten Stunden verfolgen:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/ErrorMessage.png\" alt=\"errorMessage\" \/><\/p>\n<blockquote>\n<p><strong><em>Authentication_MissingOrMalformed<\/em><\/strong><\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<h2>Vorl\u00e4ufige Kapitulation<\/h2>\n<p>Nach vielen erfolglosen Versuchen, darunter Experimente mit den Claims im Header und der Signatur, hatte ich genug. Ein paar Tage Pause brachten neue Perspektive, und ich beschloss, die Microsoft-Vorlage zum Laufen zu bringen, um Vergleichswerte zu erhalten.<\/p>\n<p>Als erstes m\u00fcssen also die .NET Assemblys her &#8211; diese bekommt man aus dem NuGet Package Manager aus der NuGet Package Source, beides oftmals nicht vorinstalliert.<br \/>\n<img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/0_InstallNuget-1.png\" alt=\"CheckNugetAvailability\" \/> <\/p>\n<blockquote>\n<p>Nur kurz angerissen, weil ich wie erw\u00e4hnt kein Fan von diesem Pfad bin, da es mannigfaltige Probleme geben kann:<\/p>\n<ul>\n<li>Unter PowerShell 7 (pwsh \/ Core) musste ich nur das Repository hinzu f\u00fcgen <code>Register-PackageSource -Name &quot;NuGet&quot; -Location &quot;https:\/\/api.nuget.org\/v3\/index.json&quot; -ProviderName NuGet<\/code>   <\/li>\n<li>Der Package Provider kann wenn n\u00f6tig mit  <code>Find-PackageProvider -Name NuGet | Install-PackageProvider -Scope CurrentUser -Force<\/code> hinzugef\u00fcgt werden     <\/li>\n<li>Das Repository v3 l\u00e4sst sich unter \u00e4lteren PowerShell Versionen nicht hinzu f\u00fcgen &#8211; alternativ <code>Register-PackageSource -Name &quot;NuGetOld&quot; -Location &quot;https:\/\/www.nuget.org\/api\/v2\/&quot; -ProviderName NuGet<\/code><\/li>\n<\/ul>\n<\/blockquote>\n<p>Beim Hinzuf\u00fcgen des Providers und des Repository endet der zus\u00e4tzliche Aufwand aber noch nicht &#8211; NuGet in PowerShell ist bekannt daf\u00fcr, dass es bei der Aufl\u00f6sung der Abh\u00e4ngigkeiten innerhalb der Assemblies zu Zirkelbez\u00fcgen kommen kann. Die Installation l\u00e4uft also f\u00fcr immer &#8211; oder man bekommt nach einer Weile den Fehler <code>Dependency loop detected for package ...<\/code><\/p>\n<p>Wir m\u00fcssen also die Pakete mit <code>-SkipDependencies<\/code> installieren, ohne die Abh\u00e4ngigkeiten aufzul\u00f6sen &#8211; und d\u00fcrfen dann f\u00fcr jeden weiteren <code>Type not Found<\/code> Fehler das zugeh\u00f6rige Assembly suchen.<br \/>\nSchon mal vorgegriffen, die Abh\u00e4ngigkeiten halten sich in Grenzen, wir brauchen:<\/p>\n<ul>\n<li>Microsoft.IdentityModel.JsonWebTokens<\/li>\n<li>Microsoft.IdentityModel.Logging<\/li>\n<li>Microsoft.IdentityModel.Tokens<\/li>\n<\/ul>\n<p>Alle weiteren Abh\u00e4ngigkeiten werden bereits von dem Microsoft.Graph.Authentication Modul oder dem globalen Katalog abgedeckt.<\/p>\n<p>Um die Assemblies zu installieren, und dann zu laden, habe ich mir folgenden Hilfscode geschrieben:<\/p>\n<pre><code class=\"language-powershell\"># Change to your liking, be aware that this will create directories\n$destination = &quot;$env:OneDrive\\Dokumente\\WindowsPowerShell\\Modules\\&quot;\n\n# Versions are relevant, since they might have specific dependencies\n$packages = @{\n    &quot;Microsoft.IdentityModel.JsonWebTokens&quot; = &quot;7.3.1&quot;\n    &quot;Microsoft.IdentityModel.Logging&quot; = &quot;7.5.0&quot;\n    &quot;Microsoft.IdentityModel.Tokens&quot; = &quot;7.5.0&quot;\n}\n\n# Install \/ Load Packages\nforeach ($p in $packages.keys){ \n    $dllPath = $destination + &quot;\\$p.$($packages.$p)\\lib\\netstandard2.0\\$p.dll&quot;\n    if ( -not (Test-Path $dllPath) )\n    {\n        Install-Package -Name $p -ProviderName NuGet -Scope CurrentUser -Destination $destination -RequiredVersion $packages.$p -Force -SkipDependencies\n    }    \n    [System.Reflection.Assembly]::LoadFrom($dllPath) | Out-Null\n}<\/code><\/pre>\n<blockquote>\n<p><strong>!Achtung!<\/strong> : Die Assemblies m\u00fcssen in jeder Sitzung neu geladen werden &#8211; ich will sie nur zum Troubleshooting nutzen und nicht permanent<\/p>\n<\/blockquote>\n<p>Nach dieser Vorbereitung funktioniert das <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/application-rollkey-prooftoken?tabs=powershell#prerequisite\">Microsoft-Beispiel<\/a>, und wir erhalten einen validen Token (geparst \u00fcber <a href=\"https:\/\/jwt.ms\">https:\/\/jwt.ms<\/a>):<br \/>\n<img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/MSToken.png\" alt=\"MicrosoftToken\" \/><\/p>\n<p><br class=\"\"><\/p>\n<h2>Der Ah-Hah Moment<\/h2>\n<p>Nachdem ich die notwendige Vorarbeit abgeschlossen hatte, wollte ich auf einem neuen PC verifizieren, dass alles generell funktioniert. &quot;Works on my Machine&quot; entspricht nicht meiner Philosophie. Beim erneuten Kopieren des Code-Snippets frisch aus der Microsoft-Dokumentation, las ich zum ersten Mal bewusst einen Satz, der ganz am Ende des Artikels steht:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/DERSatz.png\" alt=\"CriticalSentence\" \/><\/p>\n<p>Zu meiner Verteidigung &#8211; ich hatte zu Beginn nach &quot;Azure KeyVault&quot; aufgeh\u00f6rt zu lesen, da es f\u00fcr mich nicht relevant schien. Die gelistete Fehlermeldung &quot;Authentication_MissingOrMalformed&quot; war mir jedoch zu diesem Zeitpunkt so vertraut, dass sie mir sofort ins Auge sprang.<\/p>\n<p>Betrachten wir noch einmal den Token, den mein Code generiert:<br \/>\n<img decoding=\"async\" src=\"https:\/\/sparrow365.de\/wp-content\/uploads\/2024\/03\/JWTHighlight.png\" alt=\"IssueHighlighted\" \/><\/p>\n<p>Huh. Ich w\u00fcnsche mir manchmal, solche Fehler w\u00fcrden im R\u00fcckblick nicht immer so l\u00e4cherlich simpel aussehen. \ud83e\udd26 Falls jemand den Fehler oben im Artikel (nicht) eigenst\u00e4ndig entdeckt hat, w\u00fcrde ich mich freuen, davon zu h\u00f6ren.<\/p>\n<p>Alles was also fehlte, war das entfernen des &quot;=&quot; Zeichen, wenn wir unseren Header und Payload zu Base64 konvertieren:<\/p>\n<pre><code class=\"language-powershell\">#...\n$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) -replace &#039;=&#039;\n\n$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) -replace &#039;=&#039;\n#....<\/code><\/pre>\n<p>Wenigstens war der Exkurs \u00fcber NuGet nicht umsonst &#8211; da ich jetzt ein Microsoft-Beispiel des Tokens hatte, konnte ich sowohl den Header als auch die Claims angleichen. Neben diesen Anpassungen habe ich den Code bereinigt und das Generieren des Prooftoken in eine Funktion umgewandelt.<\/p>\n<p><br class=\"\"><\/p>\n<h2>Das Ergebnis<\/h2>\n<p>Nach einem ausgiebigen Umweg (oder auch nicht, falls Sie direkt zum Ergebnis gesprungen sind \ud83d\ude09) \u2013 so pr\u00e4sentiert sich nun der Prozess, wenn ich das Zertifikat einer Enterprise App aktualisiere:<\/p>\n<h3>Funktion f\u00fcr Prooftokens<\/h3>\n<p>Dieser Part ist der Umfangreichste und muss zwei mal genutzt werden, entsprechend ist Funktionalisierung sinnvoll:<\/p>\n<blockquote>\n<p>Aktualisierungen pflege ich in meinem <a href=\"https:\/\/gist.github.com\/dreadsend\/b257928af29f8827259f435104986a01\">GitHub gist<\/a><\/p>\n<\/blockquote>\n<pre><code class=\"language-powershell\">function New-Prooftoken {\n    param (\n        [Parameter(Mandatory = $true)]\n        [string]$clientId,\n\n        [Parameter(Mandatory = $true)]\n        [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert\n    )\n\n    # Very far removed, but still based on https:\/\/adamtheautomator.com\/powershell-graph-api\/\n\n    # Get base64 hash of certificate in Web Encoding\n    $CertificateBase64Hash = [System.Convert]::ToBase64String( $cert.GetCertHash() ) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n    $StartDate = (Get-Date &quot;1970-01-01T00:00:00Z&quot;).ToUniversalTime()\n    $now = (Get-Date).ToUniversalTime()\n\n    # Create JWT timestamp for expiration - 5 Minute Lifetime here\n    $JWTExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now.AddMinutes(5) ).TotalSeconds\n    $JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)\n\n    # Create JWT validity start timestamp\n    $NotBeforeExpirationTimeSpan = ( New-TimeSpan -Start $StartDate -End $now ).TotalSeconds\n    $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)\n\n    # Create JWT header\n    $JWTHeader = @{\n        alg = &quot;RS256&quot;\n        typ = &quot;JWT&quot;\n        x5t = $CertificateBase64Hash \n        kid = $cert.Thumbprint\n    }\n\n    # Create JWT payload\n    $JWTPayLoad = @{\n        aud = &quot;00000002-0000-0000-c000-000000000000&quot;\n        exp = $JWTExpiration\n        iss = $clientID\n        nbf = $NotBefore\n        iat = $NotBefore\n    }\n\n    # Convert header and payload to base64\n    $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))\n    $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) -replace &#039;=&#039;\n\n    $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))\n    $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) -replace &#039;=&#039;\n\n    $JWT = $EncodedHeader + &quot;.&quot; + $EncodedPayload\n\n    # Define RSA signature and hashing algorithm\n    $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1\n    $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256\n\n    # Sign the JWT\n    $rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)\n    $Signature = [Convert]::ToBase64String(\n        $rsaCert.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)\n    ) -replace &#039;\\+&#039;, &#039;-&#039; -replace &#039;\/&#039;, &#039;_&#039; -replace &#039;=&#039;\n\n    # Add Signature to JWT\n    $JWT = $JWT + &quot;.&quot; + $Signature\n\n    return ConvertTo-SecureString $JWT -AsPlainText -Force \n\n    end {\n        # Clear Senstive Values\n        $sensitiveVars = @(&quot;Signature&quot;,&quot;JWT&quot;)\n        Remove-Variable $sensitiveVars\n        [gc]::collect()\n    }\n}<\/code><\/pre>\n<h3>Rotieren des Zertifikats<\/h3>\n<p>Zusammengefasst machen wir folgendes:<\/p>\n<ol>\n<li>Laden der Zertifikate<\/li>\n<li>Verbinden mit der Enterprise App<\/li>\n<li>Hinzuf\u00fcgen des neuen Zertifikats<\/li>\n<li><em>[In der Applikation, die die Enterprise App nutzt, die Zertifikate wechseln!]<\/em><\/li>\n<li>Zur Sicherheit neu verbinden<\/li>\n<li>Das alte Zertifikat entfernen<\/li>\n<\/ol>\n<p><strong>F\u00fcr all das sind keine zus\u00e4tzlichen Rechte auf der Enterprise App notwendig!<\/strong><\/p>\n<pre><code class=\"language-powershell\">###### Configure Here #####\n\n# Load Certificates, Alternative: Get-PfxCertificate for .cer Files\n$oldcert = Get-Item &quot;Cert:\\CurrentUser\\My\\$($graphConfig.thumb)&quot;\n$newCert = Get-Item &quot;Cert:\\CurrentUser\\My\\$($graphConfig.newThumb)&quot;\n\n# Replace to your Values\n$clientID = $graphconfig.clientID\n$tenantID = $graphconfig.tenantID\n\n###########################\n\nConnect-MgGraph -TenantId $tenantID -ClientId $clientID -Certificate $oldCert -NoWelcome\n\n# Add new Certificate, store information to Rotate\n$proof = New-Prooftoken -clientId $clientID -cert $oldcert\n$params = @{\n    keyCredential      = @{\n        type  = &quot;AsymmetricX509Cert&quot;\n        usage = &quot;Verify&quot;\n        key   = [convert]::ToBase64String($newCert.GetRawCertData())\n    }\n    passwordCredential = $null\n    proof              = [System.Net.NetworkCredential]::new(&#039;&#039;,$proof).Password\n}\n$newKeyCredential = Invoke-MgGraphRequest POST &quot;\/v1.0\/applications(appId=&#039;$clientID&#039;)\/addKey&quot; -Body $params -OutputType PSObject\n\n# Reconnect to make sure that the new certificate is loaded\nDisconnect-MgGraph | Out-Null\nConnect-MgGraph -TenantId $tenantID -ClientId $clientID -Certificate $newCert -NoWelcome\n\n# Rotate the Certificates in your Application NOW ;)\n\n# Read all currently Registered Certificate Credentials, Filter to only the old Credentials\n$currentKeys = Invoke-MgGraphRequest GET &quot;\/v1.0\/applications(appId=&#039;$clientID&#039;)?`$select=id,appId,keyCredentials&quot; -OutputType PSObject\n$oldKeys = $currentKeys.keyCredentials | Where-Object keyId -ne $newKeyCredential.keyId\n\n# Remove the old Certificate(s)\nforeach ($k in $oldKeys){\n    $proof = New-Prooftoken -clientId $clientID -cert $newcert\n    $params = @{\n        keyId = $oldkeys.keyId\n        proof = [System.Net.NetworkCredential]::new(&#039;&#039;,$proof).Password\n    }\n    $null = Invoke-MgGraphRequest POST &quot;\/v1.0\/applications(appId=&#039;$clientID&#039;)\/removeKey&quot; -Body $params\n}\n\nDisconnect-MgGraph | Out-Null\nRemove-Variable @(&quot;params&quot;, &quot;proof&quot;)\n[gc]::collect()<\/code><\/pre>\n<p><br class=\"\"><\/p>\n<h2>Review<\/h2>\n<p>Dieses Thema war umfangreich und teilweise sehr herausfordernd, doch gibt es kaum etwas Befriedigenderes als das funktionierende Ergebnis harter Arbeit. Haben Sie neue Erkenntnisse gewonnen oder konnten Sie Ihre Verwaltung von Credentials vereinfachen? Lassen sie es mich wissen.      <\/p>\n","protected":false},"excerpt":{"rendered":"<p>W\u00e4hrend ich an meinem Appell gegen Application.ReadWrite.All arbeitete, stie\u00df ich auf eine potenzielle M\u00f6glichkeit, ohne Graph API-Rechte das Authentifizierungszertifikat einer Applikation zu rotieren. Nach einigem Herum-Probieren hatte ich es nicht zu meiner Zufriedenheit in PowerShell umgesetzt, entsprechend habe ich es vorerst beiseite gelegt, um den Artikel abszuschlie\u00dfen. Das Thema lie\u00df mich aber nicht los, da&#8230; &raquo; <a class=\"read-more-link\" href=\"https:\/\/sparrow365.de\/index.php\/2024\/03\/29\/durchdrehen-beim-rotieren-von-entra-id-app-zertifikaten-mit-powershell\/\">weiterlesen<\/a><\/p>\n","protected":false},"author":2,"featured_media":595,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24,48],"tags":[156,64,52,60,56,253],"class_list":["post-594","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-me-id","category-powershell","tag-best-practices","tag-entra","tag-entra-id","tag-graph","tag-graph-api","tag-least-privilege-de"],"_links":{"self":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/594","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/comments?post=594"}],"version-history":[{"count":8,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/594\/revisions"}],"predecessor-version":[{"id":929,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/594\/revisions\/929"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media\/595"}],"wp:attachment":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media?parent=594"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/categories?post=594"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/tags?post=594"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}