Public/Install-Certificate.ps1

using namespace System.IO
using namespace System.Security.AccessControl
using namespace System.Security.Cryptography
using namespace System.Security.Cryptography.X509Certificates

function Install-Certificate {
    <#
        .SYNOPSIS
        Imports a certificate into the Windows certificate store.

        .DESCRIPTION
        Installs an X.509 certificate from a specified file into a specified
        certificate store location, and grants persistent read permissions on the
        private key to the specified user, so that the key remains accessible
        after a reboot.

        .PARAMETER FilePath
        Path to the certificate file to import. Supports .pfx and PEM-encoded formats
        such as .crt.

        .PARAMETER PrivateKeyPath
        Path to a PEM-encoded private key file to associate with the certificate.
        Only applicable for PEM certificates; ignored for .pfx files.

        .PARAMETER StoreLocation
        Specifies the certificate store location where the certificate will be installed.
        Accepted values are defined here:
        https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.storelocation

        .PARAMETER StoreName
        Specifies the name of the X.509 certificate store to open.
        Accepted values are defined here:
        https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.storename

        .PARAMETER Password
        Password used to decrypt the certificate or private key file, if encrypted.

        .PARAMETER User
        Specifies the user context under which the certificate will be installed.
        The default value is the current domain user.

        .INPUTS
        None. You can't pipe objects to Install-Certificate.

        .OUTPUTS
        X509Certificate. Returns an object representing the installed X.509 certificate.

        .EXAMPLE
        PS>$Certificate = "./path/to/certificate.pfx"
        PS>$Password = Read-Host -Prompt "Password" -AsSecureString
        PS>Install-Certificate -FilePath $Certificate -StoreLocation LocalMachine -Password $Password

        Installs the certificate.pfx certificate into the LocalMachine certificate
        store using the specified password.
    #>

    [OutputType([X509Certificate2])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $FilePath,

        [Parameter(Mandatory)]
        [StoreLocation] $StoreLocation,

        [Parameter()]
        [string] $PrivateKeyPath,

        [Parameter(Mandatory)]
        [StoreName] $StoreName,

        [SecureString] $Password,

        [string] $User = "$env:USERDOMAIN\$env:USERNAME"
    )
    begin {
        if (!$IsWindows) {
            Write-Error "This Cmdlet only works on the Windows Operating System" `
                -Category NotImplemented `
                -ErrorAction Stop
        }

        $IsPfx = [Path]::GetExtension($FilePath) -ieq ".pfx"

        $Arguments = @{
            CertStoreLocation = "Cert:\$StoreLocation\$StoreName"
            FilePath = $FilePath
        }

        if ($null -ne $Password) {
            $Arguments.Add("Password", $Password)
        }
    }
    process {
        $Certificate = if ($IsPfx) {
            Import-PfxCertificate @Arguments
        } else {
            $CertificateStore = [X509Store]::new($StoreName, $StoreLocation)

            try {
                $HasPassword = $null -ne $Password
                $CertificateStore.Open([OpenFlags]::ReadWrite)

                $PemCertificate = if($HasPassword) {
                    [X509Certificate2]::new($FilePath, $Password, [X509KeyStorageFlags]::DefaultKeySet)
                } else {
                    [X509Certificate2]::new($FilePath)
                }

                if (![string]::IsNullOrEmpty($PrivateKeyPath)) {
                    $PemContent = [File]::ReadAllText($PrivateKeyPath)
                    $PrivateKey = [RSA]::Create()

                    if ($HasPassword) {
                        # NOTE: ImportFromEncryptedPem does not implement an overload for SecureString
                        $PlaintextPassword = ConvertFrom-SecureString -SecureString $Password -AsPlainText
                        $PrivateKey.ImportFromEncryptedPem($PemContent, $PlaintextPassword)
                    } else {
                        $PrivateKey.ImportFromPem($PemContent)
                    }

                    $PemCertificate = [RSACertificateExtensions]::CopyWithPrivateKey($PemCertificate, $PrivateKey)
                    $PrivateKey.Dispose()

                    # RSA.Create() produces an ephemeral key with no CNG key container. Re-importing via PFX round-trip
                    # forces Windows to persist the key in the machine key store, giving it a UniqueName for ACL assignment.
                    $Exported = $PemCertificate.Export([X509ContentType]::Pfx)
                    $PemCertificate = [X509Certificate2]::new($Exported, $null, [X509KeyStorageFlags]::MachineKeySet -bor [X509KeyStorageFlags]::PersistKeySet)
                    Write-Verbose "Retrieved certificate with thumbprint '$($PemCertificate.Thumbprint)'."
                }

                $CertificateStore.Add($PemCertificate)
            }
            catch [ArgumentException], [CryptographicException] {
                Write-Error "Failed to load certificate '$FilePath' to the certificate store: $_" `
                    -Category InvalidData `
                    -ErrorAction Stop
            }
            finally {
                $CertificateStore.Close()
                $CertificateStore.Dispose()
            }

            # If no private key is provided, the certificate will only contain the public key.
            Write-Output $PemCertificate
        }

        # Use the GetRSAPrivateKey extension method, which returns an implementation-agnostic abstract base class.
        # On Windows the private key is backed by an RSACng instance (Cryptography Next Generation); the legacy
        # PrivateKey property is avoided because it returns platform-specific types (RSAOpenSsl on Linux/macOS).
        $PrivateKey = [RSACertificateExtensions]::GetRSAPrivateKey($Certificate)

        if ($null -eq $PrivateKey) {
            Write-Error "The certificate '$FilePath' has no private key." `
                -Category ObjectNotFound `
                -ErrorAction Stop
        }

        $UniqueName = $PrivateKey.Key.UniqueName
        Write-Verbose "Detected Unique Name '$UniqueName'."

        $AclPath = if ($IsPfx) {
            "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$UniqueName"
        } else {
            "$env:ProgramData\Microsoft\Crypto\Keys\$UniqueName"
        }

        # Grant persistent read permissions to the domain user, so that the certificate doesn't need to
        # be re-installed after a reboot.
        $Acl = Get-Acl -Path $AclPath
        $Rule = [FileSystemAccessRule]::new($User, [FileSystemRights]::Read, [AccessControlType]::Allow)
        $Acl.AddAccessRule($Rule)
        Set-Acl -Path $AclPath -AclObject $Acl
    }
    end {
        Write-Output $Certificate
    }
}