@@ -140,20 +140,54 @@ function Add-DbaComputerCertificate {
140140 }
141141
142142 Write-Message - Level Verbose - Message " Flags: $flags "
143+
144+ # Track if we're dealing with a certificate collection from a file
145+ $isCollection = $false
146+ $collectionData = $null
147+
143148 if ($Path ) {
144149 if (-not (Test-Path - Path $Path )) {
145150 Stop-Function - Message " Path ($Path ) does not exist." - Category InvalidArgument
146151 return
147152 }
148153
149154 try {
150- # local has to be exportable to export to remote
151- $bytes = [System.IO.File ]::ReadAllBytes($Path )
155+ # Read file bytes and import locally to get certificate collection
156+ $fileBytes = [System.IO.File ]::ReadAllBytes($Path )
157+
152158 # Use X509Certificate2Collection to import the full certificate chain
153159 $certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
154- $null = $certCollection.Import ($bytes , $SecurePassword , " Exportable, PersistKeySet" )
155- # Convert collection to array for processing
156- $Certificate = @ ($certCollection )
160+
161+ # Handle password conversion for password-protected certificates (PFX files)
162+ $plainPassword = $null
163+ $ptr = [IntPtr ]::Zero
164+
165+ if ($SecurePassword ) {
166+ # Convert SecureString to plain text password for import/export operations
167+ # Using plain text for both Import() and Export() in all PowerShell versions
168+ # This is standard practice for .NET certificate operations
169+ $ptr = [System.Runtime.InteropServices.Marshal ]::SecureStringToGlobalAllocUnicode($SecurePassword )
170+ $plainPassword = [System.Runtime.InteropServices.Marshal ]::PtrToStringUni($ptr )
171+ }
172+
173+ try {
174+ # Import using plain text password (or null for non-password-protected certificates)
175+ # Works reliably in all PowerShell versions v3+
176+ $null = $certCollection.Import ($fileBytes , $plainPassword , " Exportable, PersistKeySet" )
177+
178+ # Export the entire collection as a single PFX to preserve the chain
179+ # This re-exports with the password, creating a fresh encrypted byte array that can be passed to remote
180+ $collectionData = $certCollection.Export ([System.Security.Cryptography.X509Certificates.X509ContentType ]::PFX, $plainPassword )
181+ $isCollection = $true
182+
183+ # Still set $Certificate so the process block knows we have something to process
184+ $Certificate = @ ($certCollection )
185+ } finally {
186+ # Always clean up the plain text password from memory
187+ if ($ptr -ne [IntPtr ]::Zero) {
188+ [System.Runtime.InteropServices.Marshal ]::ZeroFreeGlobalAllocUnicode($ptr )
189+ }
190+ }
157191 } catch {
158192 Stop-Function - Message " Can't import certificate." - ErrorRecord $_
159193 return
@@ -164,15 +198,15 @@ function Add-DbaComputerCertificate {
164198 $scriptBlock = {
165199 param (
166200 $CertificateData ,
167- [SecureString ] $SecurePassword ,
201+ [string ] $PlainPassword ,
168202 $Store ,
169203 $Folder ,
170204 $flags
171205 )
172206
173207 # Use X509Certificate2Collection to import the full certificate chain
174208 $certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
175- $certCollection.Import ($CertificateData , $SecurePassword , $flags )
209+ $certCollection.Import ($CertificateData , $PlainPassword , $flags )
176210
177211 Write-Verbose - Message " Importing certificate chain to $Folder \$Store using flags: $flags "
178212 $tempStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($Folder , $Store )
@@ -201,26 +235,62 @@ function Add-DbaComputerCertificate {
201235 return
202236 }
203237
204- foreach ($cert in $Certificate ) {
205- try {
206- $certData = $cert.Export ([System.Security.Cryptography.X509Certificates.X509ContentType ]::PFX, $SecurePassword )
207- } catch {
208- Stop-Function - Message " Can't export certificate" - ErrorRecord $_ - Continue
209- }
238+ # Convert SecureString to plain text for passing to remote scriptblock
239+ # (PowerShell remoting encrypts the connection, so this is safe)
240+ $plainPassword = $null
241+ $ptr = [IntPtr ]::Zero
242+
243+ if ($SecurePassword ) {
244+ $ptr = [System.Runtime.InteropServices.Marshal ]::SecureStringToGlobalAllocUnicode($SecurePassword )
245+ $plainPassword = [System.Runtime.InteropServices.Marshal ]::PtrToStringUni($ptr )
246+ }
210247
211- foreach ($computer in $ComputerName ) {
212- if ($PSCmdlet.ShouldProcess (" $computer " , " Attempting to import cert" )) {
213- if ($flags -contains " UserProtected" -and -not $computer.IsLocalHost ) {
214- Stop-Function - Message " UserProtected flag is only valid for localhost because it causes a prompt, skipping for $computer " - Continue
248+ try {
249+ # If we have a collection from a file, import it as a single unit to preserve the chain
250+ if ($isCollection -and $collectionData ) {
251+ foreach ($computer in $ComputerName ) {
252+ if ($PSCmdlet.ShouldProcess (" $computer " , " Attempting to import cert collection" )) {
253+ if ($flags -contains " UserProtected" -and -not $computer.IsLocalHost ) {
254+ Stop-Function - Message " UserProtected flag is only valid for localhost because it causes a prompt, skipping for $computer " - Continue
255+ }
256+ try {
257+ Invoke-Command2 - ComputerName $computer - Credential $Credential - ArgumentList $collectionData , $plainPassword , $Store , $Folder , $flags - ScriptBlock $scriptBlock - ErrorAction Stop |
258+ Select-DefaultView - Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer
259+ } catch {
260+ Stop-Function - Message " Failure" - ErrorRecord $_ - Target $computer - Continue
261+ }
215262 }
263+ }
264+ } else {
265+ # Handle individual certificates from pipeline
266+ foreach ($cert in $Certificate ) {
216267 try {
217- Invoke-Command2 - ComputerName $computer - Credential $Credential - ArgumentList $certdata , $SecurePassword , $Store , $Folder , $flags - ScriptBlock $scriptBlock - ErrorAction Stop |
218- Select-DefaultView - Property FriendlyName , DnsNameList , Thumbprint , NotBefore , NotAfter , Subject , Issuer
268+ # Export requires plain text password
269+ $certData = $cert .Export ([ System.Security.Cryptography.X509Certificates.X509ContentType ]::PFX , $plainPassword )
219270 } catch {
220- Stop-Function - Message " Failure" - ErrorRecord $_ - Target $computer - Continue
271+ Stop-Function - Message " Can't export certificate" - ErrorRecord $_ - Continue
272+ }
273+
274+ foreach ($computer in $ComputerName ) {
275+ if ($PSCmdlet.ShouldProcess (" $computer " , " Attempting to import cert" )) {
276+ if ($flags -contains " UserProtected" -and -not $computer.IsLocalHost ) {
277+ Stop-Function - Message " UserProtected flag is only valid for localhost because it causes a prompt, skipping for $computer " - Continue
278+ }
279+ try {
280+ Invoke-Command2 - ComputerName $computer - Credential $Credential - ArgumentList $certdata , $plainPassword , $Store , $Folder , $flags - ScriptBlock $scriptBlock - ErrorAction Stop |
281+ Select-DefaultView - Property FriendlyName, DnsNameList, Thumbprint, NotBefore, NotAfter, Subject, Issuer
282+ } catch {
283+ Stop-Function - Message " Failure" - ErrorRecord $_ - Target $computer - Continue
284+ }
285+ }
221286 }
222287 }
223288 }
289+ } finally {
290+ # Always clean up the plain text password from memory
291+ if ($ptr -ne [IntPtr ]::Zero) {
292+ [System.Runtime.InteropServices.Marshal ]::ZeroFreeGlobalAllocUnicode($ptr )
293+ }
224294 }
225295 }
226296}
0 commit comments