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
function Import-Pem {
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName='File',Mandatory,Position=0)]
        [string]$InputFile,
        [Parameter(ParameterSetName='String',Mandatory)]
        [string]$InputString
    )

    # BouncyCastle has a lovely PemReader class that can spit out certs, keys, csrs, etc.
    # We have to do a little extra work for PKCS8 encoded private keys though.

    if ('File' -eq $PSCmdlet.ParameterSetName) {
        # normalize the file path and read it in
        $InputFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InputFile)
        $pemStr = Get-Content $InputFile -Raw
    } else {
        # This should be a single string value with all of the line breaks intact
        $pemStr = $InputString
    }

    # parse the PEM
    try {
        $sr = [IO.StringReader]::new($pemStr)
        $reader = [Org.BouncyCastle.OpenSsl.PemReader]::new($sr)
        $pemObj = $reader.ReadObject()
    } finally {
        if ($null -ne $sr) { $sr.Close() }
    }

    if ($pemObj -is [Org.BouncyCastle.X509.X509Certificate] -or
        $pemObj -is [Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest] -or
        $pemObj -is [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair])
    {
        # Certs, Requests, and private keys that get parsed as a full key pair
        # are all things we can just return as-is
        Write-Debug "PemReader found '$($pemObj.GetType())'. Returning as-is"
        return $pemObj
    }
    elseif ($pemObj -is [Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters]) {
        # A PKCS8 encoded RSA private key comes out as just the private key
        # parameters. We have to generate the public key parameters and fold
        # them into a full key pair object.
        Write-Debug "PemReader found '$($pemObj.GetType())'. Attempting to convert to AsymmetricCipherKeyPair."

        $pubSpec = [Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters]::new(
            $false,$pemObj.Modulus,$pemObj.PublicExponent
        )

        # $pemObj is our private parameters
        return [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]::new($pubSpec,$pemObj)
    }
    elseif ($pemObj -is [Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters]) {
        # A PKCS8 encoded EC private key comes out as just the key parameters
        # We have to transform them into a full key pair object
        Write-Debug "PemReader found '$($pemObj.GetType())'. Attempting to convert to AsymmetricCipherKeyPair."

        $multiplier = [Org.BouncyCastle.Math.EC.Multiplier.FixedPointCombMultiplier]::new()
        $q = $multiplier.Multiply($pemObj.Parameters.G, $pemObj.D)
        $pubSpec = [Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters]::new(
            $pemObj.AlgorithmName, $q, $pemObj.PublicKeyParamSet
        )

        # $pemObj is our private parameters
        return [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]::new($pubSpec,$pemObj)
    }
    else {
        # not sure what we ended up with
        throw "PemReader found unsupported '$($pemObj.GetType())'."
    }

    return $pemObj

    # Old info we don't need anymore

    # 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

    # 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

}