none
Remove certificates from Active Directory (but not all of them) RRS feed

  • Question

  • I have a customer who has piles of users that have more than one published certificate. There is never a situation where they should have more than one published in Active Directory. I can obtain the list of certificates and even identify the certificates I want to remove, but I cannot figure out how to write the good certificate back to Active Directory, or delete just one certificate. Below is my example. I am restricted to ADSI provider. RSAT is not an option.

    I have tried two methods so far. I tried just writting the good certificate to the value after looking through all the certificates (not shown in the functional example)

    $User.putex(1,"userCertificate",$CertificateObject)

    I have also tried deleting the certificates that should no longer be published (Current attempt below)

    Function CheckUserCertificate {
        [cmdletbinding(SupportsShouldProcess=$True)]
        Param(
            [Parameter(Mandatory=$True)][string]$Path
        )
        Begin{
            $ErrorActionPreference="SilentlyContinue"
        } #End Begin
        Process{
            $User = [adsi]$Path
            Try{
                $CertificateBlob = $User.getex("userCertificate")
            }
            Catch{}
            ForEach ($CertificateObject in $CertificateBlob){
                Write-Verbose "Getting Certificate from AD for $($User.name)"
                [Array]$Certs += new-object System.Security.Cryptography.X509Certificates.X509Certificate2(,$CertificateObject) | sort NotBefore

            }
            If ($Certs.count -gt 1){
                Write-Verbose "$($User.Name) has more than one certificate"
                $C = 1
                ForEach ($Cert in $Certs){
                    #Exporting the certificate to a file in case something goes wrong.
                    $Bytes = $Cert.Export("Cert")
                    [system.IO.file]::Writeallbytes(($OutputPath + "$(get-date -Format yyyyMMddHHmmss)-$($User.name)-$C.cer"),$Bytes)
                    
                    #We sorted by NotBefore. Only the first one should be kept.
                    If ($C -gt 1){
                        Write-Verbose "Deleting Certificate $Cert"
                        $ErrorActionPreference="Continue" #Just here for trouble shooting
                        $Error.clear()                    #Just here for trouble shooting
                        $User.putex(4,"userCertificate",$CertificateObject)
                        $User.SetInfo
                        $Error                            #Just here for trouble shooting
                    }
                    $C++
                  
                } #End ForEach Loop through Certificates
            } #End of If Certs Greater than one.
        } #End process
        End{
            #Nothing here yet because I have not decided what to return
            #Work in progress.
        } # End End
    } # End CheckUserCertificate Function

    Can someone point me to what I am missing?


    • Edited by Oldguard Tuesday, August 26, 2014 3:12 PM
    Tuesday, August 26, 2014 3:11 PM

Answers

  • My friendly neighborhood programmer pointed out the fix for this...

    $user.userCertificate.value = $bytes

    I will post a full example when I clean it up a bit.

    • Marked as answer by Oldguard Wednesday, August 27, 2014 8:25 PM
    Wednesday, August 27, 2014 8:24 PM

All replies

  • Do not turn off errors.

    We do not use End to return objects.  Just return them to the pipeline.

    What is your specific question?  You seem to be asking a lot of questions rolled into one blob.


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 3:29 PM
  • Only one question...

    How do I remove a single certificate from a list of certificates in the userCertificate attribute using the ADSI provider?

    Tuesday, August 26, 2014 3:32 PM
  • This is closer to what you want and it uses the try/catch correctly.

    Function CheckUserCertificate {
        Param(
            [Parameter(Mandatory=$True)]
            [string]$Path
        )
    
        Process{
            Try{
                $User = [adsi]$Path
                $CertificateBlob = $User.getex("userCertificate")
                ForEach ($CertificateObject in $CertificateBlob){
                    Write-Verbose "Getting Certificate from AD for $($User.name)"
                    [Array]$Certs += new-object System.Security.Cryptography.X509Certificates.X509Certificate2(,$CertificateObject) | sort NotBefore
    
                }
                If($Certs.count -gt 1){
                    Write-Verbose "$($User.Name) has more than one certificate"
                    ForEach ($Cert in $Certs){
                        #Exporting the certificate to a file in case something goes wrong.
                        $Bytes = $Cert.Export("Cert")
                        [system.IO.file]::Writeallbytes(($OutputPath + "$(get-date -Format yyyyMMddHHmmss)-$($User.name)-$C.cer"),$Bytes)
    				}   
                    #We sorted by NotBefore. Only the first one should be kept.
                    $User.putex(4,"userCertificate",$Certs[0])
                    $User.SetInfo                  
                }else{
                    Write-Verbose "$($User.Name) has only one certificate"
            }
            
            Catch{
                Write-Verbose "$($User.Name) has thrown an error"
                Write-Error $_
            }
            
        } #End process
    } 
    
    
    


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 3:40 PM
  • Only one question...

    How do I remove a single certificate from a list of certificates in the userCertificate attribute using the ADSI provider?

    http://en-us.sysadmins.lv/Lists/Posts/Post.aspx?ID=49

    The certs are an array.  Remove elements and write whole remaining array back to AD.


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 4:11 PM
  • Wouldn't $Certs[0] be the one I want to keep?

    $Cert[0] seems like it would be good, but when I try that it doesn't work. The Value of $Certs[$C-1] also looks good to me... I guess I am still a little off track.

    Tuesday, August 26, 2014 4:16 PM
  • I should probably add that that Certs and Cert are X509Certificate2 objects and not the original byte format I started with. The byte format is $CertificateBlob which I will go try now in case that is what I should have been using.
    Tuesday, August 26, 2014 4:20 PM
  • I should probably add that that Certs and Cert are X509Certificate2 objects and not the original byte format I started with. The byte format is $CertificateBlob which I will go try now in case that is what I should have been using.

    You need to use the byte version and it needs to be awrapped in an array.

    @($cert)

    OR

    @($certs[0]))


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 4:25 PM
  • Nothing but failures

    Here are a list of my failures for your amusement.

    $User.putex(4,"userCertificate",@($CertificateObject))

    $User.putex(4,"userCertificate",@($Cert[0]))

    I didn't try $CertificateObject[0] because that only returns one byte.

    Tuesday, August 26, 2014 4:42 PM
  • You never say what the error is.


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 4:44 PM
  • I would think that you would just assign the cert array.

    $user.userCertificate=@($cert)
    $user.CommitChanges()


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 4:47 PM
  • I would think that you would just assign the cert array.

    $user.userCertificate=@($cert)
    $user.CommitChanges()


    ¯\_(ツ)_/¯


    Yup.  This works.

    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 4:53 PM
  • Sorry about that. Fair point

    [DBG]: PS C:\windows\system32>> $User.putex(4,"userCertificate",@($CertificateObject))
    Exception calling "putex" with "3" argument(s): "Unspecified error
    "
    At line:1 char:35
    + $User.putex(4,"userCertificate",@($CertificateObject))
    +                                   ~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : CatchFromBaseAdapterMethodInvokeTI
     

    [DBG]: PS C:\windows\system32>> $User.userCertificate=@($Cert)
    Exception setting "userCertificate": "Unspecified error
    "
    At line:1 char:1
    + $User.userCertificate=@($Cert)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], SetValueInvocationException
        + FullyQualifiedErrorId : CatchFromBaseAdapterSetValue
     

    [DBG]: PS C:\windows\system32>> $User.userCertificate=@($CertificateObject)
    Exception setting "userCertificate": "Unspecified error
    "
    At line:1 char:1
    + $User.userCertificate=@($CertificateObject)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], SetValueInvocationException
        + FullyQualifiedErrorId : CatchFromBaseAdapterSetValue
     

    [DBG]: PS C:\windows\system32>> $User.userCertificate=@($Cert[0])
    Exception setting "userCertificate": "Unspecified error
    "
    At line:1 char:1
    + $User.userCertificate=@($Cert[0])
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], SetValueInvocationException
        + FullyQualifiedErrorId : CatchFromBaseAdapterSetValue

    Tuesday, August 26, 2014 4:57 PM
  • Same error as noted above. You can see in the errors I sent that I tried this too... I also tried playing around with the format:

    $User.userCertificate="$(@($Cert[0]))"

    That is successful, but damages the store... Lucky I saved the certificate and can fix the user...

    Tuesday, August 26, 2014 5:02 PM
  • Not working here...

    [DBG]: PS C:\windows\system32>> $User.userCertificate=@($Cert)
    Exception setting "userCertificate": "Unspecified error
    "
    At line:1 char:1
    + $User.userCertificate=@($Cert)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], SetValueInvocationException
        + FullyQualifiedErrorId : CatchFromBaseAdapterSetValue

    Tuesday, August 26, 2014 5:08 PM
  • $cert has to be a binary array.

    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 6:20 PM
  • Looking at the problem further...

    My type data for $certificateObject is already a byte array. I don't know if it should need to be converted.

    I can write the same bytes back that I read from Active Directory. For example:

    #This works writing both certificates back.
    $user.userCertificate = $CertificateBlob
    
    #This Doesn't
    $user.userCertificate = $CertificateBlob[1]

    It seems like I need to do something to get the certificate in a different way.


    • Edited by Oldguard Tuesday, August 26, 2014 6:39 PM
    Tuesday, August 26, 2014 6:39 PM
  • What you are seeing is that the array is a special array.  You need to get a certificate blob object and add the certificate to it use the cert class.  Look around for an example.

    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 6:47 PM
  • Like this?
    #This returns the certificate properties successfully
    
    new-object System.Security.Cryptography.X509Certificates.X509Certificate2(,$CertificateBlob[0])
    
    #This does not work.
    $user.usercertificate = new-object System.Security.Cryptography.X509Certificates.X509Certificate2(,$CertificateBlob[0])
    
    #This will write the attribute, but it corrupts the attribute in active directory when I commit it...
    
    $user.usercertificate = @(new-object )System.Security.Cryptography.X509Certificates.X509Certificate2(,$CertificateBlob[0])



    • Edited by Oldguard Tuesday, August 26, 2014 7:03 PM
    Tuesday, August 26, 2014 6:59 PM
  • So I know the bits are right. Here is what I did:

    I exported the blob that I got from AD that goes back in fine:

    $certificateblob | out-file c:\somelocation\file1.txt

    Then I export the two I cannot write back

    $certificateblob[0] | out-file c:\somelocation\file1.txt

    $certificateblob[1] | out-file c:\somelocation\file2.txt

    I then compared the two parts with the original. I am not losing any bits, I am not gaining any bits. The only thing I can figure is that by pulling them individually, I am in some way changing the type.

    So Did this:

    $certificateblob | fl * |  out-file c:\somelocation\Type1.txt

    $certificateblob[0] | fl * |  out-file c:\somelocation\Type2.txt

    The first one reports to be a:

    DeclaredMembers            : {Void Set(Int32, System.Object), System.Object& Address(Int32), System.Object Get(Int32), Void .ctor(Int32)}
    DeclaredMethods            : {Void Set(Int32, System.Object), System.Object& Address(Int32), System.Object Get(Int32)}

    The second one reports this.

    DeclaredMembers            : {Void Set(Int32, Byte), Byte& Address(Int32), Byte Get(Int32), Void .ctor(Int32)}
    DeclaredMethods            : {Void Set(Int32, Byte), Byte& Address(Int32), Byte Get(Int32)}

    Is my problem just a type conversion to get it back to a system object?

    Tuesday, August 26, 2014 7:41 PM
  • You cannot use a text file to store a binary structure.  A cert is a binary structure. 

    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 7:47 PM
  • My assumption based on your earlier responses is that this works for you and not me... Now I am just tryring to understand why...

    I guess I will keep fighting through it tomorrow.

    To me a byte array is just a hex view of a binary number. If they compare the same as a byte array, they should compare the same as a binary number. I know my method is not a binary comparison, but I think for all practical purposes it works to verify that I am not changing anything. Since the two groups of byte arrays both convert to X509Certificate2 and export to file just fine (and can be re-imported manually into AD from the files), at this point I am pretty certain the byte array just needs to be converted to the right format. I am just looking for someone who understands how to properly format the byte array so that the ADSI provider will except it.

    Any one have any ideas?

    Tuesday, August 26, 2014 7:59 PM
  • A byte array is [byte[]] and is no a hex anything.  A hex representation is a text string in the form of hex characters or hex pairs

    0123456789ABCDEF

    0x00 - 0xFF

    Consider the differences here:

    PS >[byte[]]$x=0
    PS >1..255|%{$x+=$_}
    PS > '0x{0:X}' -f $x[10]
    0xA
    PS > $x[10]
    10
    PS >

    byte 10 has a value of 10 or F when represented as a hex string.  Its actual value is 1010 binary.

    You array must be an array of [byte[]] and not a string of hex.


    ¯\_(ツ)_/¯

    Tuesday, August 26, 2014 8:12 PM
  • My friendly neighborhood programmer pointed out the fix for this...

    $user.userCertificate.value = $bytes

    I will post a full example when I clean it up a bit.

    • Marked as answer by Oldguard Wednesday, August 27, 2014 8:25 PM
    Wednesday, August 27, 2014 8:24 PM
  • That is what I posted above.  Assign or PutEx.  It has to be a byte array.  [byte[]]


    ¯\_(ツ)_/¯

    Wednesday, August 27, 2014 8:34 PM
  • You definitely got us going in the right direction with writing directly to the user object, but we didn't need to convert it to a byte array. The export of the certificate is already a byte array. We already had what we needed when we did the following.

    $Bytes = $Cert.Export("Cert")

    All that was needed was to write to the .value of the userCertificate attribute like this:

    $user.userCertificate.value=$Bytes
    $user.CommitChanges()

    If that can be done with putex, I failed to figure out how, but the new solution is actually pretty clean and efficient and we are going to use it...

     
    • Proposed as answer by m_hartmann Friday, December 2, 2016 2:15 PM
    • Unproposed as answer by m_hartmann Friday, December 2, 2016 2:15 PM
    Wednesday, August 27, 2014 9:59 PM
  • You may use this function to just remove some certificates from a user object in ad.

    It also backs up the certificates to .cer files.

    function RemoveCertificate {
    	param( 
    		[string] $Domain,
    		[string] $SamAccountName,
    		[string] $BackupPath,
    		[string[]] $ThumbPrints
    	)
    	$removed = 0
    	$dc = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain((New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('domain',$Domain))).FindDomainController()
    	$searcher = $DC.GetDirectorySearcher()
    	$searcher.PageSize = 50
    	$searcher.PropertiesToLoad.AddRange(("samaccountname"))
    	$searcher.Filter = "(samaccountname={0})" -f $SamAccountName
    	$remove = @()
    	$u = $searcher.FindOne().GetDirectoryEntry()
    	if ($u -ne $null) {
    		try {
    			$certs = $u.GetEx("userCertificate")
    			for ($i=0;$i -lt $certs.count;$i++) {
    				$c = [System.Security.Cryptography.X509Certificates.X509Certificate2] $certs[$i]
    				if ( $ThumbPrints -contains $c.Thumbprint ) {
    					$remove += ,$certs[$i]
    					$file = "{0}\{1}-{2}-{3}.cer" -f $BackupPath,$Domain,$SamAccountName,$c.Thumbprint
    					[io.file]::WriteAllBytes($file ,$certs[$i]);
    				}
    			}
    			if ($remove.count -gt 0) {
    				$u.PutEx(4,"userCertificate",$remove)
    				$u.SetInfo()
    			}
    			$removed = $remove.count
    		}
    		finally {
    			$removed
    		}
    	}
    }


    • Edited by m_hartmann Friday, December 2, 2016 2:19 PM
    • Proposed as answer by m_hartmann Friday, December 2, 2016 3:07 PM
    Friday, December 2, 2016 2:17 PM