Public/Get-PACertificate.ps1
function Get-PACertificate { [CmdletBinding()] [OutputType('PoshACME.PACertificate')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText','')] param( [Parameter(ParameterSetName='Specific',Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)] [string]$MainDomain, [Parameter(ParameterSetName='Specific',ValueFromPipelineByPropertyName)] [ValidateScript({Test-ValidFriendlyName $_ -ThrowOnFail})] [string]$Name, [Parameter(ParameterSetName='List',Mandatory)] [switch]$List ) Begin { try { # Make sure we have an account configured if (-not (Get-PAAccount)) { throw "No ACME account configured. Run Set-PAAccount or New-PAAccount first." } } catch { $PSCmdlet.ThrowTerminatingError($_) } # prep to calculate SHA1 thumbprints $sha1 = [Security.Cryptography.SHA1CryptoServiceProvider]::new() } Process { # since the params in this function are a subset of the params for Get-PAOrder, we're # just going to pass them directly to it to get order(s) associated with the certificates if (-not ($orders = Get-PAOrder @PSBoundParameters)) { return } $orders | ForEach-Object { $order = $_ $certFile = Join-Path $order.Folder 'cert.cer' # skip if if there's no cert file if (-not (Test-Path $certFile -PathType Leaf)) { return } # import the cert $cert = Import-Pem -InputFile $certFile # build the list of SANs $altNames = $cert.GetSubjectAlternativeNames() | ForEach-Object { if ($_[0] -eq [Org.BouncyCastle.Asn1.X509.GeneralName]::DnsName) { # second index is the actual DNS name $_[1] } elseif ($_[0] -eq [Org.BouncyCastle.Asn1.X509.GeneralName]::IPAddress) { # second index is a IP hex string like "#01010101" that we need to parse ([ipaddress]([byte[]] -split ($_[1].Substring(1) -replace '..', '0x$& '))).ToString() } } # convert the PfxPass to a securestring if ($order.PfxPass) { $secPfxPass = ConvertTo-SecureString $order.PfxPass -AsPlainText -Force } else { $secPfxPass = [Security.SecureString]::new() } # derive the ARI ID value # https://letsencrypt.org/2024/04/25/guide-to-integrating-ari-into-existing-acme-clients#step-3-constructing-the-ari-certid # https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#name-the-renewalinfo-resource $akiExt = $cert.GetExtensionValue([Org.BouncyCastle.Asn1.X509.X509Extensions]::AuthorityKeyIdentifier) if ($akiExt) { $akiBytes = [Org.BouncyCastle.Asn1.X509.AuthorityKeyIdentifier]::GetInstance($akiExt.GetOctets()).GetKeyIdentifier() $serialBytes = $cert.SerialNumber.ToByteArray() $ariID = '{0}.{1}' -f (ConvertTo-Base64Url $akiBytes),(ConvertTo-Base64Url $serialBytes) } else { Write-Warning "Cert with subject $($cert.SubjectDN) and serial $($cert.SerialNumber) has no AKI extension. Unable to generate ARIId value." $ariID = $null } # send the output object to the pipeline [pscustomobject]@{ PSTypeName = 'PoshACME.PACertificate' # add the literal subject rather than just the domain name Subject = $cert.SubjectDN.ToString() # PowerShell's cert:\ provider outputs these in local time, but BouncyCastle # outputs in UTC. So we'll convert so they match NotBefore = $cert.NotBefore.ToLocalTime() NotAfter = $cert.NotAfter.ToLocalTime() KeyLength = $order.KeyLength # the thumbprint is a SHA1 hash of the DER encoded cert which is not actually # stored in the cert itself Thumbprint = [BitConverter]::ToString($sha1.ComputeHash($cert.GetEncoded())).Replace('-','') # add the ARI ID value ARIId = $ariID # add the serial Serial = $cert.SerialNumber.ToString() # add the full list of SANs AllSANs = [string[]]@($altNames) # add the associated file paths whether they exist or not CertFile = (Join-Path $order.Folder 'cert.cer').ToString() KeyFile = (Join-Path $order.Folder 'cert.key').ToString() ChainFile = (Join-Path $order.Folder 'chain.cer').ToString() FullChainFile = (Join-Path $order.Folder 'fullchain.cer').ToString() PfxFile = (Join-Path $order.Folder 'cert.pfx').ToString() PfxFullChain = (Join-Path $order.Folder 'fullchain.pfx').ToString() PfxPass = $secPfxPass } } } } |