Wissen/C08_ScripteSignieren.ps1

# ? TITEL Scriptdateien signieren
# ? DESCRIPTION Aufbau einer PKI, erzeugen von Zertifikaten und Skripte signieren und testen
# ? TAGS Zertifikat CodeSigningCert ExecutionPolicy
# ? VERSION 2019.11.15

# ! WARUM?
# ! a) Schutz gegen Manipulation von PS-Scriptdateien (ps1, psm1, ps1xml, ...)
# ! b) Produktiv die Ausführungsrichtlinien auf "AllSigned" setzen, d.h. Skriptdateien werden nur noch ausgeführt, wenn:
# ! 1. Skriptdatei X.509 signiert ist
# ! 2. das X.509-Zertifikat von einer "Vertrauenswürdigen Stammzertifizierungsstelle" (Root) abstammt ODER
# ! es ist selbst in Root abgelegt!
# ! 3. das X.509-Zertifikat im Zertifikatspeicher für "Vertrauenswürdige Herausgeber" (TrustPublisher) enthalten ist

#region Einfache Umsetzung über ein selbstsigniertes Zertifikat

# ! 1. Testumgebung vorbereiten:
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope Process      -Force
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope CurrentUser  -Force
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine -Force
Get-ExecutionPolicy -List
Set-Location -Path C:\Temp

# ! 2. Problem erkennen, s. Fehlermeldung: (LÖSUNG => Sginieren !)
"' > > > Hallo Welt! < < < ' | Write-Host -ForegroundColor Green -BackgroundColor Yellow" | Set-Content -Path .\Test.ps1 -Force
. ".\Test.ps1"

# ! 3. Mögliche Zertifikate die zum Signieren geeignet wären ermitteln, um evtl. das Erstellen eines eigenen Signierer-Zertifikates zu überspringen:
Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert

# ! 4. Mein Signierer-Zertifikate erstellen:
$myPfxCert = New-SelfSignedCertificate -Subject "CN=_FirstName_LastName (PS Developer), E=v.nachname@abc.local" `
                                       -HashAlgorithm SHA512 `
                                       -KeyExportPolicy ExportableEncrypted `
                                       -CertStoreLocation Cert:\CurrentUser\My `
                                       -Type CodeSigningCert `
                                       -NotAfter (Get-Date).AddYears(5) 

# ! 5. Signierer-Zertifikate mit dem privaten Schlüssel exportieren
$myPfxCertPassword = Read-Host -Prompt "Das Passwort zum schützen des privaten Schlüssels angeben" -AsSecureString
$myPfxCert | Export-PfxCertificate -Password $myPfxCertPassword -FilePath .\MyCodeSigningCert.pfx

# ! 6. Das eben erstellte Signierer-Zertifikate rückstandlos aus allen Zertifikatsspeichern wieder löschen:
Get-ChildItem -Path Cert:\ -Recurse | 
    Where-Object -FilterScript { $_ -is [System.Security.Cryptography.X509Certificates.X509Certificate2] -and $_.Thumbprint -CEQ $myPfxCert.Thumbprint } | 
    Remove-Item

# ! 7. Aus dem Signierer-Zertifikate ein öffentliches Zertifikat erstellen:
Get-PfxCertificate -FilePath .\MyCodeSigningCert.pfx | Export-Certificate -FilePath .\PublicSignerCertificate.cer -Type CERT -Force

# ! 8. Vertrauensstellung einrichten:
# ! Das öffentliche Zertifikat ist z.Zt. nicht vertrauenswürdig, daher muss dies ...
# ! ... 1. im Zertifikatsspeicher Root abgelegt werden, um dem Zertifikat generell zu vertrauen und ...
# ! ... 2. im Zertifikatsspeicher TrustedPublisher abgelegt werden, um den signierten .PS1-Dateien im besonderen zu vertrauen.
# ! Beide Schritte können auch per GPO oder Remote erfolgen.
Import-Certificate -FilePath .\PublicSignerCertificate.cer -CertStoreLocation Cert:\LocalMachine\Root # ! 2. Zertifikat im Root-Store speichern
Import-Certificate -FilePath .\PublicSignerCertificate.cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher # ! 3. Zertifikat im TrustedPublisher-Store speichern

# ! 9. Aktueller Stand der Dinge:
Get-ChildItem -Path .\Test.ps1, ` # ? Die zu signierende .PS1-Datei
                    .\MyCodeSigningCert.pfx, ` # ? Das Signierer-Zertifikate mit dem geschützten privaten Schlüssel
                    .\PublicSignerCertificate.cer # ? Öffentliches Signierer-Zertifikate zum bestätigen von Signaturen

$PublicSignerCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("$(Get-Location)\PublicSignerCertificate.cer")

# ? Das Öffentliches Signierer-Zertifikate liegt im Zertifikatsspeicher Root, um dem Zertifikat generell zu vertrauen.
Get-ChildItem -Path Cert:\*\Root -Recurse | Where-Object Thumbprint -CEQ $PublicSignerCertificate.Thumbprint

# ? Das Öffentliches Signierer-Zertifikate liegt im Zertifikatsspeicher TrustedPublisher, um den signierten .PS1-Dateien im besonderen zu vertrauen.
Get-ChildItem -Path Cert:\*\TrustedPublisher -Recurse | Where-Object Thumbprint -CEQ $PublicSignerCertificate.Thumbprint

# ! 10. Eichrichtung abgeschlossen, es folgt nun das Signieren:
$cert = Get-ChildItem -Path .\*.pfx | Out-GridView -Title "Zertifikat wählen" -OutputMode Single | Get-PfxCertificate

Get-ChildItem -Path . | 
    Where-Object Extension -In '.PS1', '.PSM1' |
    Out-GridView -Title "Zu signierende .PS1 oder PSM1-Datei(en) auswählen" -OutputMode Multiple | 
    Set-AuthenticodeSignature -Certificate $cert `
                              -HashAlgorithm SHA512 `
                              -TimestampServer http://timestamp.globalsign.com/scripts/timstamp.dll # ! <= OPTIONAL um die Gültigkeitsdauer zu verifizieren

# ! 11. Signieren abgeschlossen, es folgt nun das Testen:

 # ? Wird .PS1-Datei nun problemlos ausgeführt?
. '.\Test.ps1'

# ? Für Testzwecke einmal den Dateiinhalt manipulieren:
Start-Process -FilePath '.\Test.ps1'


 # ? Wird .PS1-Datei nun NICHT mehr ausgeführt?
. '.\Test.ps1'

# ? Die digitale Signatur sollte UNGÜLTIG sein?
Get-AuthenticodeSignature -FilePath '.\Test.ps1' | Format-List -Property *

# ? Die Manipulation der der .PS1-Datei wieder rückgängig machen:
Start-Process -FilePath '.\Test.ps1'

 # ? Wird .PS1-Datei nun problemlos ausgeführt?
 . '.\Test.ps1'

 # ? Die digitale Signatur sollte GÜLTIG sein?
Get-AuthenticodeSignature -FilePath '.\Test.ps1' | Format-List -Property *

# ! 12. Die Tests sind abgeschlossen, es folgt nun das Aufräumen:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process      -Force
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser  -Force
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force
Get-ExecutionPolicy -List

$PublicSignerCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("$(Get-Location)\PublicSignerCertificate.cer")
Get-ChildItem -Path Cert:\LocalMachine\Root, Cert:\LocalMachine\TrustedPublisher | Where-Object Thumbprint -CEQ $PublicSignerCertificate.Thumbprint | Remove-Item
Get-ChildItem -Path .\Test.ps1, .\MyCodeSigningCert.pfx, .\PublicSignerCertificate.cer | Remove-Item

#endregion

#region Komplexe Umsetzung über einen Mini-PKI

#
# 0. VORBEREITUNG
#

$CAName          = "_PowerShell Root CA TEMP"                               # Name der Zertifizierungsstelle
$PSDeveloperName = "_VORNAME NACHNAME (PS Developer)"                       # Name des PS-Signierer
$PS1Name         = "Get-EmptyDirectory.ps1"                                 # Die zu signierende Datei
Set-Location C:\Temp                                                        # Akt. Arbeitsverzeichnis festlegen
Test-Path -Path ".\$Ps1Name"                                                # Vorhanden?
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope Process       -Force  # Auch per GPO möglich.
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope CurrentUser   -Force  # Auch per GPO möglich.
Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine  -Force  # Auch per GPO möglich.
Get-ExecutionPolicy -List                                                   # Check?

# Problem ...
& "C:\temp\$PS1Name"
#Lösung ... Lösung => Signieren!

# 1. Vielleicht besitzen Sie schon so ein x.509 Zertifikat mit Verwendungszweck "Codesigning" inkl. des "Private Key"?
Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert
# ... Wenn vorhanden dan weiter mit Punkt 5.

# 2. ROOT-CA erstellen
$CACert = New-SelfSignedCertificate -Subject "CN=$CAName" `
                                    -HashAlgorithm SHA512 `
                                    -NotAfter (Get-Date).AddYears(10) `
                                    -Type Custom `
                                    -KeySpec Signature `
                                    -KeyExportPolicy Exportable `
                                    -KeyLength 4096 `
                                    -KeyUsageProperty Sign `
                                    -KeyUsage CertSign `
                                    -CertStoreLocation Cert:\CurrentUser\My
Get-ChildItem -Path Cert:\CurrentUser\CA | Where-Object Thumbprint -EQ $CACert.Thumbprint | Remove-Item # ! die Kopie in \CA ist Unnötig!
$CACert | Export-Certificate -FilePath ".\$CAName.cer" 
# ? TESTING
Get-ChildItem -Path Cert:\ -Recurse | Where-Object Thumbprint -EQ $CACert.Thumbprint
. ".\$CAName.cer"

# 3. Öffentlich Root-CA verteilen (z.B. über GPO) in den Speicher für vertrauenswürdige Stammzertifizierungsstellen (Root)
Import-Certificate -FilePath ".\$CAName.cer" -CertStoreLocation Cert:\LocalMachine\Root
# ? TESTING
Get-ChildItem -Path Cert:\ -Recurse | Where-Object Thumbprint -EQ $CACert.Thumbprint
. ".\$CAName.cer"

# 4. Signierungszertifikate pro Benutzer in "Eigene Zertifikate (MY)" installieren
$PSDeveloperCert = New-SelfSignedCertificate -Subject $PSDeveloperName `
                                             -HashAlgorithm SHA512 `
                                             -CertStoreLocation Cert:\CurrentUser\My `
                                             -Type CodeSigningCert `
                                             -Signer $CACert
Export-Certificate -Cert "Cert:\CurrentUser\My\$($PSDeveloperCert.Thumbprint)" -FilePath "$PSDeveloperName.cer" -Force 
# ? TESTING
. ".\$PSDeveloperName.cer"

# 5. Jetzt Können *.ps1-Dateien signiert werden
Set-AuthenticodeSignature -FilePath ".\$PS1Name" -Certificate $PSDeveloperCert -Force -HashAlgorithm SHA512 -IncludeChain all
# ? TESTING
. ".\$PS1Name"

# 6. OPTIONAL Das öffentliche Benutzer-Zertifikat von 3. muss z.B. per GPO auf alle betroffenen Host's verteilt
Import-Certificate -FilePath "$PSDeveloperName.cer" -CertStoreLocation Cert:\LocalMachine\TrustedPublisher
# ? TESTING
Get-ChildItem -Path Cert:\LocalMachine\TrustedPublisher
. ".\$PS1Name"

# 7. Kontrollieren (Mit / Ohne eine Änderung an der signierten Datei)
Get-Content ".\$PS1Name"
Get-AuthenticodeSignature -FilePath ".\$PS1Name" | Format-List -Property *
Start-Process -FilePath ".\$PS1Name"                                        # TEST: Einmal Code manipulieren
Get-AuthenticodeSignature -FilePath ".\$PS1Name" | Format-List -Property *  # TEST: Valid?
. ".\$PS1Name"                                                              # TEST: Wird ausgeführt?

# 8. Empfohlene Einstellung für den Entwicklungs-Rechner
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force

# 9. Signieren im Alltag
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | Out-GridView -Title "Zertifikat wählen" -OutputMode Single
Get-ChildItem -Path .\*.ps1 | 
    Out-GridView -Title "Zu signierende .PS1 oder psm1-Datei(en) auswählen" -OutputMode Multiple |
    Set-AuthenticodeSignature -Certificate $cert -IncludeChain all -HashAlgorithm SHA512

# 10. A) AUFRÄUMEN: Öffentliches Zertifikat des vertrauenswürden Herausgeber (Der Signierer) löschen
Get-ChildItem -Path Cert:\LocalMachine\TrustedPublisher | Where-Object Thumbprint -EQ $PSDeveloperCert.Thumbprint | Remove-Item
Get-ChildItem -Path Cert:\CurrentUser\My                | Where-Object Thumbprint -EQ $PSDeveloperCert.Thumbprint | Remove-Item

# 10. B) AUFRÄUMEN: Öffentliches & privates Zertifikat der Zertifizierungsstelle löschen
Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object Thumbprint -EQ $CACert.Thumbprint | Remove-Item
Get-ChildItem -Path Cert:\CurrentUser\My    | Where-Object Thumbprint -EQ $CACert.Thumbprint | Remove-Item

# 10. C) AUFRÄUMEN: Öffentliche Zertifikat der Zertifizierungsstelle und des vertrauenswürden Herausgeber (Signierer) im Arbeitsverzeichnis löschen
Get-ChildItem -Path .                            | Where-Object Extension -IN '.cer'      | Remove-Item

# 10. D) AUFRÄUMEN: Ausführungsrichtlinien auf RemoteSigned setzen.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process      -Force
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser  -Force
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force

# 10. E) AUFRÄUMEN: TEST: Sind alle betroffenen Zertifikat im Laufwerk Cert:\ gelöscht?
Get-ChildItem -Path Cert:\ -Recurse | Where-Object Thumbprint -IN $CACert.Thumbprint, $PSDeveloperCert.Thumbprint 

# 10. F) AUFRÄUMEN: TEST: Sind alle betroffenen Zertifikat im Arbeitsverzeichnis gelöscht?
Get-ChildItem -Path . -Recurse | Where-Object -Property Extension -EQ ".cert"

# 10. G) AUFRÄUMEN: TEST: Passen die Ausführungsrichtlinien?
Get-ExecutionPolicy -List

#endregion