Private/Import-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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
function Import-Pem {
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName='File',Mandatory,Position=0)]
        [string]$InputFile,
        [Parameter(ParameterSetName='String',Mandatory)]
        [string]$InputString
    )

    # DER uses TLV (Tag/Length/Value) triplets.
    # First byte is the tag - https://en.wikipedia.org/wiki/X.690#Types
    # Second byte is either the total length of the value when less than 0x80 (128)
    # or the number of bytes that make up the value not counting the most significant bit (the 8)
    # So 0x77 (less than 0x80) means length is 119 (0x77) bytes
    # 0x82 (more than 0x80) means the length is the next 2 (0x82-0x80) bytes
    # Value starts the byte after the length bytes end

    if ('File' -eq $PSCmdlet.ParameterSetName) {
        # normalize the file path and read it in
        $InputFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InputFile)
        $pemStr = (Get-Content $InputFile) -join ''
    } else {
        $pemStr = $InputString.Replace("`n",'')
    }

    # private keys
    if ($pemStr -like '*-----BEGIN *PRIVATE KEY-----*' -and $pemStr -like '*-----END *PRIVATE KEY-----*') {

        $base64 = $pemStr.Substring($pemStr.IndexOf('KEY-----')+8)
        $base64 = $base64.Substring(0,$base64.IndexOf('-'))
        $keyBytes = [Convert]::FromBase64String($base64)

        # We need to identify enough of the DER encoded ASN.1 structure to differentiate between
        # RSA vs EC keys in order to call the right BouncyCastle libraries to import them.

        # On the keys we care about, the first tag is always a SEQUENCE (0x30) and the first
        # tag within that sequence is an INTEGER (0x02) which is a Version field.
        # Version = 1 always means an EC key
        # Version = 0 either means a PKCS1 RSA key or a PKCS8 key that could be either RSA or EC
        # Need to check the second item in the sequence to say for sure.
        # If Second tag is INTEGER, PKCS1 RSA key
        # If Second tag is a SEQUENCE, PKCS8 and need to check first child for Algorithm OID
        # Child = 1.2.840.113549.1.1.1 (RSA) [Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers]::RsaEncryption
        # Child = 1.2.840.10045.2.1 (EC) [Org.BouncyCastle.Asn1.X9.X9ObjectIdentifiers]::IdECPublicKey

        # throw if we don't find a SEQUENCE tag in the first byte
        if ($keyBytes[0] -ne 0x30) { throw "Invalid private key: No sequence in first byte" }

        # read in the bytes as an Asn1Sequence object
        $seq = [Org.BouncyCastle.Asn1.Asn1Sequence]::GetInstance($keyBytes)

        # check for RSA keys
        if ($seq[0] -eq 0 -and
            ($seq[1] -is [Org.BouncyCastle.Asn1.DerInteger] -or
            ($seq[1].Count -eq 2 -and $seq[1][0] -eq [Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers]::RsaEncryption)) ) {

            Write-Debug "Found RSA key type"

            # We can deal with either PKCS1 or PKCS8, because the PKCS1 can be extracted from PKCS8
            if ($seq.Count -eq 3) {
                Write-Debug "Extracting RSA PKCS1 from PKCS8"
                $seq = [Org.BouncyCastle.Asn1.Asn1Sequence]::GetInstance($seq[2].GetOctets())
            }

            # The resulting sequence should have 9 items, otherwise it's incomplete/malformed
            if ($seq.Count -ne 9) { throw "Invalid sequence in RSA private key" }

            # build the key parameters we'll need to build the AsymmetricCipherKey later
            $rsa = [Org.BouncyCastle.Asn1.Pkcs.RsaPrivateKeyStructure]::GetInstance($seq)
            $pubSpec = New-Object Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters($false,$rsa.Modulus,$rsa.PublicExponent)
            $privSpec = New-Object Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters(
                $rsa.Modulus, $rsa.PublicExponent, $rsa.PrivateExponent,
                $rsa.Prime1, $rsa.Prime2, $rsa.Exponent1, $rsa.Exponent2,
                $rsa.Coefficient)

        # check fo EC keys
        } elseif ($seq[0] -eq 1 -or
                  ($seq[0] -eq 0 -and $seq[1].Count -eq 2 -and
                   $seq[1][0] -eq [Org.BouncyCastle.Asn1.X9.X9ObjectIdentifiers]::IdECPublicKey) ) {

            Write-Debug "Found EC key type"

            # Haven't figured out how to extract the key from PKCS8 yet because it's not the same format
            # as a raw SEC1 key
            if ($seq.Count -eq 3) {
                throw "Unsupported PKCS8 EC key"
            }

            # Makes sure we're dealing with a raw SEC1 key rather than a PKCS8 container
            if ($seq.Count -ne 4) { "Unsupported EC key encoding" }

            # build the key parameters we'll need to build the AsymmetricCipherKey later
            $pKey = [Org.BouncyCastle.Asn1.Sec.ECPrivateKeyStructure]::GetInstance($seq)
            $ecPubKeyOid = [Org.BouncyCastle.Asn1.DerObjectIdentifier]([Org.BouncyCastle.Asn1.X9.X9ObjectIdentifiers]::IdECPublicKey)
            $algId = New-Object Org.BouncyCastle.Asn1.X509.AlgorithmIdentifier($ecPubKeyOid,$pKey.GetParameters())
            $privInfo = New-Object Org.BouncyCastle.Asn1.Pkcs.PrivateKeyInfo($algId,$pKey.ToAsn1Object())
            $privSpec = [Org.BouncyCastle.Security.PrivateKeyFactory]::CreateKey($privInfo)
            $pubKey = $pKey.GetPublicKey()

            if ($pubKey -ne $null) {
                $pubInfo = New-Object Org.BouncyCastle.Asn1.X509.SubjectPublicKeyInfo($algId,$pubKey.GetBytes())
                $pubSpec = [Org.BouncyCastle.Security.PublicKeyFactory]::CreateKey($pubInfo)
            } else {
                $pubSpec = [Org.BouncyCastle.Crypto.Generators.ECKeyPairGenerator]::GetCorrespondingPublicKey([Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters]$privSpec)
            }

        } else {
            throw "Unsupported private key type"
        }

        # build the key and return it
        $newKey = New-Object Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair($pubSpec,$privSpec)
        return $newKey

    # certificates
    } elseif ($pemStr -like '*-----BEGIN CERTIFICATE-----*' -and $pemStr -like '*-----END CERTIFICATE-----*') {

        # For certs, we can use the native PemReader to make things easier
        if ('File' -eq $PSCmdlet.ParameterSetName) {
            try {
                $sr = New-Object IO.StreamReader($InputFile)
                $reader = New-Object Org.BouncyCastle.OpenSsl.PemReader($sr)
                $cert = $reader.ReadObject()
            } finally {
                if ($null -ne $sr) { $sr.Close() }
            }
        } else {
            # get the byte array from the pem string
            $base64 = $pemStr.Substring($pemStr.IndexOf('CERTIFICATE-----')+16)
            $base64 = $base64.Substring(0,$base64.IndexOf('-'))
            $certBytes = [Convert]::FromBase64String($base64)

            # let BC parse it
            $certParser = New-Object Org.BouncyCastle.X509.X509CertificateParser
            $cert = $certParser.ReadCertificate($certBytes)
        }
        return $cert

    } else {
        throw "Unsupported PEM type"
    }

}