New-SecureStoreCertificate.ps1
|
<# .SYNOPSIS Creates a password-protected self-signed certificate within SecureStore. .DESCRIPTION New-SecureStoreCertificate provisions RSA or ECDSA self-signed certificates, exporting a PFX protected by the supplied password and optionally a PEM. It supports SAN and EKU entries, ensures atomic writes, and can store certificates in the Windows certificate store. .PARAMETER CertificateName Logical name used for the output PFX/PEM files or certificate friendly name. .PARAMETER Password Password protecting the exported PFX. Accepts plain text or SecureString. .PARAMETER FolderPath Base SecureStore path containing the certs directory. Defaults to the module's standard path. .PARAMETER ValidityYears Number of years the certificate remains valid. .PARAMETER Subject Optional X.500 subject name. Defaults to CN=<CertificateName> when omitted. .PARAMETER Algorithm Certificate key algorithm. Supports RSA or ECDSA. .PARAMETER KeyLength RSA key length in bits. Ignored for ECDSA certificates. .PARAMETER CurveName ECDSA curve name. Ignored for RSA certificates. .PARAMETER DnsName Optional DNS subject alternative names. .PARAMETER IpAddress Optional IP subject alternative names. .PARAMETER Email Optional email subject alternative names. .PARAMETER Uri Optional URI subject alternative names. .PARAMETER EnhancedKeyUsage Optional EKU list to embed within the certificate. .PARAMETER ExportPem Switch to export a PEM copy alongside the PFX. .PARAMETER StoreOnly Switch to keep certificate in the Windows certificate store without exporting files. .INPUTS System.String, System.Security.SecureString for the Password parameter. .OUTPUTS PSCustomObject with certificate metadata and file paths. .EXAMPLE New-SecureStoreCertificate -CertificateName 'WebApp' -Password 'Sup3rPfx!' -DnsName 'web.local' -ExportPem Creates an RSA certificate stored as WebApp.pfx and WebApp.pem. .EXAMPLE New-SecureStoreCertificate -CertificateName 'Api' -Password 'Pass123' -StoreOnly Creates a certificate and keeps it in Cert:\CurrentUser\My without exporting files. .NOTES PFX export requires a password. PEM export with private key requires PowerShell 7+ or OpenSSL. .LINK Get-SecureStoreList #> function New-SecureStoreCertificate { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'The parameter accepts SecureString and string for compatibility with existing automation; values are converted to SecureString before use.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'Export')] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$CertificateName, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object]$Password, [Parameter(ParameterSetName = 'Export')] [ValidateNotNullOrEmpty()] [string]$FolderPath = $script:DefaultSecureStorePath, [Parameter()] [ValidateRange(1, 50)] [int]$ValidityYears = 1, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Subject, [Parameter()] [ValidateSet('RSA', 'ECDSA')] [string]$Algorithm = 'RSA', [Parameter()] [ValidateRange(256, 8192)] [int]$KeyLength, [Parameter()] [ValidateSet('nistP256', 'nistP384', 'nistP521')] [string]$CurveName, [Parameter()] [string[]]$DnsName, [Parameter()] [string[]]$IpAddress, [Parameter()] [string[]]$Email, [Parameter()] [string[]]$Uri, [Parameter()] [string[]]$EnhancedKeyUsage = @('1.3.6.1.5.5.7.3.1'), [Parameter(ParameterSetName = 'Export')] [switch]$ExportPem, [Parameter(ParameterSetName = 'StoreOnly')] [switch]$StoreOnly ) begin { if (-not (Get-Command -Name 'Sync-SecureStoreWorkingDirectory' -ErrorAction SilentlyContinue)) { . "$PSScriptRoot/Sync-SecureStoreWorkingDirectory.ps1" } } process { $securePassword = $null $certificate = $null $pfxPath = $null $pemPath = $null try { # Convert the supplied password to SecureString $securePassword = ConvertTo-SecureStoreSecureString -InputObject $Password if ($securePassword.Length -le 0) { throw [System.ArgumentException]::new('Certificate password cannot be empty.') } if (-not $StoreOnly.IsPresent) { $paths = Sync-SecureStoreWorkingDirectory -BasePath $FolderPath $pfxPath = Join-Path -Path $paths.CertsPath -ChildPath ("$CertificateName.pfx") $pemPath = Join-Path -Path $paths.CertsPath -ChildPath ("$CertificateName.pem") if (-not $PSCmdlet.ShouldProcess($pfxPath, "Create certificate '$CertificateName'")) { return } } else { if (-not $PSCmdlet.ShouldProcess("Cert:\CurrentUser\My", "Create certificate '$CertificateName'")) { return } } $subjectName = if ($Subject) { $Subject } else { "CN=$CertificateName" } if ($Algorithm -eq 'RSA') { if (-not $PSBoundParameters.ContainsKey('KeyLength')) { $KeyLength = 3072 } if ($PSBoundParameters.ContainsKey('CurveName')) { throw [System.ArgumentException]::new('CurveName is only applicable when Algorithm is ECDSA.') } } else { if ($PSBoundParameters.ContainsKey('KeyLength')) { throw [System.ArgumentException]::new('KeyLength is only applicable when Algorithm is RSA.') } if (-not $PSBoundParameters.ContainsKey('CurveName')) { $CurveName = 'nistP256' } } $textExtensions = @() $sanComponents = @() if ($DnsName) { $sanComponents += ($DnsName | ForEach-Object { "dns=$_" }) } if ($IpAddress) { $sanComponents += ($IpAddress | ForEach-Object { "ipaddress=$_" }) } if ($Email) { $sanComponents += ($Email | ForEach-Object { "email=$_" }) } if ($Uri) { $sanComponents += ($Uri | ForEach-Object { "url=$_" }) } if ($sanComponents.Count -gt 0) { $textExtensions += "2.5.29.17={text}$($sanComponents -join '&')" } if ($EnhancedKeyUsage -and $EnhancedKeyUsage.Count -gt 0) { $textExtensions += "2.5.29.37={text}$($EnhancedKeyUsage -join ',')" } $certificateParams = @{ Subject = $subjectName CertStoreLocation = 'Cert:\CurrentUser\My' NotAfter = (Get-Date).AddYears($ValidityYears) KeyExportPolicy = 'Exportable' HashAlgorithm = 'SHA256' FriendlyName = $CertificateName } if ($Algorithm -eq 'RSA' -and $script:IsWindowsPlatform) { $certificateParams['Provider'] = 'Microsoft Enhanced RSA and AES Cryptographic Provider' } if ($Algorithm -eq 'RSA') { $certificateParams['KeyAlgorithm'] = 'RSA' $certificateParams['KeyLength'] = $KeyLength $certificateParams['KeySpec'] = 'Signature' } else { if ($script:IsWindowsPlatform) { $certificateParams['Provider'] = 'Microsoft Software Key Storage Provider' } $selfSignedParameters = (Get-Command -Name New-SelfSignedCertificate).Parameters if ($selfSignedParameters.ContainsKey('CurveName')) { $certificateParams['KeyAlgorithm'] = 'ECDSA' $certificateParams['CurveName'] = $CurveName if ($selfSignedParameters.ContainsKey('CurveExportPolicy')) { $certificateParams['CurveExportPolicy'] = 'Exact' } } else { $certificateParams['KeyAlgorithm'] = "ECDSA_$CurveName" if ($selfSignedParameters.ContainsKey('CurveExport')) { $certificateParams['CurveExport'] = 'CurveName' } } } if ($textExtensions.Count -gt 0) { $certificateParams['Type'] = 'Custom' $certificateParams['TextExtension'] = $textExtensions } $certificate = New-SelfSignedCertificate @certificateParams # Export or keep in store if ($StoreOnly.IsPresent) { # Certificate stays in store Write-Verbose "Certificate '$CertificateName' created in Cert:\CurrentUser\My\$($certificate.Thumbprint)" } else { # Export to files $tempPfxPath = "$pfxPath.tmp" if (Test-Path -LiteralPath $tempPfxPath) { Remove-Item -LiteralPath $tempPfxPath -Force } try { # Export PFX Export-PfxCertificate -Cert $certificate -FilePath $tempPfxPath -Password $securePassword | Out-Null Move-Item -LiteralPath $tempPfxPath -Destination $pfxPath -Force # Export PEM if requested if ($ExportPem.IsPresent) { # Convert SecureString to plain text temporarily $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) $plainPassword = $null try { $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) # Re-import PFX with exportable flag $pfxCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $pfxPath, $plainPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ) try { # Export certificate portion $certBytes = $pfxCert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) $certPem = "-----BEGIN CERTIFICATE-----`n" + [Convert]::ToBase64String($certBytes, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END CERTIFICATE-----" # Try to export private key (PowerShell 7+ only) $keyPem = $null $keyExported = $false if ($PSVersionTable.PSVersion.Major -ge 7) { # PowerShell 7+ has the export methods try { if ($Algorithm -eq 'RSA') { $key = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($pfxCert) if ($key) { $keyBytes = $key.ExportRSAPrivateKey() $keyPem = "`n-----BEGIN RSA PRIVATE KEY-----`n" + [Convert]::ToBase64String($keyBytes, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END RSA PRIVATE KEY-----" [Array]::Clear($keyBytes, 0, $keyBytes.Length) $keyExported = $true } } else { $key = [System.Security.Cryptography.X509Certificates.ECDsaCertificateExtensions]::GetECDsaPrivateKey($pfxCert) if ($key) { $keyBytes = $key.ExportECPrivateKey() $keyPem = "`n-----BEGIN EC PRIVATE KEY-----`n" + [Convert]::ToBase64String($keyBytes, [System.Base64FormattingOptions]::InsertLineBreaks) + "`n-----END EC PRIVATE KEY-----" [Array]::Clear($keyBytes, 0, $keyBytes.Length) $keyExported = $true } } } catch { Write-Warning "Failed to export private key: $($_.Exception.Message)" } } if (-not $keyExported) { Write-Warning "PEM export: Certificate only (no private key). Private key export requires PowerShell 7+. Use PFX for full functionality or convert with OpenSSL." } # Write PEM file $pemContent = if ($keyPem) { $certPem + $keyPem } else { $certPem } [System.IO.File]::WriteAllText($pemPath, $pemContent, [System.Text.Encoding]::ASCII) [Array]::Clear($certBytes, 0, $certBytes.Length) } finally { $pfxCert.Dispose() } } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) if ($plainPassword) { $chars = $plainPassword.ToCharArray() [Array]::Clear($chars, 0, $chars.Length) } } } } finally { if (Test-Path -LiteralPath $tempPfxPath) { Remove-Item -LiteralPath $tempPfxPath -Force } } # Remove from store after export try { Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue } catch { Write-Verbose "Failed to remove certificate '$($certificate.Thumbprint)' from store: $($_.Exception.Message)" } } [PSCustomObject]@{ CertificateName = $CertificateName Subject = $subjectName Algorithm = $Algorithm KeyLength = if ($Algorithm -eq 'RSA') { $KeyLength } else { $null } CurveName = if ($Algorithm -eq 'ECDSA') { $CurveName } else { $null } Thumbprint = $certificate.Thumbprint NotAfter = $certificate.NotAfter StoreLocation = if ($StoreOnly) { "Cert:\CurrentUser\My\$($certificate.Thumbprint)" } else { $null } Paths = if (-not $StoreOnly) { [PSCustomObject]@{ Pfx = $pfxPath Pem = if ($ExportPem) { $pemPath } else { $null } } } else { $null } } } catch { throw [System.InvalidOperationException]::new("Failed to create certificate '$CertificateName'.", $_.Exception) } finally { if ($securePassword) { $securePassword.Dispose() } if ($certificate -and ($certificate -is [System.IDisposable]) -and -not $StoreOnly.IsPresent) { $certificate.Dispose() } } } } |