Private/CertificateHelper.ps1


function ConvertFrom-RsaPublicKey
{
    [OutputType('System.String')]
    param(
        [Parameter(Mandatory=$true)]
        [System.Security.Cryptography.RSA] $Rsa
    )

    $stream = [System.IO.MemoryStream]::new()
    $writer = [PemUtils.PemWriter]::new($stream)
    $writer.WritePublicKey($Rsa)
    $stream.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
    [System.IO.StreamReader]::new($stream).ReadToEnd()
}

function ConvertFrom-RsaPrivateKey
{
    [OutputType('System.String')]
    param(
        [Parameter(Mandatory=$true)]
        [System.Security.Cryptography.RSA] $Rsa
    )

    $stream = [System.IO.MemoryStream]::new()
    $writer = [PemUtils.PemWriter]::new($stream)
    $writer.WritePrivateKey($Rsa)
    $stream.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
    [System.IO.StreamReader]::new($stream).ReadToEnd()
}

function New-RsaKeyPair
{
    param(
        [int] $KeySize = 2048
    )

    $Rsa = [System.Security.Cryptography.RSA]::Create($KeySize)
    $PublicKey = ConvertFrom-RsaPublicKey -Rsa $Rsa
    $PrivateKey = ConvertFrom-RsaPrivateKey -Rsa $Rsa

    return [PSCustomObject]@{
        PublicKey = $PublicKey
        PrivateKey = $PrivateKey
    }
}

function ConvertTo-PemEncoding
{
    [OutputType('System.String')]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Label,
        [Parameter(Mandatory=$true)]
        [byte[]] $RawData
    )

    $base64 = [Convert]::ToBase64String($RawData)

    $offset = 0
    $line_length = 64
    $sb = [System.Text.StringBuilder]::new()
    $sb.AppendLine("-----BEGIN $label-----") | Out-Null
    while ($offset -lt $base64.Length) {
        $line_end = [Math]::Min($offset + $line_length, $base64.Length)
        $sb.AppendLine($base64.Substring($offset, $line_end - $offset)) | Out-Null
        $offset = $line_end
    }
    $sb.AppendLine("-----END $label-----") | Out-Null

    return $sb.ToString().Trim()
}

function ConvertFrom-PemEncoding
{
    [OutputType('byte[]')]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Label,
        [Parameter(Mandatory=$true)]
        [string] $PemData
    )

    $base64 = $PemData `
        -Replace "`n","" -Replace "`r","" `
        -Replace "-----BEGIN $Label-----", "" `
        -Replace "-----END $Label-----", ""

    return [Convert]::FromBase64String($base64)
}

function Split-PemChain
{
    [OutputType('System.String[]')]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Label,
        [Parameter(Mandatory=$true)]
        [string] $PemData
    )

    [string[]] $PemChain = @()
    $PemData | Select-String  -Pattern "(?smi)^-{2,}BEGIN $Label-{2,}.*?-{2,}END $Label-{2,}" `
        -Allmatches | ForEach-Object { $_.Matches } | ForEach-Object { $PemChain += $_.Value }

    return $PemChain
}

function Get-IsPemCertificateAuthority
{
    [OutputType('bool')]
    param(
        [Parameter(Mandatory=$true)]
        [string] $PemData
    )

    $der = ConvertFrom-PemEncoding -Label 'CERTIFICATE' -PemData:$PemData
    $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]] $der)

    foreach ($extension in $cert.Extensions) {
        if ($extension.Oid.Value -eq "2.5.29.19") {
            $extension = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension] $extension
            return $extension.CertificateAuthority
        }
    }

    return $false
}

function Get-PemCertificate
{
    param(
        [string] $CertificateFile,
        [string] $PrivateKeyFile,
        [string] $Password
    )

    if (($CertificateFile -match ".pfx") -or ($CertificateFile -match ".p12")) {
        $AsByteStream = if ($PSEdition -eq 'Core') { @{AsByteStream = $true} } else { @{'Encoding' = 'Byte'} }
        $CertificateData = Get-Content -Path $CertificateFile -Raw @AsByteStream
        $collection = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new()
        $collection.Import($CertificateData, $Password, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
    
        [string[]] $PemChain = @()
        $PrivateKey = $null
    
        foreach ($cert in $collection) {
            if ($cert.HasPrivateKey) {
                $PrivateKey = ConvertFrom-RsaPrivateKey -Rsa $cert.PrivateKey
            }
            $PemCert = ConvertTo-PemEncoding -Label 'CERTIFICATE' -RawData $cert.RawData    
            $PemChain += $PemCert
        }
    
        if ($PemChain.Count -eq 0) {
            throw "Empty certificate chain!"
        }

        if (Get-IsPemCertificateAuthority -PemData $PemChain[0]) {
            [array]::Reverse($PemChain)
        }
    
        $Certificate = $PemChain -Join "`n"

        return [PSCustomObject]@{
            Certificate = $Certificate
            PrivateKey = $PrivateKey
        }
    } else {
        $PemData = Get-Content -Path $CertificateFile -Raw
        [string[]] $PemChain = Split-PemChain -Label 'CERTIFICATE' -PemData $PemData

        if ($PemChain.Count -eq 0) {
            throw "Empty certificate chain!"
        }
        
        if (Get-IsPemCertificateAuthority -PemData $PemChain[0]) {
            [array]::Reverse($PemChain)
        }

        $Certificate = $PemChain -Join "`n"
        $PrivateKey = Get-Content -Path $PrivateKeyFile -Raw

        return [PSCustomObject]@{
            Certificate = $Certificate
            PrivateKey = $PrivateKey
        }
    }
}