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', [Parameter(ParameterSetName = 'Keychain')] [string]$FallbackPfxPath, [Parameter(ParameterSetName = 'Keychain')] [SecureString]$FallbackPfxPassword ) 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 -FallbackPfxPath $FallbackPfxPath -FallbackPfxPassword $FallbackPfxPassword } '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. .DESCRIPTION Uses Swift scripts to properly access macOS Keychain identities, including the modern data protection keychain. Falls back to PFX file if provided. The standard 'security' command-line tool cannot access identities in the data protection keychain, so we use Swift with the Security framework. #> [CmdletBinding()] param( [string]$Thumbprint, [string]$Subject, [string]$KeychainPath, [string]$FallbackPfxPath, [SecureString]$FallbackPfxPassword ) # 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 for certificate" # Get path to Swift tools $toolsPath = Join-Path $PSScriptRoot "Tools" $exportScript = Join-Path $toolsPath "ExportIdentity.swift" # Check if Swift is available $swiftPath = Get-Command swift -ErrorAction SilentlyContinue if (-not $swiftPath) { Write-Warning "Swift not found. Install Xcode Command Line Tools: xcode-select --install" Write-Warning "Falling back to PFX file if available..." if ($FallbackPfxPath -and (Test-Path $FallbackPfxPath)) { return Get-CertificateFromPfx -PfxPath $FallbackPfxPath -PfxPassword $FallbackPfxPassword } throw "Swift is required for Keychain access on macOS. Install with: xcode-select --install" } # Check if our Swift export script exists if (-not (Test-Path $exportScript)) { Write-Warning "Swift export tool not found at $exportScript" if ($FallbackPfxPath -and (Test-Path $FallbackPfxPath)) { Write-Verbose "Falling back to PFX file: $FallbackPfxPath" return Get-CertificateFromPfx -PfxPath $FallbackPfxPath -PfxPassword $FallbackPfxPassword } throw "Keychain export tool not found and no fallback PFX provided." } try { # Export identity to temp PFX using Swift $tempPfx = [System.IO.Path]::GetTempFileName() + ".pfx" $tempPassword = [guid]::NewGuid().ToString() $searchParam = if ($Thumbprint) { $Thumbprint.ToUpper() } else { $Subject } Write-Verbose "Exporting identity from Keychain using Swift..." $result = & swift $exportScript $searchParam $tempPfx $tempPassword 2>&1 if ($result -match '^SUCCESS:([^:]+):(.+)$') { $foundThumbprint = $Matches[1] $foundSubject = $Matches[2] Write-Verbose "Found identity: $foundSubject [Thumbprint: $foundThumbprint]" # Load the exported PFX if (Test-Path $tempPfx) { $securePassword = ConvertTo-SecureString -String $tempPassword -AsPlainText -Force $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $tempPfx, $securePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ) Write-Verbose "Loaded certificate: $($cert.Subject) [Thumbprint: $($cert.Thumbprint)]" return $cert } } elseif ($result -match '^ERROR:(.+)$') { $errorMsg = $Matches[1] Write-Verbose "Swift export failed: $errorMsg" # Try fallback to PFX if ($FallbackPfxPath -and (Test-Path $FallbackPfxPath)) { Write-Warning "Identity not found in Keychain. Falling back to PFX file." return Get-CertificateFromPfx -PfxPath $FallbackPfxPath -PfxPassword $FallbackPfxPassword } throw "Identity not found in Keychain: $errorMsg" } else { Write-Verbose "Unexpected Swift output: $result" if ($FallbackPfxPath -and (Test-Path $FallbackPfxPath)) { Write-Warning "Keychain access failed. Falling back to PFX file." return Get-CertificateFromPfx -PfxPath $FallbackPfxPath -PfxPassword $FallbackPfxPassword } throw "Failed to export identity from Keychain: $result" } } finally { # Clean up temp file securely if (Test-Path $tempPfx) { [System.IO.File]::WriteAllBytes($tempPfx, [byte[]]::new(1024)) Remove-Item $tempPfx -Force -ErrorAction SilentlyContinue } } } 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: $_" } } |