From 61b2af287d80fb4ea7d92b67caa1094218531c36 Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Fri, 6 Jun 2025 17:13:09 +0200 Subject: [PATCH 1/8] Allow using custom Auth Certificate lifetime in MonitorExchangeAuthCertificate.ps1 --- .build/cspell-words.txt | 3 + .../New-ExchangeAuthCertificate.ps1 | 24 +- .../MonitorExchangeAuthCertificate.ps1 | 15 +- .../New-ExchangeSelfSignedCertificate.ps1 | 344 ++++++++++++++++++ docs/Admin/MonitorExchangeAuthCertificate.md | 1 + 5 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt index 5030b98410..31c08fc4eb 100644 --- a/.build/cspell-words.txt +++ b/.build/cspell-words.txt @@ -24,6 +24,7 @@ Dsamain DTLS dumptidset DWORD +ecdsa eems EFORMS EICAR @@ -102,6 +103,7 @@ NDIS Nego Netlogon netsh +nist nmap noderunner notcontains @@ -114,6 +116,7 @@ NTFS NUMA nupkg odata +oids onmicrosoft onprem OutlookiOS diff --git a/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 index 0396186cf5..07b470c57d 100644 --- a/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 @@ -4,6 +4,7 @@ . $PSScriptRoot\..\DataCollection\Get-ExchangeServerCertificate.ps1 . $PSScriptRoot\..\..\..\Shared\ActiveDirectoryFunctions\Get-InternalTransportCertificateFromServer.ps1 . $PSScriptRoot\..\..\..\Shared\CertificateFunctions\Import-ExchangeCertificateFromRawData.ps1 +. $PSScriptRoot\..\..\..\Shared\CertificateFunctions\New-ExchangeSelfSignedCertificate.ps1 . $PSScriptRoot\..\..\..\Shared\Invoke-CatchActionError.ps1 function New-ExchangeAuthCertificate { @@ -17,8 +18,13 @@ function New-ExchangeAuthCertificate { [switch]$ConfigureNextAuthCertificate, [Parameter(Mandatory = $true, ParameterSetName = "NewNextAuthCert")] + [ValidateScript({ $_ -ge 0 })] [int]$CurrentAuthCertificateLifetimeInDays, + [Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")] + [Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")] + [int]$NewAuthCertificateLifetimeInDays, + [Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")] [Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")] [ScriptBlock]$CatchActionFunction @@ -100,6 +106,16 @@ function New-ExchangeAuthCertificate { ErrorAction = "Stop" } + $newCustomAuthCertificateParams = @{ + AlgorithmType = "RSA" + UseRSACryptoServiceProvider = $true # Make sure to set this to true as the certificate can't be used as Auth Certificate otherwise + KeySize = 2048 + LifetimeInDays = $NewAuthCertificateLifetimeInDays + SubjectName = "Microsoft Exchange Server Auth Certificate" + FriendlyName = $authCertificateFriendlyName + DomainName = @() + } + if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, $confirmationMessage, "Unattended Exchange certificate generation")) { Write-Verbose ("Internal transport certificate will be overwritten for a short time and then reset to the previous one") $internalTransportCertificate = Get-InternalTransportCertificateFromServer $env:COMPUTERNAME @@ -187,7 +203,13 @@ function New-ExchangeAuthCertificate { Write-Verbose ("Starting Auth Certificate creation process") try { if ($PSCmdlet.ShouldProcess("New-ExchangeCertificate", "Generate new Auth Certificate")) { - $newAuthCertificate = New-ExchangeCertificate @newAuthCertificateParams + if ($NewAuthCertificateLifetimeInDays -gt 0) { + Write-Verbose "Creating a custom self-signed certificate with a lifetime of $NewAuthCertificateLifetimeInDays days" + $newAuthCertificate = New-ExchangeSelfSignedCertificate @newCustomAuthCertificateParams + } else { + Write-Verbose "Creating a default self-signed certificate with a lifetime of 5 years" + $newAuthCertificate = New-ExchangeCertificate @newAuthCertificateParams + } Start-Sleep -Seconds 5 } else { $newAuthCertificateParams.GetEnumerator() | ForEach-Object { diff --git a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 index 33990b1508..0a61d6d839 100644 --- a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 @@ -87,6 +87,11 @@ param( [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [bool]$ValidateAndRenewAuthCertificate = $false, + [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] + [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] + [ValidateScript({ $_ -ge 0 })] + [int]$CustomCertificateLifetimeInDays = 0, + [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] [bool]$IgnoreUnreachableServers = $false, @@ -169,7 +174,7 @@ function Main { param() if (-not(Confirm-Administrator)) { - Write-Warning ("The script needs to be executed in elevated mode. Start the Exchange Management Shell as an Administrator.") + Write-Warning ("The script must be executed in elevated mode. Start the Exchange Management Shell as an administrator.") $Error.Clear() Start-Sleep -Seconds 2 exit @@ -460,9 +465,10 @@ function Main { Write-Host ("Renewal scenario: $($renewalActionWording)") if ($authCertStatus.ReplaceRequired) { $replaceExpiredAuthCertificateParams = @{ - ReplaceExpiredAuthCertificate = $true - CatchActionFunction = ${Function:Invoke-CatchActions} - WhatIf = $WhatIfPreference + ReplaceExpiredAuthCertificate = $true + NewAuthCertificateLifetimeInDays = $CustomCertificateLifetimeInDays + CatchActionFunction = ${Function:Invoke-CatchActions} + WhatIf = $WhatIfPreference } $renewalActionResult = New-ExchangeAuthCertificate @replaceExpiredAuthCertificateParams @@ -471,6 +477,7 @@ function Main { } elseif ($authCertStatus.ConfigureNextAuthRequired) { $configureNextAuthCertificateParams = @{ ConfigureNextAuthCertificate = $true + NewAuthCertificateLifetimeInDays = $CustomCertificateLifetimeInDays CurrentAuthCertificateLifetimeInDays = $authCertStatus.CurrentAuthCertificateLifetimeInDays CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference diff --git a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 new file mode 100644 index 0000000000..99ecaafaad --- /dev/null +++ b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 @@ -0,0 +1,344 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\Confirm-Administrator.ps1 + +function New-ExchangeSelfSignedCertificate { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Certificate creation is intentional and controlled')] + [CmdletBinding()] + param( + [ValidateScript({ $_.Length -lt 64 })] + [string]$SubjectName = $env:COMPUTERNAME, + + [string[]]$DomainName, + + [string]$FriendlyName = "Microsoft Exchange", + + [ValidateScript({ $_ -gt 0 })] + [int]$LifetimeInDays = 365, + + [ValidateSet("RSA", "ECC")] + [string]$AlgorithmType = "RSA", + + [bool]$UseRSACryptoServiceProvider = $false, + + [ValidateSet(1024, 2048, 4096)] + [int]$KeySize = 2048, + + [ValidateSet("nistP256", "nistP384", "nistP521")] + [string]$CurveName = "nistP384", + + [ValidateSet("SHA256", "SHA384", "SHA512")] + [string]$HashAlgorithm = "SHA256", + + [switch]$AddSubjectKeyIdentifier, + + [switch]$TrustCertificate + ) + + <# + Generates a self-signed certificate for Exchange with support for RSA/ECC, SANs, and optional import to trusted root store. + This function supports both legacy CSP and modern CNG key generation models. While CSP (Cryptographic Service Provider) is compatible with older systems, + CNG (Cryptography Next Generation) offers enhanced algorithm support like ECC and better key storage flexibility. + #> + + begin { + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + + if (-not(Confirm-Administrator)) { + Write-Host "Insufficient permissions to perform the certificate operation" -ForegroundColor Red + + return + } + } process { + # Generate the X500DistinguishedName for the certificate + $subject = [System.Security.Cryptography.X509Certificates.X500DistinguishedName]::new( + $(if ($SubjectName.IndexOf("cn=") -eq -1) { "cn=$SubjectName" } else { $SubjectName }), + [System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags]::UseUTF8Encoding + ) + Write-Verbose "Subject: $($subject.Name)" + + # Assign UTF-8 encoded FriendlyName to support non-ASCII characters in multilingual environments + $utf8FriendlyName = [System.Text.Encoding]::UTF8.GetString([System.Text.Encoding]::UTF8.GetBytes($FriendlyName)) + Write-Verbose "FriendlyName: $utf8FriendlyName" + + # Convert the user-specified hash algorithm string into a HashAlgorithmName object required by the CertificateRequest constructor for digital signature generation + $hashAlgorithmName = [System.Security.Cryptography.HashAlgorithmName]::new($HashAlgorithm) + Write-Verbose "HashAlgorithm: $($hashAlgorithmName.Name)" + + if ($AlgorithmType -eq "ECC") { + Write-Verbose "ECC-based certificate will be created" + + # Generate the public/private ECC key pair + $ecdsa = [System.Security.Cryptography.ECDsa]::Create() + Write-Verbose "Public/private key pair SignatureAlgorithm: $($ecdsa.SignatureAlgorithm) KeySize: $($ecdsa.KeySize)" + + $curve = [System.Security.Cryptography.ECCurve]::CreateFromFriendlyName($CurveName) + Write-Verbose "ECC Curve: $($curve.Oid.FriendlyName)" + + try { + Write-Verbose "Generating key by using $CurveName curve" + $ecdsa.GenerateKey($curve) + + # Generate the ECC CertificateRequest + Write-Verbose "Generating the ECC CertificateRequest..." + + # Initializes a new instance of the CertificateRequest class using the specified subject name, ECDSA key, and hash algorithm + $certificateRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( + $subject, + $ecdsa, + $hashAlgorithmName + ) + } catch { + Write-Host "Something went wrong while creating the CertificateRequest. Exception $_" -ForegroundColor Red + + return + } + } else { + Write-Verbose "RSA-based certificate will be created..." + + if ($UseRSACryptoServiceProvider) { + Write-Verbose "Initializing the CspParameters..." + + # Initializes a new instance of CspParameters + $cspParams = [System.Security.Cryptography.CspParameters]::new() + + # Parameters that are passed to the Cryptographic Service Provider (CSP) + #cspell:disable + $cspParams.Flags = [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore + $cspParams.ProviderType = 24 # PROV_RSA_FULL + $cspParams.KeyNumber = 1 # AT_KEYEXCHANGE + $cspParams.KeyContainerName = (New-Guid).Guid.ToString() + #cspell:enable + + Write-Verbose "Generating the public/private RSA key pair..." + # Initializes a new instance of RSACryptoServiceProvider to generate a new key pair, pass KeySize and CspParameters + $rsa = [System.Security.Cryptography.RSACryptoServiceProvider]::new( + $KeySize, + $cspParams + ) + + # Ensure the RSA private key persists beyond the current session + $rsa.PersistKeyInCsp = $true + } else { + Write-Verbose "Initializing the CngKeyCreationParameters..." + + # Initializes a new instance of CngKeyCreationParameters + $cngKeyCreationParameters = [System.Security.Cryptography.CngKeyCreationParameters]::new() + + # Parameters that are passed to the Cryptography Next Generation (CNG) + $cngKeyCreationParameters.Provider = [System.Security.Cryptography.CngProvider]::MicrosoftSoftwareKeyStorageProvider + $cngKeyCreationParameters.KeyCreationOptions = [System.Security.Cryptography.CngKeyCreationOptions]::OverwriteExistingKey + $cngKeyCreationParameters.ExportPolicy = [System.Security.Cryptography.CngExportPolicies]::AllowExport + + # Generate a unique name for the key as we can't create it as an ephemeral key for whatever reason + $cngKeyName = (New-Guid).Guid.ToString() + + # Add RSA-specific CngProperty for the key size + Write-Verbose "RSA key size: $KeySize" + $cngKeyLengthProperty = [System.Security.Cryptography.CngProperty]::new( + "Length", # Property name + [BitConverter]::GetBytes($KeySize), # Property value bytes + [System.Security.Cryptography.CngPropertyOptions]::None + ) + + Write-Verbose "Adding RSA-specific KeyLength property" + $cngKeyCreationParameters.Parameters.Add($cngKeyLengthProperty) + + # Creates a named CngKey object that provides the specified algorithm and keyName making the key persistent, using the supplied key creation parameter + Write-Verbose "Creating the RSA-based CngKey..." + $cngKey = [System.Security.Cryptography.CngKey]::Create( + [System.Security.Cryptography.CngAlgorithm]::Rsa, + $cngKeyName, + $cngKeyCreationParameters + ) + + # Generate the public/private RSA key pair + Write-Verbose "Generating the public/private RSA key pair..." + $rsa = [System.Security.Cryptography.RSACng]::new($cngKey) + } + + try { + Write-Verbose "Generating the RSA CertificateRequest..." + + # Initializes a new instance of the CertificateRequest class using the specified subject name, RSA key, hash algorithm, and using PKCS #1 v1.5 padding + $certificateRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( + $subject, + $rsa, + $hashAlgorithmName, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + } catch { + Write-Host "Something went wrong while creating the CertificateRequest. Exception $_" -ForegroundColor Red + + return + } + } + + # Add SubjectAlternativeNames if some were passed via DomainName parameter + if ($DomainName.Count -gt 0) { + Write-Verbose "DomainNames that will be added to the certificate: $([System.String]::Join(", ", $DomainName))" + + $sanBuilder = [System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder]::new() + + foreach ($name in $DomainName) { + Write-Verbose "Adding DnsName: $name" + $sanBuilder.AddDnsName($name) + } + + $certificateRequest.CertificateExtensions.Add( + $sanBuilder.Build($true) + ) + } + + try { + Write-Verbose "Processing certificate extensions..." + + # Specify the X509KeyUsageExtension + $keyUsageExtensions = [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new( + [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature -bor # DigitalSignature: The certificate's public key can be used to verify digital signatures + [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment, # KeyEncipherment: The public key can also be used to encrypt symmetric keys + $true # critical: marked as critical + ) + + $certificateRequest.CertificateExtensions.Add($keyUsageExtensions) + + # Specify the X509EnhancedKeyUsageExtension + $oids = [System.Security.Cryptography.OidCollection]::new() + $oids.Add([System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.1")) | Out-Null # Server Authentication OID + + $extendedKeyUsageExtension = [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new( + $oids, # OID for Server Authentication + $false # not critical: marked as not critical + ) + + $certificateRequest.CertificateExtensions.Add($extendedKeyUsageExtension) + + # Specify the X509BasicConstraintsExtension + $basicConstraints = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new( + $false, # certificateAuthority: this is not a CA + $false, # hasPathLengthConstraint: we don't want to enforce one + 0, # pathLengthConstraint: ignored since hasPathLengthConstraint is false + $true # critical: marked as critical + ) + + $certificateRequest.CertificateExtensions.Add($basicConstraints) + + # Add the Subject Key Identifier (SKI) as a non-critical extensions if AddSubjectKeyIdentifier parameter was set to true + if ($AddSubjectKeyIdentifier) { + $subjectKeyIdentifier = [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new( + $certificateRequest.PublicKey, + $false + ) + + $certificateRequest.CertificateExtensions.Add($subjectKeyIdentifier) + } + } catch { + Write-Host "Something went wrong while processing certificate extensions. Exception: $_" -ForegroundColor Red + + return + } + + try { + # Create the self-signed certificate + Write-Verbose "Creating the self-signed certificate with a lifetime of $LifetimeInDays days" + + $notBefore = [System.DateTimeOffset]::UtcNow + $notAfter = $notBefore.AddDays($LifetimeInDays) + $certificate = $certificateRequest.CreateSelfSigned( + $notBefore, + $notAfter + ) + + if (-not([System.String]::IsNullOrEmpty($utf8FriendlyName))) { + $certificate.FriendlyName = $utf8FriendlyName + } + + Write-Verbose "Certificate was created successfully. Thumbprint: $($certificate.Thumbprint)" + } catch { + Write-Host "Something went wrong while creating the self-signed certificate. Exception: $_" -ForegroundColor Red + + return + } + + try { + # To make the certificate and its private key exportable, we must export and re-import it with the Exportable flag + Write-Verbose "Exporting and re-importing certificate with Exportable flag to make it exportable..." + + $pfxBytes = $certificate.Export( + [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx + ) + $certificateWithExportableKey = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new() + $certificateWithExportableKey.Import( + $pfxBytes, + $null, + ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet) + ) + + # Add it to the LocalMachine store + Write-Verbose "Adding the certificate to the My/LocalMachine certificate store..." + + $machineStore = [System.Security.Cryptography.X509Certificates.X509Store]::new( + "My", + "LocalMachine" + ) + $machineStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $machineStore.Add($certificateWithExportableKey) + $machineStore.Close() + + # Add the certificate to the Trusted Root Certification Authorities if explicitly specified via TrustCertificate parameter + if ($TrustCertificate) { + Write-Verbose "Adding the certificate to the Root/LocalMachine store to make it a trusted certificate..." + + $trustedRootStore = [System.Security.Cryptography.X509Certificates.X509Store]::new( + "Root", + "LocalMachine" + ) + $trustedRootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $trustedRootStore.Add($certificateWithExportableKey) + $trustedRootStore.Close() + } + } catch { + Write-Host "Something went wrong while adding the certificate to the store. Exception: $_" -ForegroundColor Red + + return + } finally { + if ($null -ne $pfxBytes) { + Write-Verbose "Overwriting temporary .pfx with random data..." + + [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($pfxBytes) + $pfxBytes = $null + } + + if ($null -ne $certificateWithExportableKey) { + Write-Verbose "Disposing certificate from memory..." + + $certificateWithExportableKey.Dispose() + } + } + } end { + if ($null -ne $cngKeyName) { + Write-Verbose "Deleting CngKey object..." + ([System.Security.Cryptography.CngKey]::Open($cngKeyName)).Delete() + } + + if ($null -ne $ecdsa) { + # Dispose the ECC key + Write-Verbose "Disposing ECDsa key object..." + $ecdsa.Dispose() + } + + if ($null -ne $rsa) { + # Dispose the RSA key + Write-Verbose "Disposing RSA key object..." + $rsa.Dispose() + } + + return [PSCustomObject]@{ + Subject = $certificate.Subject + Thumbprint = $certificate.Thumbprint + } + } +} diff --git a/docs/Admin/MonitorExchangeAuthCertificate.md b/docs/Admin/MonitorExchangeAuthCertificate.md index 2e624aa75a..f8a4a49351 100644 --- a/docs/Admin/MonitorExchangeAuthCertificate.md +++ b/docs/Admin/MonitorExchangeAuthCertificate.md @@ -122,6 +122,7 @@ PS C:\> .\MonitorExchangeAuthCertificate.ps1 -ScriptUpdateOnly Parameter | Description ----------|------------ ValidateAndRenewAuthCertificate | This optional parameter enables the validation and renewal mode which will perform the required actions to replace an invalid/expired Auth Certificate or configures a new next Auth Certificate if the current Auth Certificate expires in < 60 days or the certificate configured as next Auth Certificate expires in < 120 days. +CustomCertificateLifetimeInDays | This optional parameter allows you to specify a custom lifetime in days for the new Auth Certificate. IgnoreUnreachableServers | This optional parameter can be used to ignore if some of the Exchange servers within the organization cannot be reached. If this parameter is used, the script only validates the servers that can be reached and will perform Auth Certificate renewal actions based on the result. Parameter can be combined with the `IgnoreHybridConfig` parameter and can also be used with the `ConfigureScriptToRunViaScheduledTask` parameter to configure the script to run via scheduled task. IgnoreHybridConfig | This optional parameter allows you to explicitly perform Auth Certificate renewal actions (if required) even if an Exchange hybrid configuration was detected. You need to run the HCW after the renewed Auth Certificate becomes the one in use. Parameter can be combined with the `IgnoreUnreachableServers` parameter and can also be used with the `ConfigureScriptToRunViaScheduledTask` parameter to configure the script to run via scheduled task. PrepareADForAutomationOnly | This optional parameter can be used in AD Split Permission scenarios. It allows you to create the AD account which can then be used to run the Exchange Auth Certificate Monitoring script automatically via Task Scheduler. From 202b3a8bb2294b32fc6fb513287fddbf473cd6fc Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Fri, 6 Jun 2025 17:36:46 +0200 Subject: [PATCH 2/8] Fix ValidateScript issue --- .../ConfigurationAction/New-ExchangeAuthCertificate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 index 07b470c57d..2271a18f0f 100644 --- a/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/ConfigurationAction/New-ExchangeAuthCertificate.ps1 @@ -18,11 +18,11 @@ function New-ExchangeAuthCertificate { [switch]$ConfigureNextAuthCertificate, [Parameter(Mandatory = $true, ParameterSetName = "NewNextAuthCert")] - [ValidateScript({ $_ -ge 0 })] [int]$CurrentAuthCertificateLifetimeInDays, [Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")] [Parameter(Mandatory = $false, ParameterSetName = "NewNextAuthCert")] + [ValidateScript({ $_ -ge 0 })] [int]$NewAuthCertificateLifetimeInDays, [Parameter(Mandatory = $false, ParameterSetName = "NewPrimaryAuthCert")] From eb0a22d1ca85787955a321df7a499985e682ba35 Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Fri, 6 Jun 2025 19:52:35 +0200 Subject: [PATCH 3/8] Add parameter to enforce new Auth Certificate creation --- .../Get-ExchangeAuthCertificateStatus.ps1 | 12 +++++++--- .../MonitorExchangeAuthCertificate.ps1 | 24 +++++++++++++++---- docs/Admin/MonitorExchangeAuthCertificate.md | 1 + 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 b/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 index 1e6a1cc5a5..96738473f8 100644 --- a/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 @@ -8,7 +8,11 @@ function Get-ExchangeAuthCertificateStatus { [OutputType([System.Object])] param( [bool]$IgnoreUnreachableServers = $false, + [bool]$IgnoreHybridSetup = $false, + + [bool]$EnforceNewNextAuthCertificateCreation = $false, + [ScriptBlock]$CatchActionFunction ) @@ -158,13 +162,15 @@ function Get-ExchangeAuthCertificateStatus { ($nextAuthCertificateValidInDays -lt 0)) { # Scenario 1: Current Auth Certificate has expired and no next Auth Certificate defined or the next Auth Certificate has expired $replaceRequired = $true - } elseif ((($currentAuthCertificateValidInDays -ge 0) -and + } elseif (((($currentAuthCertificateValidInDays -ge 0) -and ($currentAuthCertificateValidInDays -le 60)) -and (($nextAuthCertificateValidInDays -le 0) -or ($nextAuthCertificateValidInDays -le 120)) -and ($currentAuthCertificateMissingOnServersList.Count -eq 0) -and - ($nextAuthCertificateMissingOnServersList.Count -eq 0)) { + ($nextAuthCertificateMissingOnServersList.Count -eq 0)) -or + $EnforceNewNextAuthCertificateCreation) { # Scenario 2: Current Auth Certificate is valid but no next Auth Certificate defined or next Auth Certificate will expire in < 120 days + # or EnforceNewNextAuthCertificateCreation was explicitly set to true $configureNextAuthRequired = $true } elseif (($currentAuthCertificateValidInDays -le 0) -and ($nextAuthCertificateValidInDays -ge 0)) { @@ -186,7 +192,7 @@ function Get-ExchangeAuthCertificateStatus { Write-Verbose ("Replace of the primary Auth Certificate required? $($replaceRequired)") Write-Verbose ("Import of the primary Auth Certificate required? $($importCurrentAuthCertificateRequired)") - Write-Verbose ("Replace of the next Auth Certificate required? $($configureNextAuthRequired)") + Write-Verbose ("Replace of the next Auth Certificate required or explicitly desired? $($configureNextAuthRequired)") Write-Verbose ("Import of the next Auth Certificate required? $($importNextAuthCertificateRequired)") Write-Verbose ("Hybrid Configuration detected? $($null -ne $hybridConfiguration)") Write-Verbose ("Stop processing due to hybrid? $($stopProcessingDueToHybrid)") diff --git a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 index 0a61d6d839..91c813b4cb 100644 --- a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 @@ -22,6 +22,11 @@ .PARAMETER ValidateAndRenewAuthCertificate You can use this parameter to let the script perform the required Auth Certificate renewal actions. If the script runs with this parameter set to $false, no action will be made to the current Auth Configuration. +.PARAMETER EnforceNewAuthCertificateCreation + You can use this switch parameter to let the script stage a new next Auth Certificate which will become automatically active within 24 hours. +.PARAMETER CustomCertificateLifetimeInDays + You can use this parameter to specify a custom lifetime for the newly created Auth certificate. + By default, the self-signed certificate is created with a lifetime of 5 years. .PARAMETER IgnoreUnreachableServers This optional parameter can be used to ignore if some of the Exchange servers within the organization cannot be reached. If this parameter is used, the script only validates the servers that can be reached and will perform Auth Certificate @@ -87,17 +92,23 @@ param( [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [bool]$ValidateAndRenewAuthCertificate = $false, + [Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")] + [switch]$EnforceNewAuthCertificateCreation, + [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] + [Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")] [ValidateScript({ $_ -ge 0 })] [int]$CustomCertificateLifetimeInDays = 0, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] + [Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")] [bool]$IgnoreUnreachableServers = $false, [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] + [Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")] [bool]$IgnoreHybridConfig = $false, [Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")] @@ -139,6 +150,7 @@ param( [Parameter(Mandatory = $false, ParameterSetName = "MonitorExchangeAuthCertificateManually")] [Parameter(Mandatory = $false, ParameterSetName = "ConfigureAutomaticExecutionViaScheduledTask")] + [Parameter(Mandatory = $false, ParameterSetName = "EnforceNewNextAuthCertificateConfiguration")] [Parameter(Mandatory = $false, ParameterSetName = "SetupAutomaticExecutionADRequirements")] [switch]$SkipVersionCheck ) @@ -428,9 +440,10 @@ function Main { } $authCertificateStatusParams = @{ - IgnoreUnreachableServers = $IgnoreUnreachableServers - IgnoreHybridSetup = $IgnoreHybridConfig - CatchActionFunction = ${Function:Invoke-CatchActions} + IgnoreUnreachableServers = $IgnoreUnreachableServers + IgnoreHybridSetup = $IgnoreHybridConfig + EnforceNewNextAuthCertificateCreation = $EnforceNewAuthCertificateCreation + CatchActionFunction = ${Function:Invoke-CatchActions} } $authCertStatus = Get-ExchangeAuthCertificateStatus @authCertificateStatusParams @@ -444,7 +457,7 @@ function Main { if ($authCertStatus.ReplaceRequired) { $renewalActionWording = "The Auth Certificate in use must be replaced by a new one." } elseif ($authCertStatus.ConfigureNextAuthRequired) { - $renewalActionWording = "The Auth Certificate configured as next Auth Certificate must be configured or replaced by a new one." + $renewalActionWording = "The Auth Certificate configured as next Auth Certificate must be configured or replaced by a new one or is created on express request." } elseif (($authCertStatus.CurrentAuthCertificateImportRequired) -or ($authCertStatus.NextAuthCertificateImportRequired)) { $renewalActionWording = "The current or next Auth Certificate is missing on some servers and must be imported." @@ -460,7 +473,8 @@ function Main { Write-Host ("Please rerun the script using the '-IgnoreHybridConfig `$true' parameter to perform the renewal action.") -ForegroundColor Yellow Write-Host ("It's also required to run the Hybrid Configuration Wizard (HCW) after the primary Auth Certificate was replaced.") -ForegroundColor Yellow } else { - if (($ValidateAndRenewAuthCertificate) -and + if (($ValidateAndRenewAuthCertificate -or + $EnforceNewAuthCertificateCreation) -and ($renewalActionRequired)) { Write-Host ("Renewal scenario: $($renewalActionWording)") if ($authCertStatus.ReplaceRequired) { diff --git a/docs/Admin/MonitorExchangeAuthCertificate.md b/docs/Admin/MonitorExchangeAuthCertificate.md index f8a4a49351..d36e2412b3 100644 --- a/docs/Admin/MonitorExchangeAuthCertificate.md +++ b/docs/Admin/MonitorExchangeAuthCertificate.md @@ -122,6 +122,7 @@ PS C:\> .\MonitorExchangeAuthCertificate.ps1 -ScriptUpdateOnly Parameter | Description ----------|------------ ValidateAndRenewAuthCertificate | This optional parameter enables the validation and renewal mode which will perform the required actions to replace an invalid/expired Auth Certificate or configures a new next Auth Certificate if the current Auth Certificate expires in < 60 days or the certificate configured as next Auth Certificate expires in < 120 days. +EnforceNewAuthCertificateCreation | This optional switch parameter enforces the creation of a new Auth Certificate, which will become active within 24 hours after creation. CustomCertificateLifetimeInDays | This optional parameter allows you to specify a custom lifetime in days for the new Auth Certificate. IgnoreUnreachableServers | This optional parameter can be used to ignore if some of the Exchange servers within the organization cannot be reached. If this parameter is used, the script only validates the servers that can be reached and will perform Auth Certificate renewal actions based on the result. Parameter can be combined with the `IgnoreHybridConfig` parameter and can also be used with the `ConfigureScriptToRunViaScheduledTask` parameter to configure the script to run via scheduled task. IgnoreHybridConfig | This optional parameter allows you to explicitly perform Auth Certificate renewal actions (if required) even if an Exchange hybrid configuration was detected. You need to run the HCW after the renewed Auth Certificate becomes the one in use. Parameter can be combined with the `IgnoreUnreachableServers` parameter and can also be used with the `ConfigureScriptToRunViaScheduledTask` parameter to configure the script to run via scheduled task. From 3b4b97f1550a01d7c3209762d2e469b7e197acdf Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Fri, 13 Jun 2025 15:20:41 +0200 Subject: [PATCH 4/8] Enable enforced Auth Certificate as soon as possible --- .../Get-ExchangeAuthCertificateStatus.ps1 | 2 +- .../MonitorExchangeAuthCertificate.ps1 | 17 ++++++++++++++++- docs/Admin/MonitorExchangeAuthCertificate.md | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 b/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 index 96738473f8..d433aece20 100644 --- a/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/DataCollection/Get-ExchangeAuthCertificateStatus.ps1 @@ -163,7 +163,7 @@ function Get-ExchangeAuthCertificateStatus { # Scenario 1: Current Auth Certificate has expired and no next Auth Certificate defined or the next Auth Certificate has expired $replaceRequired = $true } elseif (((($currentAuthCertificateValidInDays -ge 0) -and - ($currentAuthCertificateValidInDays -le 60)) -and + ($currentAuthCertificateValidInDays -le 60)) -and (($nextAuthCertificateValidInDays -le 0) -or ($nextAuthCertificateValidInDays -le 120)) -and ($currentAuthCertificateMissingOnServersList.Count -eq 0) -and diff --git a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 index 91c813b4cb..06efd0a7ba 100644 --- a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 @@ -72,6 +72,17 @@ .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -Confirm:$false Runs the script in renewal mode without user interaction. The Auth Certificate renewal action will be performed (if required). In unattended mode the internal SMTP certificate will be replaced with the new Auth Certificate and is then set back to the previous one. + The new Auth Certificate, which is eventually created, will have a lifetime of 5 years. +.EXAMPLE + .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -CustomCertificateLifetimeInDays 365 -Confirm:$false + Runs the script in renewal mode without user interaction. The Auth Certificate renewal action will be performed (if required). + In unattended mode the internal SMTP certificate will be replaced with the new Auth Certificate and is then set back to the previous one. + The new Auth Certificate, which is eventually created, will be created with a lifetime of 365 days (1 year). +.EXAMPLE + .\MonitorExchangeAuthCertificate.ps1 -EnforceNewAuthCertificateCreation -CustomCertificateLifetimeInDays 365 -Confirm:$false + Runs the script in Auth Certificate enforcement mode.A new Auth Certificate is created and staged as new next Auth Certificate. + The Exchange AuthAdmin servicelet will publish the newly created Auth Certificate as soon as it processes it the next time (usually in a 12 hour timeframe). + The new Auth Certificate, which is created, will be created with a lifetime of 365 days (1 year). .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -IgnoreUnreachableServers $true -Confirm:$false Runs the script in renewal mode without user interaction. We only take the Exchange server into account which are reachable and will perform @@ -416,6 +427,8 @@ function Main { if ($ValidateAndRenewAuthCertificate) { Write-Host ("Mode: Testing and replacing or importing the Auth Certificate (if required)") + } else { + Write-Host ("Mode: Enforce new next Auth Certificate creation") } else { Write-Host ("The script was run without parameter therefore, only a check of the Auth Certificate configuration is performed and no change will be made") } @@ -489,10 +502,12 @@ function Main { $emailBodyRenewalScenario = "The Auth Certificate in use was invalid (expired) or not available on all Exchange Servers within your organization.
" + "It was immediately replaced by a new one which is already active.

" } elseif ($authCertStatus.ConfigureNextAuthRequired) { + # Set CurrentAuthCertificateLifetimeInDays to 2 in case that EnforceNewAuthCertificateCreation was used + # We do that to ensure that the new Auth Certificate will become next time the AuthAdmin servicelet processess it $configureNextAuthCertificateParams = @{ ConfigureNextAuthCertificate = $true NewAuthCertificateLifetimeInDays = $CustomCertificateLifetimeInDays - CurrentAuthCertificateLifetimeInDays = $authCertStatus.CurrentAuthCertificateLifetimeInDays + CurrentAuthCertificateLifetimeInDays = if ($EnforceNewAuthCertificateCreation) { 2 } else { $authCertStatus.CurrentAuthCertificateLifetimeInDays } CatchActionFunction = ${Function:Invoke-CatchActions} WhatIf = $WhatIfPreference } diff --git a/docs/Admin/MonitorExchangeAuthCertificate.md b/docs/Admin/MonitorExchangeAuthCertificate.md index d36e2412b3..39bedf5142 100644 --- a/docs/Admin/MonitorExchangeAuthCertificate.md +++ b/docs/Admin/MonitorExchangeAuthCertificate.md @@ -103,6 +103,12 @@ The following syntax executes the script in scheduled task mode, but it doesn't PS C:\> .\MonitorExchangeAuthCertificate.ps1 -ConfigureScriptToRunViaScheduledTask -AutomationAccountCredential (Get-Credential) -SendEmailNotificationTo "John.Doe@contoso.com" ``` +The following syntax runs the script in Auth Certificate enforcement mode. In this mode, the script creates a new Auth Certificate (in this example, with a custom lifetime of 1 year) and stages it as the new next Auth Certificate. An existing new next Auth Certificate will be overwritten by the new one. The new Auth Certificate will become active as soon as the Exchange AuthAdmin servicelet processess it the next time (usually within a 12 hour timeframe). + +```powershell +PS C:\> .\MonitorExchangeAuthCertificate.ps1 -EnforceNewAuthCertificateCreation -CustomCertificateLifetimeInDays 365 -Confirm:$false +``` + The following syntax runs the script in Auth Certificate export mode. In this mode, the script exports the current and (if configured) the next Auth Certificate as DER (Distinguished Encoding Rules) binary encoded `PKCS #12` files, using the `.pfx` file name extension. The naming syntax for the exported .pfx file is: `-.pfx` From 7d13e5b5118fe34e61437bb7786e3878744a9a63 Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Fri, 13 Jun 2025 20:07:06 +0200 Subject: [PATCH 5/8] Some more adjustments --- .../MonitorExchangeAuthCertificate.ps1 | 2 +- .../New-ExchangeSelfSignedCertificate.ps1 | 56 +++++++++++-------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 index 06efd0a7ba..a33babffb0 100644 --- a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 @@ -427,7 +427,7 @@ function Main { if ($ValidateAndRenewAuthCertificate) { Write-Host ("Mode: Testing and replacing or importing the Auth Certificate (if required)") - } else { + } elseif ($EnforceNewAuthCertificateCreation) { Write-Host ("Mode: Enforce new next Auth Certificate creation") } else { Write-Host ("The script was run without parameter therefore, only a check of the Auth Certificate configuration is performed and no change will be made") diff --git a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 index 99ecaafaad..303cadc7e7 100644 --- a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 +++ b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 @@ -66,6 +66,10 @@ function New-ExchangeSelfSignedCertificate { $hashAlgorithmName = [System.Security.Cryptography.HashAlgorithmName]::new($HashAlgorithm) Write-Verbose "HashAlgorithm: $($hashAlgorithmName.Name)" + # Generate a unique name for the key container + $keyContainerName = "MonitorExchangeAuthCertificate_$((New-Guid).Guid.ToString())" + Write-Verbose "Key container name is: $keyContainerName" + if ($AlgorithmType -eq "ECC") { Write-Verbose "ECC-based certificate will be created" @@ -108,7 +112,7 @@ function New-ExchangeSelfSignedCertificate { $cspParams.Flags = [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore $cspParams.ProviderType = 24 # PROV_RSA_FULL $cspParams.KeyNumber = 1 # AT_KEYEXCHANGE - $cspParams.KeyContainerName = (New-Guid).Guid.ToString() + $cspParams.KeyContainerName = $keyContainerName #cspell:enable Write-Verbose "Generating the public/private RSA key pair..." @@ -118,7 +122,7 @@ function New-ExchangeSelfSignedCertificate { $cspParams ) - # Ensure the RSA private key persists beyond the current session + # Ensure the RSA private key persists beyond the current session (stores the key in the cryptographic service provider container) $rsa.PersistKeyInCsp = $true } else { Write-Verbose "Initializing the CngKeyCreationParameters..." @@ -131,9 +135,6 @@ function New-ExchangeSelfSignedCertificate { $cngKeyCreationParameters.KeyCreationOptions = [System.Security.Cryptography.CngKeyCreationOptions]::OverwriteExistingKey $cngKeyCreationParameters.ExportPolicy = [System.Security.Cryptography.CngExportPolicies]::AllowExport - # Generate a unique name for the key as we can't create it as an ephemeral key for whatever reason - $cngKeyName = (New-Guid).Guid.ToString() - # Add RSA-specific CngProperty for the key size Write-Verbose "RSA key size: $KeySize" $cngKeyLengthProperty = [System.Security.Cryptography.CngProperty]::new( @@ -145,15 +146,15 @@ function New-ExchangeSelfSignedCertificate { Write-Verbose "Adding RSA-specific KeyLength property" $cngKeyCreationParameters.Parameters.Add($cngKeyLengthProperty) - # Creates a named CngKey object that provides the specified algorithm and keyName making the key persistent, using the supplied key creation parameter + # Create a new RSA key pair and store it in the CNG key store with the specified parameters Write-Verbose "Creating the RSA-based CngKey..." $cngKey = [System.Security.Cryptography.CngKey]::Create( - [System.Security.Cryptography.CngAlgorithm]::Rsa, - $cngKeyName, - $cngKeyCreationParameters + [System.Security.Cryptography.CngAlgorithm]::Rsa, # Specifies RSA algorithm + $keyContainerName, # Name of the key container + $cngKeyCreationParameters # Creation options ) - # Generate the public/private RSA key pair + # Wrap the existing CNG key in an RSACng object for cryptographic operations Write-Verbose "Generating the public/private RSA key pair..." $rsa = [System.Security.Cryptography.RSACng]::new($cngKey) } @@ -254,7 +255,9 @@ function New-ExchangeSelfSignedCertificate { $certificate.FriendlyName = $utf8FriendlyName } - Write-Verbose "Certificate was created successfully. Thumbprint: $($certificate.Thumbprint)" + $certificateThumbprint = $certificate.Thumbprint + + Write-Verbose "Certificate was created successfully - Thumbprint: $certificateThumbprint Subject: $($subject.Name)" } catch { Write-Host "Something went wrong while creating the self-signed certificate. Exception: $_" -ForegroundColor Red @@ -319,26 +322,33 @@ function New-ExchangeSelfSignedCertificate { } } } end { - if ($null -ne $cngKeyName) { - Write-Verbose "Deleting CngKey object..." - ([System.Security.Cryptography.CngKey]::Open($cngKeyName)).Delete() + if ($null -ne $certificate) { + Write-Verbose "Disposing X509Certificate2 object..." + # Call Dispose() to release all resources used by the X509Certificate object + $certificate.Dispose() + } + + if ($null -ne $rsa) { + Write-Verbose "Clearing and disposing RSA key object..." + # Call Clear() to release resources and delete the key from the container + $rsa.Clear() } if ($null -ne $ecdsa) { - # Dispose the ECC key - Write-Verbose "Disposing ECDsa key object..." - $ecdsa.Dispose() + # Call Clear() to release resources and delete the key from the container + Write-Verbose "Clearing and disposing ECDsa key object..." + $ecdsa.Clear() } - if ($null -ne $rsa) { - # Dispose the RSA key - Write-Verbose "Disposing RSA key object..." - $rsa.Dispose() + if ($null -ne $cngKey) { + # Call Delete() to remove the key that is associated with the object + Write-Verbose "Deleting CngKey object..." + $cngKey.Delete() } return [PSCustomObject]@{ - Subject = $certificate.Subject - Thumbprint = $certificate.Thumbprint + Subject = $subject.Name + Thumbprint = $certificateThumbprint } } } From 95318eda3d18edf63645f1ea8409109b87e2ce9c Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Mon, 16 Jun 2025 14:21:47 +0200 Subject: [PATCH 6/8] Fix typos --- .../MonitorExchangeAuthCertificate.ps1 | 4 ++-- .../New-ExchangeSelfSignedCertificate.ps1 | 7 +++---- docs/Admin/MonitorExchangeAuthCertificate.md | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 index a33babffb0..f90205cc0c 100644 --- a/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 +++ b/Admin/MonitorExchangeAuthCertificate/MonitorExchangeAuthCertificate.ps1 @@ -81,7 +81,7 @@ .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -EnforceNewAuthCertificateCreation -CustomCertificateLifetimeInDays 365 -Confirm:$false Runs the script in Auth Certificate enforcement mode.A new Auth Certificate is created and staged as new next Auth Certificate. - The Exchange AuthAdmin servicelet will publish the newly created Auth Certificate as soon as it processes it the next time (usually in a 12 hour timeframe). + The Exchange AuthAdmin servicelet will publish the newly created Auth Certificate as soon as it processes it the next time (usually in a 12 hour time frame). The new Auth Certificate, which is created, will be created with a lifetime of 365 days (1 year). .EXAMPLE .\MonitorExchangeAuthCertificate.ps1 -ValidateAndRenewAuthCertificate $true -IgnoreUnreachableServers $true -Confirm:$false @@ -503,7 +503,7 @@ function Main { "It was immediately replaced by a new one which is already active.

" } elseif ($authCertStatus.ConfigureNextAuthRequired) { # Set CurrentAuthCertificateLifetimeInDays to 2 in case that EnforceNewAuthCertificateCreation was used - # We do that to ensure that the new Auth Certificate will become next time the AuthAdmin servicelet processess it + # We do that to ensure that the new Auth Certificate will become active next time the AuthAdmin servicelet processes it $configureNextAuthCertificateParams = @{ ConfigureNextAuthCertificate = $true NewAuthCertificateLifetimeInDays = $CustomCertificateLifetimeInDays diff --git a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 index 303cadc7e7..8ad62f0faf 100644 --- a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 +++ b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 @@ -68,7 +68,6 @@ function New-ExchangeSelfSignedCertificate { # Generate a unique name for the key container $keyContainerName = "MonitorExchangeAuthCertificate_$((New-Guid).Guid.ToString())" - Write-Verbose "Key container name is: $keyContainerName" if ($AlgorithmType -eq "ECC") { Write-Verbose "ECC-based certificate will be created" @@ -199,7 +198,7 @@ function New-ExchangeSelfSignedCertificate { $keyUsageExtensions = [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new( [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature -bor # DigitalSignature: The certificate's public key can be used to verify digital signatures [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment, # KeyEncipherment: The public key can also be used to encrypt symmetric keys - $true # critical: marked as critical + $true # critical? marked as critical ) $certificateRequest.CertificateExtensions.Add($keyUsageExtensions) @@ -210,7 +209,7 @@ function New-ExchangeSelfSignedCertificate { $extendedKeyUsageExtension = [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new( $oids, # OID for Server Authentication - $false # not critical: marked as not critical + $false # critical? marked as not critical ) $certificateRequest.CertificateExtensions.Add($extendedKeyUsageExtension) @@ -220,7 +219,7 @@ function New-ExchangeSelfSignedCertificate { $false, # certificateAuthority: this is not a CA $false, # hasPathLengthConstraint: we don't want to enforce one 0, # pathLengthConstraint: ignored since hasPathLengthConstraint is false - $true # critical: marked as critical + $true # critical? marked as critical ) $certificateRequest.CertificateExtensions.Add($basicConstraints) diff --git a/docs/Admin/MonitorExchangeAuthCertificate.md b/docs/Admin/MonitorExchangeAuthCertificate.md index 39bedf5142..0490d0a54e 100644 --- a/docs/Admin/MonitorExchangeAuthCertificate.md +++ b/docs/Admin/MonitorExchangeAuthCertificate.md @@ -103,7 +103,7 @@ The following syntax executes the script in scheduled task mode, but it doesn't PS C:\> .\MonitorExchangeAuthCertificate.ps1 -ConfigureScriptToRunViaScheduledTask -AutomationAccountCredential (Get-Credential) -SendEmailNotificationTo "John.Doe@contoso.com" ``` -The following syntax runs the script in Auth Certificate enforcement mode. In this mode, the script creates a new Auth Certificate (in this example, with a custom lifetime of 1 year) and stages it as the new next Auth Certificate. An existing new next Auth Certificate will be overwritten by the new one. The new Auth Certificate will become active as soon as the Exchange AuthAdmin servicelet processess it the next time (usually within a 12 hour timeframe). +The following syntax runs the script in Auth Certificate enforcement mode. In this mode, the script creates a new Auth Certificate (in this example, with a custom lifetime of 1 year) and stages it as the new next Auth Certificate. An existing new next Auth Certificate will be overwritten by the new one. The new Auth Certificate will become active as soon as the Exchange AuthAdmin servicelet processes it the next time (usually within a 12 hour timeframe). ```powershell PS C:\> .\MonitorExchangeAuthCertificate.ps1 -EnforceNewAuthCertificateCreation -CustomCertificateLifetimeInDays 365 -Confirm:$false From 3b168923377101c8a316be59d76de684024c42af Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Tue, 17 Jun 2025 12:48:16 +0200 Subject: [PATCH 7/8] Support SupportsShouldProcess --- .../New-ExchangeSelfSignedCertificate.ps1 | 130 +++++++++++------- 1 file changed, 83 insertions(+), 47 deletions(-) diff --git a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 index 8ad62f0faf..3c0b334aeb 100644 --- a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 +++ b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 @@ -4,8 +4,7 @@ . $PSScriptRoot\..\Confirm-Administrator.ps1 function New-ExchangeSelfSignedCertificate { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Certificate creation is intentional and controlled')] - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateScript({ $_.Length -lt 64 })] [string]$SubjectName = $env:COMPUTERNAME, @@ -81,7 +80,9 @@ function New-ExchangeSelfSignedCertificate { try { Write-Verbose "Generating key by using $CurveName curve" - $ecdsa.GenerateKey($curve) + if ($PSCmdlet.ShouldProcess("Generating private key ($AlgorithmType)")) { + $ecdsa.GenerateKey($curve) + } # Generate the ECC CertificateRequest Write-Verbose "Generating the ECC CertificateRequest..." @@ -116,13 +117,15 @@ function New-ExchangeSelfSignedCertificate { Write-Verbose "Generating the public/private RSA key pair..." # Initializes a new instance of RSACryptoServiceProvider to generate a new key pair, pass KeySize and CspParameters - $rsa = [System.Security.Cryptography.RSACryptoServiceProvider]::new( - $KeySize, - $cspParams - ) - - # Ensure the RSA private key persists beyond the current session (stores the key in the cryptographic service provider container) - $rsa.PersistKeyInCsp = $true + if ($PSCmdlet.ShouldProcess("Generating private key ($AlgorithmType)")) { + $rsa = [System.Security.Cryptography.RSACryptoServiceProvider]::new( + $KeySize, + $cspParams + ) + + # Ensure the RSA private key persists beyond the current session (stores the key in the cryptographic service provider container) + $rsa.PersistKeyInCsp = $true + } } else { Write-Verbose "Initializing the CngKeyCreationParameters..." @@ -155,19 +158,23 @@ function New-ExchangeSelfSignedCertificate { # Wrap the existing CNG key in an RSACng object for cryptographic operations Write-Verbose "Generating the public/private RSA key pair..." - $rsa = [System.Security.Cryptography.RSACng]::new($cngKey) + if ($PSCmdlet.ShouldProcess("Generating private key ($AlgorithmType)")) { + $rsa = [System.Security.Cryptography.RSACng]::new($cngKey) + } } try { Write-Verbose "Generating the RSA CertificateRequest..." # Initializes a new instance of the CertificateRequest class using the specified subject name, RSA key, hash algorithm, and using PKCS #1 v1.5 padding - $certificateRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( - $subject, - $rsa, - $hashAlgorithmName, - [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 - ) + if ($PSCmdlet.ShouldProcess("Generating RSA certificate request")) { + $certificateRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( + $subject, + $rsa, + $hashAlgorithmName, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + } } catch { Write-Host "Something went wrong while creating the CertificateRequest. Exception $_" -ForegroundColor Red @@ -186,9 +193,11 @@ function New-ExchangeSelfSignedCertificate { $sanBuilder.AddDnsName($name) } - $certificateRequest.CertificateExtensions.Add( - $sanBuilder.Build($true) - ) + if ($PSCmdlet.ShouldProcess("Adding $($DomainName.Count) DnsName(s) to SAN extension")) { + $certificateRequest.CertificateExtensions.Add( + $sanBuilder.Build($true) + ) + } } try { @@ -201,7 +210,9 @@ function New-ExchangeSelfSignedCertificate { $true # critical? marked as critical ) - $certificateRequest.CertificateExtensions.Add($keyUsageExtensions) + if ($PSCmdlet.ShouldProcess("Adding the X509KeyUsageExtension")) { + $certificateRequest.CertificateExtensions.Add($keyUsageExtensions) + } # Specify the X509EnhancedKeyUsageExtension $oids = [System.Security.Cryptography.OidCollection]::new() @@ -212,7 +223,9 @@ function New-ExchangeSelfSignedCertificate { $false # critical? marked as not critical ) - $certificateRequest.CertificateExtensions.Add($extendedKeyUsageExtension) + if ($PSCmdlet.ShouldProcess("Adding the X509EnhancedKeyUsageExtension")) { + $certificateRequest.CertificateExtensions.Add($extendedKeyUsageExtension) + } # Specify the X509BasicConstraintsExtension $basicConstraints = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new( @@ -222,7 +235,9 @@ function New-ExchangeSelfSignedCertificate { $true # critical? marked as critical ) - $certificateRequest.CertificateExtensions.Add($basicConstraints) + if ($PSCmdlet.ShouldProcess("Adding the X509BasicConstraintsExtension")) { + $certificateRequest.CertificateExtensions.Add($basicConstraints) + } # Add the Subject Key Identifier (SKI) as a non-critical extensions if AddSubjectKeyIdentifier parameter was set to true if ($AddSubjectKeyIdentifier) { @@ -231,7 +246,9 @@ function New-ExchangeSelfSignedCertificate { $false ) - $certificateRequest.CertificateExtensions.Add($subjectKeyIdentifier) + if ($PSCmdlet.ShouldProcess("Adding Subject Key Identifier (SKI)")) { + $certificateRequest.CertificateExtensions.Add($subjectKeyIdentifier) + } } } catch { Write-Host "Something went wrong while processing certificate extensions. Exception: $_" -ForegroundColor Red @@ -245,17 +262,29 @@ function New-ExchangeSelfSignedCertificate { $notBefore = [System.DateTimeOffset]::UtcNow $notAfter = $notBefore.AddDays($LifetimeInDays) - $certificate = $certificateRequest.CreateSelfSigned( - $notBefore, - $notAfter - ) + if ($PSCmdlet.ShouldProcess("Creating self-signed certificate for '$($subject.Name)'")) { + $certificate = $certificateRequest.CreateSelfSigned( + $notBefore, + $notAfter + ) + } if (-not([System.String]::IsNullOrEmpty($utf8FriendlyName))) { - $certificate.FriendlyName = $utf8FriendlyName + if ($PSCmdlet.ShouldProcess("Adding FriendlyName $utf8FriendlyName")) { + certificate.FriendlyName = $utf8FriendlyName + } } $certificateThumbprint = $certificate.Thumbprint + + if ($PSCmdlet.ShouldProcess("Setting certificate thumbprint")) { + $certificateThumbprint = $certificate.Thumbprint + } else { + # Mock certificate thumbprint + $certificateThumbprint = "A1B2C3D4E5F60718293A4B5C6D7E8F9012345678" + } + Write-Verbose "Certificate was created successfully - Thumbprint: $certificateThumbprint Subject: $($subject.Name)" } catch { Write-Host "Something went wrong while creating the self-signed certificate. Exception: $_" -ForegroundColor Red @@ -266,18 +295,19 @@ function New-ExchangeSelfSignedCertificate { try { # To make the certificate and its private key exportable, we must export and re-import it with the Exportable flag Write-Verbose "Exporting and re-importing certificate with Exportable flag to make it exportable..." - - $pfxBytes = $certificate.Export( - [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx - ) - $certificateWithExportableKey = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new() - $certificateWithExportableKey.Import( - $pfxBytes, - $null, - ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bor - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet) - ) + if ($PSCmdlet.ShouldProcess("Making certificate exportable")) { + $pfxBytes = $certificate.Export( + [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx + ) + $certificateWithExportableKey = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new() + $certificateWithExportableKey.Import( + $pfxBytes, + $null, + ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet) + ) + } # Add it to the LocalMachine store Write-Verbose "Adding the certificate to the My/LocalMachine certificate store..." @@ -286,9 +316,12 @@ function New-ExchangeSelfSignedCertificate { "My", "LocalMachine" ) - $machineStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) - $machineStore.Add($certificateWithExportableKey) - $machineStore.Close() + + if ($PSCmdlet.ShouldProcess("Adding certificate to LocalMachine\My store")) { + $machineStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $machineStore.Add($certificateWithExportableKey) + $machineStore.Close() + } # Add the certificate to the Trusted Root Certification Authorities if explicitly specified via TrustCertificate parameter if ($TrustCertificate) { @@ -298,9 +331,12 @@ function New-ExchangeSelfSignedCertificate { "Root", "LocalMachine" ) - $trustedRootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) - $trustedRootStore.Add($certificateWithExportableKey) - $trustedRootStore.Close() + + if ($TrustCertificate -and $PSCmdlet.ShouldProcess("Adding certificate to LocalMachine\Root store")) { + $trustedRootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $trustedRootStore.Add($certificateWithExportableKey) + $trustedRootStore.Close() + } } } catch { Write-Host "Something went wrong while adding the certificate to the store. Exception: $_" -ForegroundColor Red From 8e966c807cdbda777895ba2521df7bef068d7e10 Mon Sep 17 00:00:00 2001 From: Lukas Sassl Date: Tue, 17 Jun 2025 13:04:21 +0200 Subject: [PATCH 8/8] Typo fixed --- .../CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 index 3c0b334aeb..1d9f37e497 100644 --- a/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 +++ b/Shared/CertificateFunctions/New-ExchangeSelfSignedCertificate.ps1 @@ -271,7 +271,7 @@ function New-ExchangeSelfSignedCertificate { if (-not([System.String]::IsNullOrEmpty($utf8FriendlyName))) { if ($PSCmdlet.ShouldProcess("Adding FriendlyName $utf8FriendlyName")) { - certificate.FriendlyName = $utf8FriendlyName + $certificate.FriendlyName = $utf8FriendlyName } }