Private/Get-NMMCertificate.ps1

function Get-NMMCertificate {
    <#
    .SYNOPSIS
        Retrieves a certificate from various storage locations.
    .DESCRIPTION
        Cross-platform certificate retrieval supporting:
        - Windows Certificate Store (CurrentUser/LocalMachine)
        - macOS Keychain
        - PFX file
        - Azure Key Vault
 
        The function auto-detects the platform and uses the appropriate method.
    .PARAMETER Thumbprint
        Certificate thumbprint to find.
    .PARAMETER Subject
        Certificate subject name to find (alternative to thumbprint).
    .PARAMETER Source
        Certificate storage source: CertStore, Keychain, PfxFile, KeyVault.
    .PARAMETER StoreLocation
        Windows cert store location: CurrentUser or LocalMachine.
    .PARAMETER StoreName
        Windows cert store name (default: My).
    .PARAMETER PfxPath
        Path to PFX file.
    .PARAMETER PfxPassword
        Password for PFX file (SecureString).
    .PARAMETER VaultName
        Azure Key Vault name.
    .PARAMETER CertificateName
        Certificate name in Key Vault.
    .PARAMETER KeychainPath
        macOS Keychain path (default: login.keychain-db).
    .OUTPUTS
        [System.Security.Cryptography.X509Certificates.X509Certificate2]
    .EXAMPLE
        Get-NMMCertificate -Thumbprint "ABC123" -Source CertStore
    .EXAMPLE
        Get-NMMCertificate -PfxPath "/path/to/cert.pfx" -PfxPassword $securePass -Source PfxFile
    #>

    [CmdletBinding(DefaultParameterSetName = 'CertStore')]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    param(
        [Parameter(ParameterSetName = 'CertStore')]
        [Parameter(ParameterSetName = 'Keychain')]
        [string]$Thumbprint,

        [Parameter(ParameterSetName = 'CertStore')]
        [Parameter(ParameterSetName = 'Keychain')]
        [string]$Subject,

        [Parameter(Mandatory = $true)]
        [ValidateSet('CertStore', 'Keychain', 'PfxFile', 'KeyVault')]
        [string]$Source,

        [Parameter(ParameterSetName = 'CertStore')]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$StoreLocation = 'CurrentUser',

        [Parameter(ParameterSetName = 'CertStore')]
        [string]$StoreName = 'My',

        [Parameter(Mandatory = $true, ParameterSetName = 'PfxFile')]
        [string]$PfxPath,

        [Parameter(ParameterSetName = 'PfxFile')]
        [SecureString]$PfxPassword,

        [Parameter(Mandatory = $true, ParameterSetName = 'KeyVault')]
        [string]$VaultName,

        [Parameter(Mandatory = $true, ParameterSetName = 'KeyVault')]
        [string]$CertificateName,

        [Parameter(ParameterSetName = 'Keychain')]
        [string]$KeychainPath = 'login.keychain-db'
    )

    process {
        switch ($Source) {
            'CertStore' {
                return Get-CertificateFromStore -Thumbprint $Thumbprint -Subject $Subject -StoreLocation $StoreLocation -StoreName $StoreName
            }
            'Keychain' {
                return Get-CertificateFromKeychain -Thumbprint $Thumbprint -Subject $Subject -KeychainPath $KeychainPath
            }
            'PfxFile' {
                return Get-CertificateFromPfx -PfxPath $PfxPath -PfxPassword $PfxPassword
            }
            'KeyVault' {
                return Get-CertificateFromKeyVault -VaultName $VaultName -CertificateName $CertificateName
            }
        }
    }
}

function Get-CertificateFromStore {
    <#
    .SYNOPSIS
        Retrieves certificate from Windows Certificate Store.
    #>

    [CmdletBinding()]
    param(
        [string]$Thumbprint,
        [string]$Subject,
        [string]$StoreLocation,
        [string]$StoreName
    )

    # Check if running on Windows
    if (-not ($IsWindows -or $PSVersionTable.PSEdition -eq 'Desktop')) {
        throw "Certificate Store is only available on Windows. Use -Source PfxFile or Keychain on other platforms."
    }

    $certPath = "Cert:\$StoreLocation\$StoreName"
    Write-Verbose "Searching certificate store: $certPath"

    $cert = $null

    if ($Thumbprint) {
        $cert = Get-ChildItem -Path $certPath | Where-Object { $_.Thumbprint -eq $Thumbprint } | Select-Object -First 1
        if (-not $cert) {
            throw "Certificate with thumbprint '$Thumbprint' not found in $certPath"
        }
    }
    elseif ($Subject) {
        $cert = Get-ChildItem -Path $certPath | Where-Object { $_.Subject -like "*$Subject*" } | Select-Object -First 1
        if (-not $cert) {
            throw "Certificate with subject containing '$Subject' not found in $certPath"
        }
    }
    else {
        throw "Either -Thumbprint or -Subject must be specified for CertStore source."
    }

    if (-not $cert.HasPrivateKey) {
        throw "Certificate found but does not have a private key. Ensure you have the private key installed."
    }

    Write-Verbose "Found certificate: $($cert.Subject) [Thumbprint: $($cert.Thumbprint)]"
    return $cert
}

function Get-CertificateFromKeychain {
    <#
    .SYNOPSIS
        Retrieves certificate from macOS Keychain.
    #>

    [CmdletBinding()]
    param(
        [string]$Thumbprint,
        [string]$Subject,
        [string]$KeychainPath
    )

    # Check if running on macOS
    if (-not $IsMacOS) {
        throw "Keychain is only available on macOS. Use -Source CertStore on Windows or -Source PfxFile for cross-platform."
    }

    if (-not $Subject -and -not $Thumbprint) {
        throw "Either -Thumbprint or -Subject must be specified for Keychain source."
    }

    Write-Verbose "Searching macOS Keychain: $KeychainPath"

    # Use security command to find and export certificate
    $searchCriteria = if ($Subject) { $Subject } else { $Thumbprint }

    try {
        # Find the certificate identity (cert + private key)
        $identityOutput = & security find-identity -v -p codesigning "$KeychainPath" 2>&1

        # Search for our certificate in the output
        $matchingLine = $identityOutput | Where-Object { $_ -match $searchCriteria }

        if (-not $matchingLine) {
            # Try searching all identities
            $identityOutput = & security find-identity -v "$KeychainPath" 2>&1
            $matchingLine = $identityOutput | Where-Object { $_ -match $searchCriteria }
        }

        if (-not $matchingLine) {
            throw "Certificate with criteria '$searchCriteria' not found in Keychain '$KeychainPath'"
        }

        # Export the certificate and private key to a temporary PKCS12
        $tempPfx = [System.IO.Path]::GetTempFileName() + ".pfx"
        $tempPassword = [guid]::NewGuid().ToString()

        try {
            # Export identity to PKCS12
            & security export -k "$KeychainPath" -t identities -f pkcs12 -P "$tempPassword" -o "$tempPfx" 2>&1 | Out-Null

            if (-not (Test-Path $tempPfx)) {
                throw "Failed to export certificate from Keychain"
            }

            # Load the PKCS12 into X509Certificate2
            $securePassword = ConvertTo-SecureString -String $tempPassword -AsPlainText -Force
            $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
                $tempPfx,
                $securePassword,
                [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
            )

            # Verify it's the right certificate
            if ($Thumbprint -and $cert.Thumbprint -ne $Thumbprint) {
                throw "Exported certificate thumbprint doesn't match requested thumbprint"
            }

            Write-Verbose "Found certificate: $($cert.Subject) [Thumbprint: $($cert.Thumbprint)]"
            return $cert
        }
        finally {
            # Clean up temp file
            if (Test-Path $tempPfx) {
                Remove-Item $tempPfx -Force
            }
        }
    }
    catch {
        throw "Failed to retrieve certificate from Keychain: $_"
    }
}

function Get-CertificateFromPfx {
    <#
    .SYNOPSIS
        Loads certificate from PFX file.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$PfxPath,

        [SecureString]$PfxPassword
    )

    if (-not (Test-Path $PfxPath)) {
        throw "PFX file not found: $PfxPath"
    }

    Write-Verbose "Loading certificate from PFX: $PfxPath"

    try {
        $flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable

        if ($PfxPassword) {
            $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $PfxPassword, $flags)
        }
        else {
            $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $null, $flags)
        }

        if (-not $cert.HasPrivateKey) {
            throw "PFX file does not contain a private key."
        }

        Write-Verbose "Loaded certificate: $($cert.Subject) [Thumbprint: $($cert.Thumbprint)]"
        return $cert
    }
    catch {
        throw "Failed to load PFX file: $_"
    }
}

function Get-CertificateFromKeyVault {
    <#
    .SYNOPSIS
        Retrieves certificate from Azure Key Vault.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$VaultName,

        [Parameter(Mandatory)]
        [string]$CertificateName
    )

    Write-Verbose "Retrieving certificate '$CertificateName' from Key Vault '$VaultName'"

    # Check if Az.KeyVault module is available
    if (-not (Get-Module -ListAvailable -Name Az.KeyVault)) {
        throw "Az.KeyVault module is required for Key Vault certificate retrieval. Install with: Install-Module Az.KeyVault"
    }

    try {
        # Import module if not already loaded
        Import-Module Az.KeyVault -ErrorAction Stop

        # Get certificate with private key
        $kvCert = Get-AzKeyVaultCertificate -VaultName $VaultName -Name $CertificateName -ErrorAction Stop

        if (-not $kvCert) {
            throw "Certificate '$CertificateName' not found in Key Vault '$VaultName'"
        }

        # Get the secret (contains private key)
        $secret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $CertificateName -AsPlainText -ErrorAction Stop

        # Convert from base64 to certificate
        $certBytes = [System.Convert]::FromBase64String($secret)
        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
            $certBytes,
            [string]::Empty,
            [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
        )

        if (-not $cert.HasPrivateKey) {
            throw "Key Vault certificate does not contain a private key."
        }

        Write-Verbose "Retrieved certificate: $($cert.Subject) [Thumbprint: $($cert.Thumbprint)]"
        return $cert
    }
    catch {
        throw "Failed to retrieve certificate from Key Vault: $_"
    }
}