Private/Wissen/C_Advance/C08_ScripteSignieren.ps1

<#
 
# Scriptdateien signieren
 
Aufbau einer PKI, erzeugen von Zertifikaten und Skripte signieren und testen
 
- **Hashtags** Zertifikat CodeSigningCert ExecutionPolicy
- **Version** 2020.05.13
 
#>


# ! WARUM SIGNIEREN ?

# ! 1. Eine Signatur schützt ein Skript (ps1, psm1, ps1xml, ...) vor Manipulation!

# ! 2. Der Signierer bestätigt mit seiner Signatur den endgültigen Status des Skriptes!

# ! 3. Dritte können erkennen wer dieses Skripte signiert / erstellt hat.

# ! 4. Die PowerShell kann über die Ausführungsrichtlinien so konfiguriert werden (AllSigned) dass nur Skripte ausgeführt werden, wenn folgende Voraussetzungen erfüllt sind:

# ! (A) Die Skriptdatei wurde mit einem X.509-Zertifikat signiert!

# ! (B) Der Zeitpunkt des Signier-Vorgangs in einem gültigen Bereich lag.

# ! (C) Die signierte Datei wurde anschließend nicht manipuliert!

# ! (D) Das Signierer-Zertifikat stammt von einer "Vertrauenswürdigen Stammzertifizierungsstelle" (Root) ab ODER es ist selbst in Root abgelegt worden!

# ! (E) Signierer-Zertifikat wurde zusätzlich im Zertifikatspeicher für "Vertrauenswürdige Herausgeber" (TrustPublisher) abgelegt!

#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'
certlm.msc  # Zertifikate - Lokaler Computer
certmgr.msc # Zertifikate - Aktueller Benutzer

# ! 2. Problem erkennen, s. Fehlermeldung: (LÖSUNG => Sginieren !)
"' > > > Hallo Welt! < < < ' | Write-Host -ForegroundColor Red -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:
$params = @{
    Subject           = 'CN=_FirstName_LastName (PS Developer), E=v.nachname@abc.local'
    HashAlgorithm     = 'SHA512'
    KeyExportPolicy   = [Microsoft.CertificateServices.Commands.KeyExportPolicy]::ExportableEncrypted # ! GEFAHR !
    CertStoreLocation = 'Cert:\CurrentUser\My'
    Type              = [ Microsoft.CertificateServices.Commands.CertificateType]::CodeSigningCert
    NotAfter          = (Get-Date).AddYears(5)
}
$myPfxCert = New-SelfSignedCertificate @params

# ! 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.
# ? 8.1. Zertifikat im Root-Store speichern
Import-Certificate -FilePath '.\PublicSignerCertificate.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'
# ? 8.2. Zertifikat im TrustedPublisher-Store speichern
Import-Certificate -FilePath '.\PublicSignerCertificate.cer' -CertStoreLocation 'Cert:\LocalMachine\TrustedPublisher'

# ! 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' -Wait

 # ? 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' -Wait

 # ? 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

#region 0. VORBEREITUNG

# TODO Benötigte Namespaces importieren:

using namespace 'Microsoft.CertificateServices.Commands'

# TODO Die zu signierende Datei ins Arbeitsverzeichnis kopieren:

Copy-Item -Path '.\Get-EuroExchange.ps1' -Destination 'C:\Temp\' -Force
Test-Path -Path 'C:\Temp\Get-EuroExchange.ps1' | Write-Warning

# TODO Die Ausführungsrichtlinien 'scharf' schalten:

Set-ExecutionPolicy -ExecutionPolicy 'AllSigned' -Scope 'Process' -Force
Get-ExecutionPolicy -List

# ! Empfohlene Einstellung für den Entwicklungs-Rechner ist:
Set-ExecutionPolicy -ExecutionPolicy 'RemoteSigned' -Scope 'CurrentUser' -Force
# s.a. Unblock-File

#endregion

#region 1. Problem > Lösung

# ! Problem ...

& 'C:\Temp\Get-EuroExchange.ps1'

# ! ... Lösung => Signieren!

# ! Vielleicht besitzen Sie schon ein x.509 Zertifikat mit Verwendungszweck "Codesigning" inkl. des "Private Key"s?

Get-ChildItem -Path 'Cert:\CurrentUser\My' -CodeSigningCert

# ! ... wenn vorhanden dan weiter mit Punkt 6.

#endregion

#region 2. ROOT-CA erstellen

# TODO Root CA erstellen:

$parameters = @{
    Subject           = 'CN=_PowerShell Root CA TEMP'
    HashAlgorithm     = 'SHA512'
    NotAfter          = (Get-Date).AddYears(10)
    Type              = [CertificateType]::Custom
    KeySpec           = 'Signature'
    KeyLength         = 4096
    KeyUsageProperty  = 'Sign'
    KeyUsage          = 'CertSign'
    KeyExportPolicy   = [KeyExportPolicy]::ExportableEncrypted
    CertStoreLocation = 'Cert:\CurrentUser\My'
}
$pfxCert = New-SelfSignedCertificate @parameters

# TODO Root CA-Zertifikate mit dem privaten Schlüssel exportieren:

$pfxPassword = Read-Host -Prompt 'ROOT CA - Das Passwort zum schützen des privaten Schlüssels angeben' -AsSecureString
$pfxCert | Export-PfxCertificate -Password $pfxPassword -FilePath 'C:\Temp\PowerShell_RootCA.pfx' -CryptoAlgorithmOption 'AES256_SHA256' -ChainOption 'BuildChain' -Force

# TODO Die Kopie in Cert:\CurrentUser\CA ist unnötig:

Get-ChildItem -Path 'Cert:\CurrentUser\CA' | Where-Object -Property 'Subject' -EQ -Value 'CN=_PowerShell Root CA TEMP' | Remove-Item

# TODO Ein öffentliches Zertifikat der Root CA exportieren:

Get-PfxCertificate -FilePath 'C:\Temp\PowerShell_RootCA.pfx' | Export-Certificate -FilePath 'C:\Temp\PowerShell_RootCA.cer'

# TODO TEST 1:

Get-ChildItem -Path 'Cert:\' -Recurse | Where-Object 'Subject' -EQ -Value 'CN=_PowerShell Root CA TEMP'

# TODO TEST 2:

& 'C:\Temp\PowerShell_RootCA.cer'

#endregion

#region 3. Root CA verteilen

# TODO Das öffentlich Root-CA-Zertifikat wird i.d.R. per GPO in den Speicher "Vertrauenswürdige Stammzertifizierungsstelle" (Root) verteilt:

Import-Certificate -FilePath 'C:\Temp\PowerShell_RootCA.cer' -CertStoreLocation 'Cert:\LocalMachine\Root' -Verbose

# TODO TEST 1:

Get-ChildItem -Path 'Cert:\' -Recurse | Where-Object -Property 'Subject' -EQ -Value 'CN=_PowerShell Root CA TEMP'

# TODO TEST 2:

& 'C:\Temp\PowerShell_RootCA.cer'

#endregion

#region 4. Signierer-Zertifikate erstellen

# TODO Ein Signierer-Zertifikat pro Benutzer von der Root CA in "Eigene Zertifikate" (MY) erstellen und installieren:

$Parameters = @{
    Subject           = 'CN=_Montgomery Scott (PS Developer)'
    HashAlgorithm     = 'SHA512'
    Type              = [CertificateType]::CodeSigningCert
    KeyExportPolicy   = [KeyExportPolicy]::ExportableEncrypted
    NotAfter          = (Get-Date).AddYears(5)
    Signer            = (Get-PfxCertificate -FilePath 'C:\Temp\PowerShell_RootCA.pfx')
    CertStoreLocation = "Cert:\CurrentUser\My"
}
$pfxCert = New-SelfSignedCertificate @Parameters

# TODO Signierer-Zertifikat mit dem privaten Schlüssel exportieren:

$pfxPassword = Read-Host -Prompt 'Signierer-Zertifikat - Das Passwort zum schützen des privaten Schlüssels angeben' -AsSecureString
$pfxCert | Export-PfxCertificate -Password $pfxPassword -FilePath 'C:\Temp\MontgomeryScott_PSDeveloper.pfx'

# TODO Signierer-Zertifikate mit privaten Schlüssel löschen, da der Cert-Speicher unsicher ist:

Get-ChildItem -Path 'Cert:\CurrentUser\My' | Where-Object -Property 'Subject' -EQ -Value 'CN=_Montgomery Scott (PS Developer)' | Remove-Item -Force

# TODO Öffentliches Zertifikat des Signierers exportieren:

Get-PfxCertificate -FilePath 'C:\Temp\MontgomeryScott_PSDeveloper.pfx' | Export-Certificate -FilePath 'C:\Temp\MontgomeryScott_PSDeveloper.cer' -Force

# TODO TEST:

& 'C:\Temp\MontgomeryScott_PSDeveloper.cer'

#endregion

#region 5. Signierer-Zertifikat verteilen

# TODO Das öffentliche Benutzer-Zertifikat von 4. muss z.B. per GPO auf alle betroffenen Host's verteilt werden:

Import-Certificate -FilePath 'C:\Temp\MontgomeryScott_PSDeveloper.cer' -CertStoreLocation 'Cert:\LocalMachine\TrustedPublisher'

# TODO TEST:

Get-ChildItem -Path 'Cert:\LocalMachine\TrustedPublisher'

#endregion

#region 6. Datei signieren

# TODO Signierer-Zertifikat auswählen:

$pfxCert = Get-ChildItem -Path 'C:\Temp\*.pfx' | Out-GridView -OutputMode Single | Get-PfxCertificate

# TODO .PS1-Datei signieren:

Get-ChildItem -Path 'C:\Temp\*.ps1' | Out-GridView -OutputMode Single | Set-AuthenticodeSignature -Certificate $pfxCert -Force -HashAlgorithm 'SHA512' -IncludeChain 'All' -TimestampServer 'http://timestamp.globalsign.com/scripts/timstamp.dll'

#endregion

#region 7. Kontrolle

# TODO Signierte Datei visualisieren:

Start-Process -FilePath 'C:\Temp\Get-EuroExchange.ps1'

# TODO Signatur der Datei prüfen:

Get-AuthenticodeSignature -FilePath 'C:\Temp\Get-EuroExchange.ps1' | Format-List -Property *

# TODO Signierte Datei ausführen:

& 'C:\Temp\Get-EuroExchange.ps1'

# TODO: Den Code der signierten Datei manipulieren und Prüfungen erneut ausführen:

Start-Process -FilePath 'C:\Temp\Get-EuroExchange.ps1'

Get-AuthenticodeSignature -FilePath 'C:\Temp\Get-EuroExchange.ps1' | Format-List -Property *

& 'C:\Temp\Get-EuroExchange.ps1'

#endregion

#region 8. Aufräumen und keine Spuren hinterlassen

# TODO Öffentliches (vertrauenswürden Herausgeber) und privates Zertifikat des Signierers löschen:

Get-ChildItem -Path 'Cert:\LocalMachine\TrustedPublisher', 'Cert:\CurrentUser\My' | Where-Object 'Subject' -EQ 'CN=_Montgomery Scott (PS Developer)' | Remove-Item

# TODO Öffentliches & privates Zertifikat der Zertifizierungsstelle löschen:

Get-ChildItem -Path 'Cert:\LocalMachine\Root', 'Cert:\CurrentUser\My' | Where-Object 'Subject' -EQ 'CN=_PowerShell Root CA TEMP' | Remove-Item

# TODO Öffentliche und private Zertifikat im Dateisystem löschen:

Get-ChildItem -Path 'C:\Temp' -Recurse | Where-Object 'Extension' -IN '.cer', '.pfx' | Remove-Item

# TODO Test-Dateien löschen:

Get-ChildItem -Path 'C:\Temp' -Recurse | Where-Object 'Extension' -IN '.ps1' | Remove-Item

# TODO Ausführungsrichtlinien auf RemoteSigned setzen:

Set-ExecutionPolicy -ExecutionPolicy 'RemoteSigned' -Scope 'Process' -Force

#endregion

#endregion