Private/CertificateHelper.ps1


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

    if ($PSEdition -eq 'Core') {
        $bytes = $Rsa.ExportRSAPublicKey()
        ConvertTo-PemEncoding -Label "PUBLIC KEY" -RawData $bytes
    } else {
        $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 ConvertTo-RsaPublicKey
{
    [OutputType('System.Security.Cryptography.RSA')]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string] $Pem
    )

    if ($PSEdition -eq 'Core') {
        $Rsa = [System.Security.Cryptography.RSA]::Create()
        $Rsa.ImportFromPem($Pem)
        $Rsa
    } else {
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($Pem)
        $stream = [System.IO.MemoryStream]::new($bytes)
        $reader = [PemUtils.PemReader]::new($stream)
        $params = $reader.ReadRsaKey();
        [System.Security.Cryptography.RSA]::Create($params)
    }
}

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

    if ($PSEdition -eq 'Core') {
        $bytes = $Rsa.ExportRSAPrivateKey()
        ConvertTo-PemEncoding -Label "PRIVATE KEY" -RawData $bytes
    } else {
        $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().Trim()
    }
}

function ConvertTo-RsaPrivateKey
{
    [OutputType('System.Security.Cryptography.RSA')]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string] $Pem
    )

    if ($PSEdition -eq 'Core') {
        $Rsa = [System.Security.Cryptography.RSA]::Create()
        $Rsa.ImportFromPem($Pem)
        $Rsa
    } else {
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($Pem)
        $stream = [System.IO.MemoryStream]::new($bytes)
        $reader = [PemUtils.PemReader]::new($stream)
        $params = $reader.ReadRsaKey();
        [System.Security.Cryptography.RSA]::Create($params)
    }
}

# No PemUtils beyond this point

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-CertificateIssuerName
{
    [OutputType('string')]
    param(
        [Parameter(Mandatory=$true)]
        [string] $PemData
    )

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

    return $cert.IssuerName.Name
}

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

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

    return $cert.SubjectName.Name
}

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

    [string[]] $PemChain = @()
    $PrivateKey = $null

    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)
    
        foreach ($cert in $collection) {
            if ($cert.HasPrivateKey) {
                $PrivateKey = ConvertFrom-RsaPrivateKey -Rsa $cert.PrivateKey
            }
            $PemCert = ConvertTo-PemEncoding -Label 'CERTIFICATE' -RawData $cert.RawData    
            $PemChain += $PemCert
        }
    } else {
        $PemData = Get-Content -Path $CertificateFile -Raw
        $PemChain = Split-PemChain -Label 'CERTIFICATE' -PemData $PemData

        $PrivateKey = Get-Content -Path $PrivateKeyFile -Raw
    }

    if ($PemChain.Count -eq 0) {
        throw "Empty certificate chain!"
    }

    $PemChain | ForEach-Object -Begin {
        $Certs = @{}
    } -Process {
        $Key = Get-CertificateSubjectName -PemData $_
        $Certs.Add($Key, $_)
    } -End {
        $Certs
    }

    $LeafCert = $PemChain | Where { -Not ( Get-IsPemCertificateAuthority -PemData $_ )}

    [string[]] $SortedPemChain = @()

    if ($LeafCert -Eq $null) {
        # Do not apply any transformation to the provided chain if no leaf certificate is found
        $SortedPemChain = $PemChain
    } else {
        # Otherwise, sort the chain: start by the leaf and then issued to issuer in order

        $SortedPemChain += $LeafCert
        $IssuerName = Get-CertificateIssuerName -PemData $LeafCert
        $SubjectName = Get-CertificateSubjectName -PemData $LeafCert

        While ($Certs.ContainsKey($IssuerName) -And ($IssuerName -Ne $SubjectName)) {
            $NextCert = $Certs[$IssuerName]
            $SortedPemChain += $NextCert
            $IssuerName = Get-CertificateIssuerName -PemData $NextCert
            $SubjectName = Get-CertificateSubjectName -PemData $NextCert
        }
    }

    $Certificate = $SortedPemChain -Join "`n"

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