Public/New-ZTAssessCertificate.ps1
|
#Requires -Version 7.0 function New-ZTAssessCertificate { <# .SYNOPSIS Creates a self-signed certificate for EntraZTAssess app-only authentication. .DESCRIPTION Generates a self-signed X.509 certificate using pure .NET cryptography so the function behaves identically on macOS, Linux, and Windows (it does not depend on the Windows-only New-SelfSignedCertificate cmdlet). Two artifacts are produced: * A DER-encoded public certificate (.cer) that is uploaded to the Entra ID app registration by New-ZTAssessAppRegistration. * A password-protected PKCS#12 archive (.pfx) that holds the private key. The .pfx is the cross-platform private-key artifact used by Connect-ZTAssessment for certificate-based authentication on macOS and Linux, which have no reliable certificate store. This is a one-time, admin-run setup step. It performs no network calls and writes nothing sensitive to disk beyond the password-protected .pfx. The PFX password is never written to disk or emitted to logs. On macOS and Linux the output directory and .pfx file are restricted to the owner (0700/0600) after they are written. .PARAMETER SubjectName The X.500 distinguished name (subject) of the certificate. Defaults to 'CN=EntraZTAssess'. .PARAMETER ValidityMonths The number of months the certificate remains valid, from 1 to 60. Defaults to 24. .PARAMETER OutputPath The directory that receives EntraZTAssess.cer and EntraZTAssess.pfx. Created if it does not exist. Defaults to the .ztassess folder under the current user's home directory. .PARAMETER PfxPassword The password protecting the exported .pfx private key, supplied as a SecureString. If omitted, the function prompts for it securely. The password is never persisted. .PARAMETER InstallToWindowsStore On Windows only, also imports the certificate (with its private key) into the CurrentUser\My certificate store so Connect-ZTAssessment can authenticate by thumbprint. Ignored with a warning on macOS and Linux. .EXAMPLE New-ZTAssessCertificate Creates EntraZTAssess.cer and EntraZTAssess.pfx under ~/.ztassess, prompting for the PFX password. .EXAMPLE $pw = Read-Host -AsSecureString 'PFX password' New-ZTAssessCertificate -SubjectName 'CN=Contoso ZTAssess' -ValidityMonths 12 -PfxPassword $pw Creates a 12-month certificate with a custom subject using a pre-collected SecureString password. .EXAMPLE New-ZTAssessCertificate -InstallToWindowsStore On Windows, also installs the certificate into CurrentUser\My so it can be referenced by thumbprint alone. .OUTPUTS PSCustomObject An object with Thumbprint, Subject, NotAfter, CerPath, PfxPath, and Platform. .NOTES Upload the resulting .cer with New-ZTAssessAppRegistration. Keep the .pfx and its password safe; anyone holding both can authenticate as the assessment application. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive admin-run provisioning function; coloured console guidance is intentional and not pipeline output.')] [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$SubjectName = 'CN=EntraZTAssess', [Parameter()] [ValidateRange(1, 60)] [int]$ValidityMonths = 24, [Parameter()] [ValidateNotNullOrEmpty()] [string]$OutputPath = (Join-Path -Path $HOME -ChildPath '.ztassess'), [Parameter()] [securestring]$PfxPassword, [Parameter()] [switch]$InstallToWindowsStore ) $ErrorActionPreference = 'Stop' # Prompt for the PFX password only when the caller did not supply one. if (-not $PfxPassword) { $PfxPassword = Read-Host -AsSecureString -Prompt 'Enter a password to protect the exported .pfx private key' } if (-not $PfxPassword -or $PfxPassword.Length -eq 0) { throw 'A non-empty PFX password is required to protect the exported private key.' } # Ensure the output directory exists before exporting. if (-not (Test-Path -LiteralPath $OutputPath)) { if ($PSCmdlet.ShouldProcess($OutputPath, 'Create output directory')) { $null = New-Item -Path $OutputPath -ItemType Directory -Force } } # Restrict the output directory to the owner on macOS/Linux, since it holds # a password-protected private key archive. SetUnixFileMode throws on # Windows, so it is only ever attempted off-Windows. if ((Test-Path -LiteralPath $OutputPath) -and -not (Test-ZTAssessIsWindowsPlatform)) { $ownerRwx = [System.IO.UnixFileMode]'UserRead, UserWrite, UserExecute' Invoke-ZTAssessSetUnixFileMode -Path $OutputPath -Mode $ownerRwx } $cerPath = Join-Path -Path $OutputPath -ChildPath 'EntraZTAssess.cer' $pfxPath = Join-Path -Path $OutputPath -ChildPath 'EntraZTAssess.pfx' # Build the key pair and self-signed certificate with pure .NET so the flow is # identical on macOS, Linux, and Windows. $rsa = [System.Security.Cryptography.RSA]::Create(2048) try { $certRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( $SubjectName, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 ) $notBefore = [System.DateTimeOffset]::UtcNow.AddMinutes(-5) $notAfter = [System.DateTimeOffset]::UtcNow.AddMonths($ValidityMonths) $certificate = $certRequest.CreateSelfSigned($notBefore, $notAfter) } finally { $rsa.Dispose() } try { # Export the DER-encoded public certificate (.cer) for upload to the app. if ($PSCmdlet.ShouldProcess($cerPath, 'Write public certificate (.cer)')) { $cerBytes = $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) [System.IO.File]::WriteAllBytes($cerPath, $cerBytes) } # Export the password-protected private key archive (.pfx). The SecureString # overload keeps the plaintext password from ever materializing in memory. if ($PSCmdlet.ShouldProcess($pfxPath, 'Write password-protected private key (.pfx)')) { $pfxBytes = $certificate.Export( [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $PfxPassword ) [System.IO.File]::WriteAllBytes($pfxPath, $pfxBytes) # Restrict the .pfx to the owner on macOS/Linux; it holds the private key. if (-not (Test-ZTAssessIsWindowsPlatform)) { $ownerRw = [System.IO.UnixFileMode]'UserRead, UserWrite' Invoke-ZTAssessSetUnixFileMode -Path $pfxPath -Mode $ownerRw } } # On Windows only, optionally import into the user's personal store so the # certificate can be referenced by thumbprint alone. if ($InstallToWindowsStore) { if (Test-ZTAssessIsWindowsPlatform) { if (-not $pfxBytes) { Write-Warning 'InstallToWindowsStore requires the .pfx to have been exported first; it was skipped (likely due to -WhatIf). Store import was not performed.' } elseif ($PSCmdlet.ShouldProcess('Cert:\CurrentUser\My', 'Install certificate to Windows store')) { # $certificate's private key is ephemeral (from CreateSelfSigned) and # is not guaranteed to persist through X509Store.Add on every .NET/OS # combination. Round-trip it through the PFX bytes just exported, with # PersistKeySet + Exportable, so the store copy reliably keeps a usable # private key for certificate-based authentication by thumbprint. $persistableCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $pfxBytes, $PfxPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ) $store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'CurrentUser') try { $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) $store.Add($persistableCertificate) } finally { $store.Close() $persistableCertificate.Dispose() } } } else { Write-Warning 'InstallToWindowsStore was requested but this is not Windows. Skipping store import; use the .pfx for certificate-based authentication instead.' } } $platformNote = if (Test-ZTAssessIsWindowsPlatform) { 'Windows: reference the certificate by thumbprint (store) or by the .pfx.' } else { 'macOS/Linux: use the .pfx with its password for certificate-based authentication.' } $result = [pscustomobject]@{ PSTypeName = 'ZTAssess.Certificate' Thumbprint = $certificate.Thumbprint Subject = $certificate.Subject NotAfter = $certificate.NotAfter CerPath = $cerPath PfxPath = $pfxPath Platform = $platformNote } Write-Host '' Write-Host 'Certificate created.' -ForegroundColor Green Write-Host (' Thumbprint : {0}' -f $result.Thumbprint) Write-Host (' Subject : {0}' -f $result.Subject) Write-Host (' Expires : {0:yyyy-MM-dd}' -f $result.NotAfter) Write-Host (' Public cer : {0}' -f $result.CerPath) Write-Host (' Private pfx: {0}' -f $result.PfxPath) Write-Host '' Write-Host 'Next step: register the application and upload the public certificate:' -ForegroundColor Cyan Write-Host (" New-ZTAssessAppRegistration -TenantId <tenant> -CertificatePath '{0}'" -f $result.CerPath) Write-Host '' return $result } finally { $certificate.Dispose() } } |