Public/Complete-NSPSslRequest.ps1
|
function Complete-NSPSslRequest { <# .SYNOPSIS Turns the issuer's certificate files into deliverables: certificate, intermediates, full-chain PEM, and a password-protected PFX. .DESCRIPTION Reads the certificate files the issuer sent - by default the newest ZIP in the request's Inputs folder (IIS format: <serial>.crt plus *_intermediates.p7b), or any .crt/.cer/.pem/.p7b /.ca-bundle files or ZIPs given with -InputPath. The request's certificate is the one whose public key matches the request's private key, so extra files in the ZIP don't matter, and a ZIP for the wrong request is refused. The intermediates are put in order from the certificate up. Self-signed roots are left out (clients already trust them; -IncludeRoot keeps one). A cross-signed intermediate that carries a root's name, such as Starfield TLS Root CA - R1 issued by Starfield Root Certificate Authority - G2, is an intermediate and stays in. Writes to the request's Deliverables folder: <domain>.crt the certificate (PEM) <domain>_intermediates.crt the intermediates, in order (PEM) <domain>_FullChain.pem certificate + intermediates (for FortiGate, nginx, Apache) <domain>.pfx certificate + intermediates + private key (for Windows/IIS) The PFX password is not saved anywhere; send it separately from the PFX. Import the PFX on Windows with Import-NSPSslPfx, which puts each certificate in the right store. Replaces 2_CombineCerts.sh from NSP-SSLCertificates. Requests started there (Rebase\<domain> with Deliverables_<domain>) work too: pass -Path <the Rebase folder>. .PARAMETER Domain The request to finish, as given to New-NSPSslRequest. .PARAMETER Path The folder that holds request folders (see New-NSPSslRequest). .PARAMETER Folder A request folder, instead of -Domain and -Path. Get-NSPSslRequest output pipes here. .PARAMETER InputPath ZIPs, certificate files or folders to read instead of the newest ZIP in Inputs. .PARAMETER PfxPassword The password for the PFX. Prompted for when not given. .PARAMETER FriendlyName The PFX certificate's friendly name. Default: <domain>_<year issued>. .PARAMETER IncludeRoot Keep a self-signed root, when the issuer sent one, in the intermediates, full chain, and PFX. .EXAMPLE Complete-NSPSslRequest -Domain vpn.contoso.com .EXAMPLE Complete-NSPSslRequest -Domain vpn.contoso.com -InputPath C:\Downloads\vpn.contoso.com.zip -PfxPassword (Read-Host -AsSecureString 'PFX password') .EXAMPLE Get-NSPSslRequest -Status CertificateReady | Complete-NSPSslRequest #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Domain')] [OutputType([pscustomobject])] param( [Parameter(Mandatory, Position = 0, ParameterSetName = 'Domain')][string]$Domain, [Parameter(ParameterSetName = 'Domain')][string]$Path, [Parameter(Mandatory, ParameterSetName = 'Folder', ValueFromPipelineByPropertyName)][string]$Folder, [string[]]$InputPath, [Parameter(Mandatory)][securestring]$PfxPassword, [string]$FriendlyName, [switch]$IncludeRoot ) process { if ($PSCmdlet.ParameterSetName -eq 'Domain') { $Folder = Join-Path (Get-NSPSslWorkRoot -Path $Path) (ConvertTo-NSPSslFolderName -Domain $Domain.Trim()) } $layout = Get-NSPSslRequestLayout -Folder $Folder if (-not (Test-Path -LiteralPath $layout.KeyPath)) { throw "No private key at $($layout.KeyPath). Start the request with New-NSPSslRequest." } if (-not $InputPath) { $zips = @(Get-ChildItem -LiteralPath $layout.Inputs -Filter '*.zip' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) if (-not $zips) { throw "No ZIP in $($layout.Inputs). Download the certificate from the issuer in IIS format and put the ZIP there, or pass -InputPath." } if ($zips.Count -gt 1) { Write-Warning "$($zips.Count) ZIPs in $($layout.Inputs); using the newest, $($zips[0].Name)." } $InputPath = @($zips[0].FullName) } $extract = Join-Path ([IO.Path]::GetTempPath()) ('NSP-SSL-' + [guid]::NewGuid().ToString('N')) $candidates = [Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate2]]::new() $seen = @{} $rsa = $null try { $files = foreach ($item in $InputPath) { if (-not (Test-Path -LiteralPath $item)) { throw "Not found: $item" } $entry = Get-Item -LiteralPath $item if ($entry.PSIsContainer) { Get-ChildItem -LiteralPath $entry.FullName -File -Recurse } elseif ($entry.Extension -eq '.zip') { $target = Join-Path $extract ([guid]::NewGuid().ToString('N')) Expand-Archive -LiteralPath $entry.FullName -DestinationPath $target Get-ChildItem -LiteralPath $target -File -Recurse } else { $entry } } foreach ($file in @($files | Where-Object { $_.Extension -in '.crt', '.cer', '.pem', '.der', '.p7b', '.p7c', '.ca-bundle' })) { try { foreach ($certificate in Read-NSPSslCertificateFile -Path $file.FullName) { if (-not $seen.ContainsKey($certificate.Thumbprint)) { $seen[$certificate.Thumbprint] = $true; $candidates.Add($certificate) } } } catch { Write-Warning "Skipped $($file.Name): not a certificate file ($($_.Exception.Message))." } } if (-not $candidates.Count) { throw "No certificates found in $($InputPath -join ', ')." } $keyParameters = ConvertFrom-NSPSslPrivateKeyPem -Text ([IO.File]::ReadAllText($layout.KeyPath)) $modulus = [Convert]::ToBase64String($keyParameters.Modulus) $leaf = $candidates | Where-Object { $publicKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($_) $publicKey -and [Convert]::ToBase64String($publicKey.ExportParameters($false).Modulus) -eq $modulus } | Sort-Object NotAfter -Descending | Select-Object -First 1 if (-not $leaf) { throw "None of the $($candidates.Count) certificates in $($InputPath -join ', ') belong to the private key for $($layout.Domain). Check that the files are for this request and were issued from its CSR." } $chain = @(Get-NSPSslCertificateChain -Certificate $leaf -Candidates $candidates.ToArray()) $issuers = @($chain | Where-Object { $IncludeRoot -or -not (Test-NSPSslSelfSigned -Certificate $_) }) if (-not $chain -or -not (Test-NSPSslSelfSigned -Certificate $chain[-1])) { $top = if ($chain) { $chain[-1] } else { $leaf } if (-not $chain) { Write-Warning "The issuer sent no intermediates for $($leaf.Subject); clients may not be able to build its chain." } Write-Verbose "Chain ends at $($top.Subject), issued by $($top.Issuer) (not included)." } $now = Get-Date if ($leaf.NotAfter -lt $now) { Write-Warning "The certificate expired on $($leaf.NotAfter)." } if ($leaf.NotBefore -gt $now) { Write-Warning "The certificate isn't valid until $($leaf.NotBefore)." } $issuedNames = @(Get-NSPSslDnsName -Certificate $leaf) $missing = @($layout.Names | Where-Object { $_ -notin $issuedNames }) if ($missing) { Write-Warning "Requested but not on the certificate: $($missing -join ', ')." } if (-not $PSCmdlet.ShouldProcess($layout.Deliverables, "Write the certificate, intermediates, full chain and PFX for $($leaf.Subject)")) { return } $certificatePem = ConvertTo-NSPSslPem -Label 'CERTIFICATE' -Bytes $leaf.RawData $issuersPem = ($issuers | ForEach-Object { ConvertTo-NSPSslPem -Label 'CERTIFICATE' -Bytes $_.RawData }) -join '' [IO.File]::WriteAllText($layout.CertificatePath, $certificatePem) [IO.File]::WriteAllText($layout.IntermediatesPath, $issuersPem) [IO.File]::WriteAllText($layout.FullChainPath, $certificatePem + $issuersPem) $rsa = New-NSPSslRsa -Parameters $keyParameters $withKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($leaf, $rsa) if (-not $FriendlyName) { $FriendlyName = '{0}_{1}' -f (ConvertTo-NSPSslFolderName -Domain $layout.Domain), $leaf.NotBefore.Year } $withKey.FriendlyName = $FriendlyName $bundle = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection [void]$bundle.Add($withKey) foreach ($issuer in $issuers) { [void]$bundle.Add($issuer) } $pfx = $bundle.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, (ConvertFrom-NSPSslSecureString -SecureString $PfxPassword)) [IO.File]::WriteAllBytes($layout.PfxPath, $pfx) [pscustomobject]@{ Domain = $layout.Domain Subject = $leaf.Subject Names = $issuedNames Thumbprint = $leaf.Thumbprint NotAfter = $leaf.NotAfter Chain = @($issuers | ForEach-Object { $_.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false) }) FriendlyName = $FriendlyName CertificatePath = $layout.CertificatePath IntermediatesPath = $layout.IntermediatesPath FullChainPath = $layout.FullChainPath PfxPath = $layout.PfxPath } } finally { if ($rsa) { $rsa.Dispose() } Remove-Item -LiteralPath $extract -Recurse -Force -ErrorAction SilentlyContinue } } } |