Private/Export-Pem.ps1

function Export-Pem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [psobject]$InputObject,
        [Parameter(Mandatory,Position=1)]
        [string]$OutputFile
    )

    if ($InputObject -is [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]) {
        $BCKeyPair = $InputObject

        if ($BCKeyPair.Private -is [Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters]) {

            # grab the things we need to build an ECPrivateKeyStructure that includes the public key
            $privParam = $BCKeyPair.Private
            $orderBitLength = $privParam.Parameters.N.BitLength
            $x962 = [Org.BouncyCastle.Asn1.X9.X962Parameters]::new($privParam.PublicKeyParamSet)
            $pubKey = [Org.BouncyCastle.Asn1.DerBitString]::new($BCKeyPair.Public.Q.GetEncoded())

            # create the structure
            $privKeyStruct = [Org.BouncyCastle.Asn1.Sec.ECPrivateKeyStructure]::new(
                $orderBitLength, $privParam.D, $pubKey, $x962
            )

            # ECPrivateKeyStructure.GetDerEncoded() seems to return a SEC1 version of the key
            $privKeyStr = [Convert]::ToBase64String($privKeyStruct.GetDerEncoded())

            # SEC1 means 'EC PRIVATE KEY' rather than just 'PRIVATE KEY' for PKCS8
            # build an array with the proper header/footer
            $pem = @('-----BEGIN EC PRIVATE KEY-----')
            for ($i=0; $i -lt $privKeyStr.Length; $i += 64) {
                $pem += $privKeyStr.Substring($i,[Math]::Min(64,($privKeyStr.Length-$i)))
            }
            $pem += '-----END EC PRIVATE KEY-----'

        } elseif ($BCKeyPair.Private -is [Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters]) {

            # build the PrivateKeyInfo object
            $rsaInfo = [Org.BouncyCastle.Pkcs.PrivateKeyInfoFactory]::CreatePrivateKeyInfo($BCKeyPair.Private)

            # the PrivateKeyInfo.GetDerEncoded() method seems to return a PKCS8 version of the key
            $privKeyStr = [Convert]::ToBase64String($rsaInfo.GetDerEncoded())

            # PKCS8 means 'PRIVATE KEY' rather than 'RSA PRIVATE KEY'
            # build an array with the proper header/footer
            $pem = @('-----BEGIN PRIVATE KEY-----')
            for ($i=0; $i -lt $privKeyStr.Length; $i += 64) {
                $pem += $privKeyStr.Substring($i,[Math]::Min(64,($privKeyStr.Length-$i)))
            }
            $pem += '-----END PRIVATE KEY-----'

        } else {
            throw "Unsupported BouncyCastle KeyPair type"
        }

    } elseif ($InputObject -is [Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest]) {

        # get the raw Base64 encoded version
        $reqStr = [Convert]::ToBase64String($InputObject.GetEncoded())

        # build an array with the header/footer
        # https://stackoverflow.com/questions/28628744/is-there-a-spec-for-csr-begin-headers
        # Apparently including "NEW" is the old way
        $pem = @('-----BEGIN CERTIFICATE REQUEST-----')
        for ($i=0; $i -lt $reqStr.Length; $i += 64) {
            $pem += $reqStr.Substring($i,[Math]::Min(64,($reqStr.Length-$i)))
        }
        $pem += '-----END CERTIFICATE REQUEST-----'

    } elseif ($InputObject -is [array]) {
        # this should be a string array output from Split-PemChain that we just
        # need to write to disk with proper line endings.
        $pem = $InputObject

    } else {
        throw "Unsuppored InputObject type"
    }

    # resolve relative paths
    $OutputFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputFile)

    # Usually, PEM files are ANSI/ASCII encoded with UNIX line endings which means none of the
    # normal PowerShell stuff for outputting files will work. So we'll use a .NET StreamWriter
    # instead.
    try {
        $sw = [IO.StreamWriter]::new($OutputFile, $false, [Text.Encoding]::ASCII)
        $sw.NewLine = "`n"
        foreach ($line in $pem) {
            $sw.WriteLine($line)
        }
    } finally { if ($null -ne $sw) { $sw.Close() } }

}