Eigenverft.Manifested.Drydock.Certificates.ps1
|
function New-DomainCsr { <# .SYNOPSIS Generates a PKCS#10 certificate signing request (CSR) and a PFX containing the private key (stored inside a temporary self-signed certificate). .DESCRIPTION Creates a new RSA key pair and builds a CSR in PEM format that you can copy/paste into a CA or hosting control panel. Additionally, a PFX file is always created. The PFX contains: - a temporary self-signed certificate, and - the corresponding private key. This PFX is intended to be used later to combine the private key with the provider-issued certificate and chain. .PARAMETER CommonName Required. The primary DNS name of the certificate (for example: eigenverft.com). .PARAMETER DnsNames Optional. Additional DNS names for the Subject Alternative Name (SAN) extension. It is recommended to include the CommonName here as well. .PARAMETER Country Required. Two-letter country code (for example: DE). Case-insensitive; will be normalized to upper case. .PARAMETER State Optional. State or province. .PARAMETER Locality Optional. City or locality. .PARAMETER Organization Optional. Legal organization name. .PARAMETER OrganizationalUnit Optional. Organizational unit. .PARAMETER KeyLength Optional. RSA key size in bits. Default is 2048. .PARAMETER HashAlgorithm Optional. Hash algorithm for the CSR signature. One of SHA256, SHA384, SHA512. .PARAMETER OutputPath Optional. File path where the CSR will be saved. .PARAMETER PfxPath Required. File path where the PFX (containing a temporary self-signed certificate and the private key) will be written. .PARAMETER PfxPassword Required. Plain-text password to protect the PFX file. Handle this value securely in your own code (for example, by not hard-coding it). .PARAMETER CopyToClipboard Optional. If specified, the resulting CSR PEM text is copied to the clipboard. .EXAMPLE New-DomainCsr ` -CommonName "eigenverft.com" ` -DnsNames "eigenverft.com","www.eigenverft.com" ` -Country "DE" ` -State "NRW" ` -Locality "Düsseldorf" ` -Organization "Eigenverft GmbH" ` -OrganizationalUnit "IT" ` -OutputPath ".\eigenverft.csr" ` -PfxPath ".\eigenverft_key.pfx" ` -PfxPassword "ChangeMe!" ` -CopyToClipboard #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$CommonName, [string[]]$DnsNames, [Parameter(Mandatory = $true)] [ValidatePattern('^[A-Za-z]{2}$')] [string]$Country, [string]$State, [string]$Locality, [string]$Organization, [string]$OrganizationalUnit, [ValidateRange(2048, 8192)] [int]$KeyLength = 2048, [ValidateSet("SHA256","SHA384","SHA512")] [string]$HashAlgorithm = "SHA256", [string]$OutputPath, [Parameter(Mandatory = $true)] [string]$PfxPath, [Parameter(Mandatory = $true)] [string]$PfxPassword, [switch]$CopyToClipboard ) # Normalize country code to upper case. $Country = $Country.ToUpperInvariant() # Build X.500 subject distinguished name. $subjectParts = @() $subjectParts += "CN=$CommonName" if ($Organization) { $subjectParts += "O=$Organization" } if ($OrganizationalUnit) { $subjectParts += "OU=$OrganizationalUnit" } if ($Locality) { $subjectParts += "L=$Locality" } if ($State) { $subjectParts += "ST=$State" } if ($Country) { $subjectParts += "C=$Country" } $subjectString = [string]::Join(", ", $subjectParts) $subject = New-Object System.Security.Cryptography.X509Certificates.X500DistinguishedName($subjectString) # Create an RSA key pair. $rsa = [System.Security.Cryptography.RSA]::Create($KeyLength) $hashName = [System.Security.Cryptography.HashAlgorithmName]::$HashAlgorithm $padding = [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 $request = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( $subject, $rsa, $hashName, $padding ) # Basic constraints: end-entity, not a CA. $basicConstraints = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new( $false, # certificateAuthority $false, # hasPathLengthConstraint 0, # pathLengthConstraint (ignored here) $true # critical ) $null = $request.CertificateExtensions.Add($basicConstraints) # Key usage extension: digital signature + key encipherment. $keyUsageFlags = [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature ` -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment $keyUsage = [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new( $keyUsageFlags, $true # critical ) $null = $request.CertificateExtensions.Add($keyUsage) # Enhanced key usage: server authentication. $ekuOids = New-Object System.Security.Cryptography.OidCollection $null = $ekuOids.Add( [System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.1", "Server Authentication") ) $eku = [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new( $ekuOids, $false # not critical ) $null = $request.CertificateExtensions.Add($eku) # SAN extension. if ($DnsNames -and $DnsNames.Count -gt 0) { $sanBuilder = [System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder]::new() foreach ($dns in $DnsNames) { if (-not [string]::IsNullOrWhiteSpace($dns)) { $sanBuilder.AddDnsName($dns.Trim()) } } $sanExtension = $sanBuilder.Build() $null = $request.CertificateExtensions.Add($sanExtension) } # Create CSR (PKCS#10). $csrBytes = $request.CreateSigningRequest() $csrBase64 = [System.Convert]::ToBase64String( $csrBytes, [System.Base64FormattingOptions]::InsertLineBreaks ) $csrPem = @( "-----BEGIN CERTIFICATE REQUEST-----" $csrBase64 "-----END CERTIFICATE REQUEST-----" ) -join "`r`n" # Save CSR to file (ASCII encoding for PEM). if ($OutputPath) { $dir = [System.IO.Path]::GetDirectoryName($OutputPath) if (-not [string]::IsNullOrWhiteSpace($dir) -and -not (Test-Path -Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Set-Content -Path $OutputPath -Value $csrPem -NoNewline -Encoding ascii } # Always export private key as PFX with a temporary self-signed certificate. $notBefore = [DateTimeOffset]::Now.AddDays(-1) $notAfter = [DateTimeOffset]::Now.AddYears(1) $tempCert = $request.CreateSelfSigned($notBefore, $notAfter) $pfxBytes = $tempCert.Export( [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $PfxPassword ) $pfxDir = [System.IO.Path]::GetDirectoryName($PfxPath) if (-not [string]::IsNullOrWhiteSpace($pfxDir) -and -not (Test-Path -Path $pfxDir)) { New-Item -ItemType Directory -Path $pfxDir -Force | Out-Null } [System.IO.File]::WriteAllBytes($PfxPath, $pfxBytes) # Copy CSR to clipboard, if requested. if ($CopyToClipboard) { try { if (Get-Command Set-Clipboard -ErrorAction SilentlyContinue) { $csrPem | Set-Clipboard } else { Write-Warning "Set-Clipboard is not available in this session." } } catch { Write-Warning "Failed to copy CSR to clipboard. Error: $($_.Exception.Message)" } } # Output the CSR PEM so it can be copied from the console as well. $csrPem } function New-CertPfxFromChain { <# .SYNOPSIS Creates a PFX file from a signed certificate, its chain and an existing private key. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$LeafCertPath, [string[]]$ChainCertPaths, [Parameter(Mandatory = $true)] [string]$KeyPfxPath, [Parameter(Mandatory = $true)] [string]$KeyPfxPassword, [Parameter(Mandatory = $true)] [string]$OutputPfxPath, [Parameter(Mandatory = $true)] [string]$OutputPfxPassword ) # Helper: load a certificate from DER or PEM .crt. function Get-CertificateFromFile { param([string]$Path) if (-not (Test-Path -Path $Path)) { throw "Certificate file '$Path' not found." } $raw = [System.IO.File]::ReadAllBytes($Path) try { return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($raw) } catch { # Try PEM (-----BEGIN CERTIFICATE-----). $text = [System.IO.File]::ReadAllText($Path) $begin = "-----BEGIN CERTIFICATE-----" $end = "-----END CERTIFICATE-----" $startIndex = $text.IndexOf($begin) if ($startIndex -lt 0) { throw "File '$Path' is not a valid DER or PEM certificate." } $startIndex += $begin.Length $endIndex = $text.IndexOf($end, $startIndex) if ($endIndex -lt 0) { throw "File '$Path' contains a BEGIN CERTIFICATE marker but no matching END." } $base64 = $text.Substring($startIndex, $endIndex - $startIndex) $base64 = ($base64 -replace '\s','') $bytes = [System.Convert]::FromBase64String($base64) return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) } } $storageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable # 1) Load existing PFX that contains the private key from the CSR. $keyCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2( $KeyPfxPath, $KeyPfxPassword, $storageFlags ) if (-not $keyCert.HasPrivateKey) { throw "The PFX at '$KeyPfxPath' does not contain a private key." } # 2) Load leaf certificate from provider. $leafCert = Get-CertificateFromFile -Path $LeafCertPath # 3) Attach the private key to the leaf certificate (PS 5.1 / .NET Framework-safe). $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($keyCert) if (-not $rsa) { throw "Private key in '$KeyPfxPath' is not an RSA key (this script currently expects RSA)." } $leafWithKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey( [System.Security.Cryptography.X509Certificates.X509Certificate2]$leafCert, [System.Security.Cryptography.RSA]$rsa ) if (-not $leafWithKey) { throw "Failed to attach private key from '$KeyPfxPath' to leaf certificate '$LeafCertPath'." } # 4) Build collection: leaf + chain. $collection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection $null = $collection.Add($leafWithKey) if ($ChainCertPaths) { foreach ($chainPath in $ChainCertPaths) { if (-not [string]::IsNullOrWhiteSpace($chainPath)) { $chainCert = Get-CertificateFromFile -Path $chainPath $null = $collection.Add($chainCert) } } } # 5) Export new PFX. $pfxBytes = $collection.Export( [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $OutputPfxPassword ) $outDir = [System.IO.Path]::GetDirectoryName($OutputPfxPath) if ($outDir -and -not (Test-Path -Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } [System.IO.File]::WriteAllBytes($OutputPfxPath, $pfxBytes) Write-Host "Created PFX: $OutputPfxPath" } function Test-CertPfx { <# .SYNOPSIS Inspects a PFX file and writes basic certificate and chain information to the console. .DESCRIPTION Loads a PFX file, lists all contained certificates, identifies the certificate that has a private key (the leaf for typical TLS usage) and attempts to build a chain from leaf to root using the other certificates in the PFX as additional chain elements. This is intended as a diagnostic helper to verify that a PFX produced by New-CertPfxFromChain contains: - exactly one leaf certificate with a private key, and - the expected intermediate / root certificates. .PARAMETER PfxPath Path to the PFX file that should be inspected. .PARAMETER PfxPassword Plain-text password that protects the PFX file. .EXAMPLE Test-CertPfx -PfxPath ".\eigenverft_final.pfx" -PfxPassword "FinalPfxPassword123!" Loads eigenverft_final.pfx, dumps the certificates inside, and shows the constructed chain from leaf to root including chain build status. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PfxPath, [Parameter(Mandatory = $true)] [string]$PfxPassword ) if (-not (Test-Path -Path $PfxPath)) { throw "PFX file '$PfxPath' not found." } # Load all certificates from the PFX into a collection. $certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection $certs.Import( $PfxPath, $PfxPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ) Write-Host "PFX file : $PfxPath" Write-Host "Password : (provided)" Write-Host "Certs in PFX: $($certs.Count)" Write-Host "" # Dump basic info for each certificate in the PFX. $index = 0 foreach ($c in $certs) { $index++ Write-Host "[$index] ----------------------------------------" Write-Host (" Subject : {0}" -f $c.Subject) Write-Host (" Issuer : {0}" -f $c.Issuer) Write-Host (" NotBefore : {0}" -f $c.NotBefore) Write-Host (" NotAfter : {0}" -f $c.NotAfter) Write-Host (" Thumbprint : {0}" -f $c.Thumbprint) Write-Host (" HasPrivateKey: {0}" -f $c.HasPrivateKey) Write-Host "" } # Identify the leaf certificate (the one with the private key). $leafWithKey = $certs | Where-Object { $_.HasPrivateKey } if (-not $leafWithKey -or $leafWithKey.Count -eq 0) { Write-Warning "No certificate with a private key found in the PFX. Expected one leaf certificate with a private key." return } if ($leafWithKey.Count -gt 1) { Write-Warning "More than one certificate with a private key found in the PFX. Using the first one for chain analysis." $leaf = $leafWithKey | Select-Object -First 1 } else { $leaf = $leafWithKey } Write-Host "Leaf certificate (with private key) selected for chain build:" Write-Host (" Subject : {0}" -f $leaf.Subject) Write-Host (" Issuer : {0}" -f $leaf.Issuer) Write-Host (" Thumbprint: {0}" -f $leaf.Thumbprint) Write-Host "" # Build a chain from the leaf certificate. $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain # Do not perform revocation checks here; this is a structural test only. $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck $chain.ChainPolicy.RevocationFlag = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EndCertificateOnly $chain.ChainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::IgnoreWrongUsage # Add all non-leaf certificates from the PFX to the ExtraStore so the chain # builder can use them, even if they are not in the OS stores. foreach ($c in $certs) { if ($c.Thumbprint -ne $leaf.Thumbprint) { [void]$chain.ChainPolicy.ExtraStore.Add($c) } } $chainBuilt = $chain.Build($leaf) Write-Host "Chain build result: $chainBuilt" if (-not $chainBuilt -and $chain.ChainStatus.Count -gt 0) { Write-Host "Chain status:" foreach ($status in $chain.ChainStatus) { if ($status.Status -ne [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags]::NoError) { Write-Host (" - {0}: {1}" -f $status.Status, $status.StatusInformation.Trim()) } } Write-Host "" } Write-Host "Chain elements (leaf -> root) according to X509Chain:" $i = 0 foreach ($elem in $chain.ChainElements) { $i++ $c = $elem.Certificate Write-Host (" ({0}) Subject : {1}" -f $i, $c.Subject) Write-Host (" Issuer : {0}" -f $c.Issuer) Write-Host (" Thumbprint: {0}" -f $c.Thumbprint) Write-Host "" } } |