Private/Export-Pem.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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 = New-Object Org.BouncyCastle.Asn1.X9.X962Parameters -ArgumentList $privParam.PublicKeyParamSet
            $pubKey = New-Object Org.BouncyCastle.Asn1.DerBitString -ArgumentList @(,$BCKeyPair.Public.Q.GetEncoded())

            # create the structure
            $privKeyStruct = New-Object Org.BouncyCastle.Asn1.Sec.ECPrivateKeyStructure -ArgumentList $orderBitLength,$privParam.D,$pubKey,$x962

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

            # PKCS1 means 'EC PRIVATE KEY' rather than just 'PRIVATE KEY'
            # 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 = New-Object IO.StreamWriter($OutputFile, $false, [Text.Encoding]::ASCII)
        $sw.NewLine = "`n"
        foreach ($line in $pem) {
            $sw.WriteLine($line)
        }
    } finally { if ($null -ne $sw) { $sw.Close() } }

}