Cryptography/New-RsaKeyPair.ps1

# Copyright 2012 Aaron Jensen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function New-RsaKeyPair
{
    <#
    .SYNOPSIS
    Generates a public/private RSA key pair.
 
    .DESCRIPTION
    Uses the `makecert.exe` and `pvk2pfx.exe` programs to generate a public/private RSA key pair, and saves each to files of your choosing. The public key is saved as an X509Certificate. The private key is saved as a PFX file. Both can be loaded by .NET's `X509Certificate` class. Returns `System.IO.FileInfo` objects for the public and private key, in that order.
 
    You will be prompted for the private key password. Once when creating the private key, once to save it to a file, and finally to export it to a PFX file. Sorry about that: the `makecert.exe` tool doesn't have an password command-line parameter. The first two prompts will be GUIs, so you can't run this command headless. To create a password-less private key, click "None" when prompted for the private key password, and leave the other password prompts blank.
 
    `makecert.exe` and `pvk2pfx.exe` are part of the Windows SDK. They can be downloaded from the following locations:
 
        * [Windows 7](http://www.microsoft.com/en-us/download/details.aspx?id=8279)
        * [Windows 8](http://msdn.microsoft.com/en-us/windows/desktop/hh852363.aspx)
        * [Windows 8.1](http://msdn.microsoft.com/en-us/windows/desktop/bg162891.aspx)
 
    .OUTPUTS
    System.IO.FileInfo
 
    .LINK
    http://www.microsoft.com/en-us/download/details.aspx?id=8279
 
    .LINK
    http://msdn.microsoft.com/en-us/windows/desktop/hh852363.aspx
 
    .LINK
    http://msdn.microsoft.com/en-us/windows/desktop/bg162891.aspx
 
    .EXAMPLE
    New-RsaKeyPair -Subject 'CN=MyName' -PublicKeyFile 'MyName.cer' -PrivateKeyFile 'MyName.pfx'
 
    Demonstrates the minimal parameters needed to generate a key pair. The key will use a sha512 signing algorithm, have a length of 4096 bits, expire on `DateTime::MaxValue`, as an `individual` authority. The public key will be saved in the current directory as `MyName.cer`. The private key will be saved to the current directory as `MyName.pfx`.
 
    .EXAMPLE
    New-RsaKeyPair -Subject 'CN=MyName' -PublicKeyFile 'MyName.cer' -PrivateKeyFile 'MyName.pfx' -Algorithm 'sha1' -ValidFrom (Get-Date -Year 2015 -Month 1 -Day 1) -ValidTo (Get-Date -Year 2015 -Month 12 -Day 31) -Length 1024 -Authority 'commercial'
 
    Demonstrates how to use all the parameters to create a truly customized key pair. The generated certificate will use the sha1 signing algorithm, becomes effective 1/1/2015, expires 12/31/2015, is 1024 bits in length, as specifies `commercial` as the signing authority.
    #>

    [OutputType([IO.FileInfo])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]
        # The key's subject. Should be of the form `CN=Name,OU=Name,O=SuperMagicFunTime,ST=OR,C=US`. Only the `CN=Name` part is required.
        $Subject,

        [ValidateSet('md5','sha1','sha256','sha384','sha512')]
        [string]
        # The signature algorithm. Default is `sha512`.
        $Algorithm = 'sha512',

        [DateTime]
        # The date/time the keys will become valid. Default is now.
        $ValidFrom = (Get-Date),

        [DateTime]
        # The date/time the keys should expire. Default is `DateTime::MaxValue`.
        $ValidTo = ([DateTime]::MaxValue),

        [int]
        # The length, in bits, of the generated key length. Default is `4096`.
        $Length = 4096,

        [ValidateSet('commercial','individual')]
        [string]
        # The signing authority of the certificate. Must be `commercial` (for certificates used by commercial software publishers) or `individual`, for certificates used by individual software publishers. Default is `individual`.
        $Authority = 'individual',

        [Parameter(Mandatory=$true,Position=1)]
        [string]
        # The file where the public key should be stored. Saved as an X509 certificate.
        $PublicKeyFile,

        [Parameter(Mandatory=$true,Position=2)]
        [string]
        # The file where the private key should be stored. The private key will be saved as an X509 certificate in PFX format and will include the public key.
        $PrivateKeyFile,

        [Switch]
        # Overwrites `PublicKeyFile` and/or `PrivateKeyFile`, if they exist.
        $Force
    )

    Set-StrictMode -Version 'Latest'

    function Find-WindowsSdkCommand
    {
        param(
            [Parameter(Mandatory=$true)]
            [string]
            $Name
        )

        Set-StrictMode -Version 'Latest'

        $item = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Microsoft SDKs\Windows' | 
                    Get-ItemProperty -Name 'InstallationFolder' |
                    Select-Object -ExpandProperty 'InstallationFolder' |
                    ForEach-Object { Join-Path -Path $_ -ChildPath ('Bin\{0}' -f $Name) } |
                    Where-Object { Test-Path -Path $_ -PathType Leaf } |
                    Get-Item |
                    Sort-Object -Property { $_.VersionInfo.ProductVersion } -Descending |
                    Select-Object -First 1

        if( -not $item )
        {
            Write-Error ('Command ''{0}'' not found. Please install a Windows SDK.' -f $Name)
            return
        }
        Write-Verbose ('{0} v{1}' -f $item.FullName,$item.VersionInfo.ProductVersion)
     
         $item   
    }

    function Resolve-KeyPath
    {
        param(
            [Parameter(Mandatory=$true)]
            [string]
            $Path
        )

        Set-StrictMode -Version 'Latest'

        $Path = Resolve-FullPath -Path $Path

        if( (Test-Path -Path $Path -PathType Leaf) )
        {
            if( -not $Force )
            {
                Write-Error ('File ''{0}'' exists. Use the -Force switch to overwrite.' -f $Path)
                return
            }
        }
        else
        {
            $root = Split-Path -Parent -Path $Path
            if( -not (Test-Path -Path $root -PathType Container) )
            {
                New-Item -Path $root -ItemType 'Directory' -Force -Verbose:$VerbosePreference | Out-Null
            }
        }

        return $Path
    }

    $PublicKeyFile = Resolve-KeyPath -Path $PublicKeyFile
    if( -not $PublicKeyFile )
    {
        return
    }

    $PrivateKeyFile = Resolve-KeyPath -Path $PrivateKeyFile
    if( -not $PrivateKeyFile )
    {
        return
    }

    if( (Test-Path -Path $PrivateKeyFile -PathType Leaf) )
    {
        if( -not $Force )
        {
            Write-Error ('Private key file ''{0}'' exists. Use the -Force switch to overwrite.' -f $PrivateKeyFile)
            return
        }
    }

    $tempDir = 'Carbon+NewRsaKeyPair+{0}+' -f ([IO.Path]::GetRandomFileName())
    $tempDir = Join-Path -Path $env:TEMP -ChildPath $tempDir
    New-Item -Path $tempDir -ItemType 'Directory' -Verbose:$VerbosePreference | Out-Null

    try
    {
        $makeCert = Find-WindowsSdkCommand 'makecert.exe'
        if( -not $makeCert )
        {
            return
        }

        $pvk2pfx = Find-WindowsSdkCommand 'pvk2pfx.exe'
        if( -not $pvk2pfx )
        {
            return
        }

        $privateKeyPath = Join-Path -Path $tempDir -ChildPath 'private.pkv'
        $output = & $makeCert.FullName -r `
                             -a $Algorithm `
                             -sky 'exchange' `
                             -n $Subject `
                             -pe `
                             -sv $privateKeyPath `
                             -len $Length `
                             -b ($ValidFrom.ToString('MM/dd/yyyy')) `
                             -e ($ValidTo.ToString('MM/dd/yyyy')) `
                             '-$' $Authority `
                             $PublicKeyFile
        if( $LASTEXITCODE )
        {
            Write-Error ('Failed to create public/private key pair:{0}{1}' -f ([Environment]::NewLine),($output -join ([Environment]::NewLine)))
            return
        }
        else
        {
            $output | Write-Verbose
        }

        $password = Read-Host -Prompt 'Enter private key password' -AsSecureString
        $password = Convert-SecureStringToString $password
        $passwordArgName = ''
        $passwordArgValue = ''
        if( $password )
        {
            $passwordArgName = '-pi'
            $passwordArgValue = $password
        }

        $output = & $pvk2pfx.FullName -pvk $privateKeyPath -spc $PublicKeyFile -pfx $PrivateKeyFile $passwordArgName $passwordArgValue -f
        if( $LASTEXITCODE )
        {
            Write-Error ('Failed to create PFX file for public/key pair:{0}{1}' -f ([Environment]::NewLine),($output -join ([Environment]::NewLine)))
            return
        }
        else
        {
            $output | Write-Verbose
        }

        Get-Item $PublicKeyFile
        Get-Item $PrivateKeyFile
    }
    finally
    {
        Remove-Item -Path $tempDir -Recurse
    }
}